Skip to content

Instantly share code, notes, and snippets.

@Wtower
Last active December 8, 2020 06:16
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save Wtower/0b181cc06f816e4feac14e7c0aa2e9d0 to your computer and use it in GitHub Desktop.
Save Wtower/0b181cc06f816e4feac14e7c0aa2e9d0 to your computer and use it in GitHub Desktop.
Bi-directional many-to-many relationship in django admin
class PageTypeForm(forms.ModelForm):
""" Override default page type form to show related blocks
This shows both ends of m2m in admin
First add a custom ModelMultipleChoiceField
Remove the widget to not use the double list widget
https://www.lasolution.be/blog/related-manytomanyfield-django-admin-site.html
https://github.com/django/django/blob/master/django/contrib/admin/widgets.py#L24
"""
blocks = forms.ModelMultipleChoiceField(
ContentBlock.objects.all(),
widget=FilteredSelectMultiple("Blocks", True),
required=False,
)
def __init__(self, *args, **kwargs):
""" Initialize form
If this is an existing object, load related
:param args
:param kwargs
:return: None
"""
super(PageTypeForm, self).__init__(*args, **kwargs)
if self.instance.pk:
self.initial['blocks'] = self.instance.blocks.values_list('pk', flat=True)
def save(self, *args, **kwargs):
""" Handle saving of related blocks
:param args
:param kwargs
:return: instance
"""
instance = super(PageTypeForm, self).save(*args, **kwargs)
if instance.pk:
for block in instance.blocks.all():
if block not in self.cleaned_data['blocks']:
instance.blocks.remove(block)
for block in self.cleaned_data['blocks']:
if block not in instance.blocks.all():
instance.blocks.add(block)
return instance
class Meta:
""" Meta class """
model = PageType
fields = ['name', 'description', 'guidelines', 'url_pattern']
from django import forms
from django.contrib.admin.widgets import FilteredSelectMultiple
class ModelBiMultipleChoiceField(forms.ModelMultipleChoiceField):
""" This shows both ends of m2m in admin """
def __init__(self, queryset, required=False, widget=None, label=None, initial=None, help_text='',
double_list=None, *args, **kwargs):
""" First add a custom ModelMultipleChoiceField
Specify a `double_list` label in order to use the double list widget
Field name should be the same with model's m2m field
https://www.lasolution.be/blog/related-manytomanyfield-django-admin-site.html
https://github.com/django/django/blob/master/django/contrib/admin/widgets.py#L24
"""
if double_list:
widget = FilteredSelectMultiple(double_list, True)
super(ModelBiMultipleChoiceField, self).__init__(
queryset, required, widget, label, initial, help_text, *args, **kwargs)
class ManyToManyModelForm(forms.ModelForm):
""" This is a generic form to use with the ModelBiMultipleChoiceField """
def __init__(self, *args, **kwargs):
""" Initialize form
:param args
:param kwargs
:return: None
"""
super(ManyToManyModelForm, self).__init__(*args, **kwargs)
# If this is an existing object, load related
if self.instance.pk:
# browse through all form fields and pick the ModelBiMultipleChoiceField
for field_name in self.base_fields:
field = self.base_fields[field_name]
if type(field).__name__ == 'ModelBiMultipleChoiceField':
# Get instance property dynamically
# field should be same name with reverse model (ie. form.blocks vs instance.blocks)
self.initial[field_name] = getattr(self.instance, field_name).values_list('pk', flat=True)
# # Use the following to add an add new block icon
# from django.db.models import ManyToManyRel
# from django.contrib import admin
# rel = ManyToManyRel(ContentBlock, PageType)
# self.fields['blocks'].widget = RelatedFieldWidgetWrapper(self.fields['blocks'].widget, rel, admin.site)
def save(self, *args, **kwargs):
""" Handle saving of related
:param args
:param kwargs
:return: instance
"""
instance = super(ManyToManyModelForm, self).save(*args, **kwargs)
if instance.pk:
# browse through all form fields and pick the ModelBiMultipleChoiceField
for field_name in self.base_fields:
field = self.base_fields[field_name]
if type(field).__name__ == 'ModelBiMultipleChoiceField':
# the m2m records, eg if model field is `blocks`, this would be `instance.blocks.all()`
recordset = getattr(self.instance, field_name)
records = recordset.all()
# remove records that have been removed in form
for record in records:
if record not in self.cleaned_data[field_name]:
recordset.remove(record)
# add records that have been added in form
for record in self.cleaned_data[field_name]:
if record not in records:
recordset.add(record)
return instance
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment