Symmetrical editing fo a M2M relation in Django admin ¶
Given:
class Team(models.Model): name = models.CharField(_('name'), max_length=150, unique=True) jobs = models.ManyToManyField(Job, verbose_name=u'Jobs', blank=True, related_name='teams') @admin.register(Team) class TeamAdmin(admin.ModelAdmin): filter_horizontal = ['jobs',] ...
Django already provides a widget for editing the relation in the Team change form.
To have a similar behaviour in the Job change form as well, proceed as follows:
from django import forms from django.contrib.admin.widgets import FilteredSelectMultiple class JobAdminForm(forms.ModelForm): teams = forms.ModelMultipleChoiceField( queryset=Team.objects.all(), required=False, widget=FilteredSelectMultiple( verbose_name=_('Teams'), is_stacked=False ) ) class Meta: model = Job exclude = [] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance and self.instance.pk: self.fields['teams'].initial = self.instance.teams.all() def save(self, commit=True): job = super().save(commit=commit) if commit: job.teams = self.cleaned_data['teams'] else: old_save_m2m = self.save_m2m def new_save_m2m(): old_save_m2m() job.teams.set(self.cleaned_data['teams']) self.save_m2m = new_save_m2m return job @admin.register(Job) class JobAdmin(BaseModelAdmin): form = JobAdminForm ...
Similarly, you can even add multiple M2M relation to the JobAdminForm:
class JobAdminForm(forms.ModelForm): users = forms.ModelMultipleChoiceField( queryset=User.objects.all(), required=False, widget=FilteredSelectMultiple( verbose_name=_('Users'), is_stacked=False ) ) teams = forms.ModelMultipleChoiceField( queryset=Team.objects.all(), required=False, widget=FilteredSelectMultiple( verbose_name=_('Teams'), is_stacked=False ) ) class Meta: model = Job exclude = [] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) if self.instance and self.instance.pk: self.fields['users'].initial = self.instance.users.all() self.fields['teams'].initial = self.instance.teams.all() def save(self, commit=True): job = super().save(commit=commit) if commit: job.users = self.cleaned_data['users'] job.teams = self.cleaned_data['teams'] else: old_save_m2m = self.save_m2m def new_save_m2m(): old_save_m2m() job.users.set(self.cleaned_data['users']) job.teams.set(self.cleaned_data['teams']) self.save_m2m = new_save_m2m return job