-
-
Save Grokzen/a64321dd69339c42a184 to your computer and use it in GitHub Desktop.
# Based on post from: https://snipt.net/chrisdpratt/symmetrical-manytomany-filter-horizontal-in-django-admin/#L-26 | |
# Only reposting to avoid loosing it. | |
""" | |
When adding a many-to-many (m2m) relationship in Django, you can use a nice filter-style multiple select widget to manage entries. However, Django only lets you edit the m2m relationship this way on the forward model. The only built-in method in Django to edit the reverse relationship in the admin is through an InlineModelAdmin. | |
Below is an example of how to create a filtered multiple select for the reverse relationship, so that editing entries is as easy as in the forward direction. | |
IMPORTANT: I have no idea for what exact versions of Django this will work for, is compatible with or was intended for. | |
I am sure this have stopped working slightly for new:er versions of Django. I do not use this myself currently with any new code so i can't really tell for sure. | |
!! Use code at your own risk !! | |
""" | |
### pizza/models.py ### | |
from django.db import models | |
class Pizza(models.Model): | |
name = models.CharField(max_length=50) | |
toppings = models.ManyToManyField(Topping, related_name='pizzas') | |
class Topping(models.Model): | |
name = models.CharField(max_length=50) | |
### pizza/admin.py ### | |
from django import forms | |
from django.contrib import admin | |
from django.utils.translation import ugettext_lazy as _ | |
from django.contrib.admin.widgets import FilteredSelectMultiple | |
from .models import Pizza, Topping | |
class PizzaAdmin(admin.ModelAdmin): | |
filter_horizonal = ('toppings',) | |
class ToppingAdminForm(forms.ModelForm): | |
pizzas = forms.ModelMultipleChoiceField( | |
queryset=Pizza.objects.all(), | |
required=False, | |
widget=FilteredSelectMultiple( | |
verbose_name=_('Pizzas'), | |
is_stacked=False | |
) | |
) | |
class Meta: | |
model = Topping | |
def __init__(self, *args, **kwargs): | |
super(ToppingAdminForm, self).__init__(*args, **kwargs) | |
if self.instance and self.instance.pk: | |
self.fields['pizzas'].initial = self.instance.pizzas.all() | |
def save(self, commit=True): | |
topping = super(ToppingAdminForm, self).save(commit=False) | |
if commit: | |
topping.save() | |
if topping.pk: | |
topping.pizzas = self.cleaned_data['pizzas'] | |
self.save_m2m() | |
return topping | |
class ToppingAdmin(admin.ModelAdmin): | |
form = ToppingAdminForm | |
admin.site.register(Pizza, PizzaAdmin) | |
admin.site.register(Topping, ToppingAdmin) |
This does not work on create as topping wont have a pk
@dickermoshe I have not run or used this gist since it was created so i can't say if it is broken or not when running in any new django or python version. If you have a patch to make it work, please post it and i can update the gist with it
I think the "Calendar Events" below are as broken as editing pizzas from a toppings record.
I have
class Technique:
videos = models.ManyToManyField(Video, related_name="techniques", blank=True)
class CalendarEvent:
techniques = models.ManyToManyField(Technique, related_name="calendar_events", blank=True)
In the screenshot you can see:
class TechniqueAdmin:
filter_horizontal = ("videos",)
# Manages the reverse relation: technique.calendar_events
form = TechniqueAdminForm
Videos are managed inside Techniques and displayed through a filter_horizontal
, so everything works correctly. CalendarEvents are displayed through a manually defined form like ToppingAdminForm
and the resulting HTML is different from the preceding row.
Do you know how to fix this?
@seguri The resulting HTML is different because Django wraps the widget with RelatedFieldWidgetWrapper
. https://github.com/django/django/blob/0a560eab550696dbc163d57258ef6f3cdb9511a3/django/contrib/admin/options.py#L213
I got around this by subclassing FilteredSelectMultiple
and specifying a custom template
class ReverseFilterSelectMultiple(FilteredSelectMultiple):
template_name = "admin/widgets/reverse_filter_select_multiple.html"
Or better yet, just use RelatedFieldWidgetWrapper
. That way you would get the plus icon to add new model instances. I was having issues at first, but I think something along these lines would work.
from django.contrib import admin
from django.contrib.admin.widgets import FilteredSelectMultiple, RelatedFieldWidgetWrapper
from django.db import ManyToManyRel, ManyToManyField
RelatedFieldWidgetWrapper(
FilteredSelectMultiple(
verbose_name='Calendar Events',
is_stacked=False,
),
rel=ManyToManyRel(
field=ManyToManyField(Video),
to=CalendarEvent,
through=CalendarEvent.through,
),
admin_site=admin.site,
)
Thanks!