djangomanytomanyfield

Django: ModelMultipleChoiceField not working when Form's model is not the one where a ManyToMany field is defined but "the other class"


I am trying to create an event staff management tool in Django. One part of the data model is that a staff member (class Staff) is linked to a scheduled shift (ScheduledEventShift) via ManyToManyField through StaffShift.

Every member of staff can see the shifts available for him / her and select them via ModelMultipleChoiceField. This works perfectly fine and the relation is correctly written to the StaffShift table depending on whether the shift is ticked or not.

(sorry the tool is in German, but it should be clear from context what I mean)

enter image description here

enter image description here

What I want to do now is "the opposite" of that, i.e. I as an admin want to look at a single shift, see which staff members have applied for that shift and untick those that are "too much" (i.e. 2 required, 3 applied).

When using a ModelMultipleChoiceField in that direction, I am able to pick exactly those staff members that I should pick in the QuerySet (this is all fine) but all of them are always unticked (which should not be as they are exactly those that are in the StaffShift through table for that particular shift) and it has no effect whatsoever on the StaffShift table if I change the ticks.

enter image description here

Here the relevant part of my code:

models.py:

class Staff(models.Model):
    <...>
    shifts = models.ManyToManyField(ScheduledEventShift, through='StaffShift', related_name='applied_staff')

forms.py:

class ScheduledEventShiftEditForm(forms.ModelForm):

    class Meta:
        model = ScheduledEventShift
        fields = ['shift_count_required', 'applied_staff', 'event_shift_info']

    shift_count_required = models.IntegerField()
    applied_staff = forms.ModelMultipleChoiceField(
        queryset=None,
        widget=forms.CheckboxSelectMultiple)

views.py:

class ScheduledEventShiftUpdateView(ObjectUpdateView):
    model = ScheduledEventShift
    form_class = ScheduledEventShiftEditForm
    <...>

    def get_form(self, form_class=None):
        form = super().get_form(form_class=self.form_class)
        form.fields['applied_staff'].queryset = ScheduledEventShift.objects.filter(
            id=self.kwargs.get('pk')).first().applied_staff.all()

Is such a ManyToMany relationship with through table not perfectly symmetrical (i.e. connected models can be used the same way from both sides)? Do I have to do this in a different way when my model for the form is not the class where the ManyToManyField and the through table are defined but "the other class"? Thanks a lot


Edit: I was able to create a maximally reduced example from scratch where I encounter exactly the same issue (I wanted to make sure that nothing in the rest of the code has some side effect). For this example I will post the full .py files. StaffUpdateView/Form works as intended -- it saves the changes to StaffShift. ShiftUpdateView/Form shows all the staff but no ticks and also saves no changes.

models.py

from django.db import models

class Shift(models.Model):
    shift_name = models.CharField(max_length=32)

class Staff(models.Model):
    staff_name = models.CharField(max_length=32)
    shifts = models.ManyToManyField(Shift, through='StaffShift', related_name='staff')

class StaffShift(models.Model):
    staff = models.ForeignKey(Staff, on_delete=models.CASCADE)
    shift = models.ForeignKey(Shift, on_delete=models.CASCADE)

views.py:

from django.views.generic import UpdateView

from .forms import *

class StaffUpdateView(UpdateView):
    form_class = StaffUpdateForm
    template_name = 'playground/base_form.html'
    model = Staff

    def get_success_url(self):
        return f'/staff/{self.get_object().id}/'

class ShiftUpdateView(UpdateView):
    form_class = ShiftUpdateForm
    template_name = 'playground/base_form.html'
    model = Shift
    def get_success_url(self):
        return f'/shift/{self.get_object().id}/'

forms.py:

from django import forms

from .models import *

class StaffUpdateForm(forms.ModelForm):

    class Meta:
        model = Staff
        fields = ['staff_name', 'shifts']

    staff_name = forms.CharField(max_length=32)
    shifts = forms.ModelMultipleChoiceField(
        queryset=Shift.objects.all(),
        widget=forms.CheckboxSelectMultiple)

class ShiftUpdateForm(forms.ModelForm):

    class Meta:
        model = Shift
        fields = ['shift_name', 'staff']

    shift_name = forms.CharField(max_length=32)
    staff = forms.ModelMultipleChoiceField(
        queryset=Staff.objects.all(),
        widget=forms.CheckboxSelectMultiple)

Solution

  • Because staff is not a field on the Shift model, we have to manually set the initial value for the field and also save the changes manually too

    class ShiftUpdateForm(ModelForm):
    
        class Meta:
            model = Shift
            fields = ['shift_name', 'staff']
    
        shift_name = forms.CharField(max_length=32)
        staff = forms.ModelMultipleChoiceField(
            queryset=Staff.objects.all(),
            widget=forms.CheckboxSelectMultiple
        )
    
        def __init__(self, *args, **kwargs):
            super().__init__(*args, **kwargs)
            self.fields['staff'].initial = self.instance.staff.all()
    
        def save(self, *args, **kwargs):
            instance = super().save(*args, **kwargs)
            instance.staff.set(self.cleaned_data['staff'])
            return instance