Manage M2M field with check boxes

Dear all,
I’m trying to manage a model form where one of the filed comes from a M2M relationship. I would like to know if what I intend to do can be done “naturally”, as a solution I imagine looks to me a bit “nasty”, with javascript and hidden field.
I have 2 models : Event, on which the form is based, and UserGroup, linked to Event with M2M relationship. To create / update an event, I would like to have a list with as many groups as defined, and a checkbox in front of each group to allow the user to select groups he wants to be part of the event.
Here is the form as I defined it yet:

class EventDetail(forms.ModelForm):
    groups = forms.ModelMultipleChoiceField(
        label = "Liste des groupes",
        queryset = None,
        widget = forms.CheckboxSelectMultiple,
        required = False
        )

    class Meta:
        model = Event
        fields = ['event_name', 'event_date', 'quorum', 'rule']

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        instance = kwargs.get('instance', None)
        self.fields['groups'].queryset= UserGroup.objects.\
                                                filter(company=instance.company, hidden=False).\
                                                order_by('group_name')

Then comes the first question: how to manage the display, and add information regarding each group? I mean, groups have dedicated attributes, and I would like to be able to display some of them.
At this stage, the solution I implemented is to define an __str__() method dedicated to this display.

But, while I was at the beginning focused on this first question, I was able to display check boxes, and even to manage users choices (when he (un)selected some choices, the POST updated database accordingly.
Unfortunately, I was convinced it was “natural”, and now that I decided to use this workaround, I’m not able anymore to display, even less manage check boxes.

Here is my view (please do not pay attention to formsets… this will probably be the subject of my next publication here ^^):

@user_passes_test(lambda u: u.is_superuser or (u.id is not None and u.usercomp.is_admin))
def adm_event_detail(request, comp_slug, evt_id=0):
    company = Company.get_company(request.session['comp_slug'])
    QuestionFormset = formset_factory(QuestionDetail, extra=3)

    if evt_id > 0:
        current_event = Event.objects.get(id=evt_id)
        event_form = EventDetail(request.POST or None, instance=current_event)
        question_set = QuestionFormset(initial=Question.objects.filter(event=current_event))

    else:
        event_form = EventDetail(request.POST or None)
        question_set = QuestionFormset()

    if request.method == 'POST':
        if any(event_form.is_valid(), question_set.is_valid()):
            if evt_id == 0:
                # Create new event
                event_data = {
                    "company": company,
                    "groups": event_form.cleaned_data["groups"],
                    "event_name": event_form.cleaned_data["event_name"],
                    "event_date": event_form.cleaned_data["event_date"],
                    "quorum": event_form.cleaned_data["quorum"],
                    "rule":event_form.cleaned_data["rule"]
                }
                new_event = Event.create_event(event_data)
            else:
                new_event = event_form.save()

            for item in question_set:
                if item.cleaned_data:
                    question = item.save()
                    new_event
        else:
            print("****** FORMULAIRE NON VALIDE *******")
            print(event_form.errors)

    return render(request, "polls/adm_event_detail.html", locals())

Finally, here is the part of the template where the form is supposed to be displayed:

    <ul style="list-style-type: none">
    {% for grp in event_form.groups %}
        <li>{{ grp }}</li>
    {% endfor %}
    </ul>

What I’m looking for is:

  • groups already defined as part of the event are checked
  • when I submit the form, the list of groups linked to the event is updated according to user’s choice, and the display after validation presents the latest status. At this stage, I have only empty check boxes

Many thanks in advance
PS: I’m using Django 2.2 so far

Take a look at the CheckboxSelectMultiple widget. It pretty much covers everything you’re looking for.

Sorry, but your answer doesn’t help at all :-/

Anyway, I found the solution by myself, and I would like to share it here, hoping this could help if anybody encounter the same problems.

As far as I included some context data, I moved all the stuff in my view. It was not part of the question, but to give a complete overview, the new form is defined like this:

class EventDetail(forms.ModelForm):
    groups = forms.ModelMultipleChoiceField(
        label = "Liste des groupes",
        queryset = None,
        widget = forms.CheckboxSelectMultiple,
        required = False
        )

    class Meta:
        model = Event
        fields = ['event_name', 'event_date', 'quorum', 'rule']

Let’s come to the main subject: initialize the form and being able to validate it.
Initialization: defining the queryset is not enough, it only define the list of values to be displayed. To put the initial values, you need to use the initial attribute. It might be evident for most of you, but reading the doc I thought it was a difference between a form linked to a model and a one that is not, in other words, I thought we might choose between queryset and initial attribute. I was wrong, here I need both.
Then, to validate the form, I need to manage data related to this M2M relationship separately. This means, a form.save() statement is not enough and will save nothing for this field. To manage this field, I need to add() each value… but do not forget to remove the ones that could be no longer be linked to the main object!

According to this, here is my final working view:

@user_passes_test(lambda u: u.is_superuser or (u.id is not None and u.usercomp.is_admin))
def adm_event_detail(request, comp_slug, evt_id=0):
    company = Company.get_company(comp_slug)

    if evt_id > 0:
        current_event = Event.objects.get(id=evt_id)
        event_form = EventDetail(request.POST or None, instance=current_event)
        event_form.fields['groups'].initial= current_event.groups.all()

    else:
        event_form = EventDetail(request.POST or None)

    event_form.fields['groups'].queryset= UserGroup.objects.\
                                            filter(company=company, hidden=False).\
                                            order_by('group_name')
    
    if request.method == 'POST':
        if event_form.is_valid():
            if evt_id == 0:
                # Create new event
                event_data = {
                    "company": company,
                    "groups": event_form.cleaned_data["groups"],
                    "event_name": event_form.cleaned_data["event_name"],
                    "event_date": event_form.cleaned_data["event_date"],
                    "quorum": event_form.cleaned_data["quorum"],
                    "rule":event_form.cleaned_data["rule"]
                }
                new_event = Event.create_event(event_data)
            else:
                new_event = event_form.save()
                new_event.groups.clear()
                new_event = event_form.save()
                new_event.groups.add(*event_form.cleaned_data['groups'])

        else:
            print("****** FORMULAIRE NON VALIDE *******")
            print(event_form.errors)

    return render(request, "polls/adm_event_detail.html", locals())

And I updated the template, as the loop is actually not necessary (unless anyone could propose me another idea to display group details); the equivalent of what I put in the first post is now this single line of code:

        {{ event_form.groups }}

I just added a CSS property to hide list’s bullet points:

#id_groups {
  list-style-type: none;
}

It might be possible to make the view simpler with other functions - I would be very interested to know! - but at least it works for me =)