What are the steps to make a ModelForm work with a ManyToMany relationship with an intermediary model in Django?

10,036

Solution 1

…
if form.is_valid():
    client_mod = form.save(commit=False)
    client_mod.save()
    for groupe in form.cleaned_data.get('groupes'):
        clientgroupe = ClientGroupe(client=client_mod, groupe=groupe)
        clientgroupe.save()
    …

Solution 2

If you use the save method right now, Django will try to save using the manager (which Django doesn't allow). Unfortunately, the behavior you want is a little bit trickier than what ModelForm does by default. What you need to do is create a formset.

First of all, you will need to change the options of your ClientForm so that it doesn't display the groupes attribute.

class ClientForm(ModelForm):
    class Meta:
        model = Client
        exclude = ('groupes',)

Next, you must change the view to display the formset:

from django.forms.models import inlineformset_factory

def modifier(request, id):
    client = Client.objects.get(id=id)    
    form = ClientForm(instance = client)
    # Create the formset class
    GroupeFormset = inlineformset_factory(Client, Groupe)
    # Create the formset
    formset = GroupeFormset(instance = client)

    dict = {
        "form": form
        , "formset" : formset
        , "instance" : client
    }

    if request.method == "POST":
        form = ClientForm(request.POST, instance = client)
        formset = GroupeFormset(request.POST, instance = client)

        if form.is_valid() and formset.is_valid():
            client_mod = form.save()
            formset.save()

            id = client_mod.id
            return HttpResponseRedirect(
                "/client/%(id)s/?err=success" % {"id" : id}
            )
        else:
            return HttpResponseRedirect(
                "/client/%(id)s/?err=warning" % {"id" : id}
            )

    return render_to_response(
        "client/modifier.html"
        , dict
        , context_instance=RequestContext(request)
    )

And obviously, you must also tweak your template to render the formset.

If you need any other advice on formsets, see these articles:

Model formsets
Formsets

Solution 3

You probably need to remove the ManyToMany field from your Client model, or else carefully exclude it from your form. Unfortunately, the default widget for the ManyToMany field cannot populate the ClientGroupe Model properly (even if the missing field, dt, had been set to autonow=True). This is something you'll either need to break out into another form, or handle in your view.

Share:
10,036
kronwied
Author by

kronwied

Walking reader. generalist.

Updated on June 19, 2022

Comments

  • kronwied
    kronwied almost 2 years
    • I have a Client and Groupe Model.
    • A Client can be part of multiple groups.
    • Clients that are part of a group can use its group's free rental rate at anytime but only once. That is where the intermediary model (ClientGroupe) comes in with that extra data.

    For now, when I try to save the m2m data, it just dies and says I should use the ClientGroupe Manager...so what's missing?

    Here are my models:

    class Groupe(models.Model):
        nom = models.CharField(max_length=1500, blank=True)
    
    class Client(models.Model):
        nom = models.CharField(max_length=450, blank=True)
        prenom = models.CharField(max_length=450, blank=True)
        groupes = models.ManyToManyField(Groupe, null = True, blank = True, through='ClientGroupe')
    
    class ClientGroupe(models.Model):
        client = models.ForeignKey(Client)
        groupe = models.ForeignKey(Groupe)
        dt = models.DateField(null=True, blank=True) # the date the client is using its group's free rental rate    
    
        class Meta:
            db_table = u'clients_groupes'
    

    and here's my view:

    def modifier(request, id):
        client = Client.objects.get(id=id)    
        form = ClientForm(instance = client)
    
        dict = {
            "form": form
            , "instance" : client
        }
    
        if request.method == "POST":
            form = ClientForm(request.POST, instance = client)
    
            if form.is_valid():
                client_mod = form.save()
    
                id = client_mod.id
                return HttpResponseRedirect(
                    "/client/%(id)s/?err=success" % {"id" : id}
                )
            else:
                return HttpResponseRedirect(
                    "/client/%(id)s/?err=warning" % {"id" : id}
                )
    
        return render_to_response(
            "client/modifier.html"
            , dict
            , context_instance=RequestContext(request)
        )
    

    EDIT:

    and here's the ClientForm code:

    class ClientForm(ModelForm):
        class Meta:
            model = Client
    

    EDIT #2: here's the error message:

    AttributeError at /client/445/
    
    Cannot set values on a ManyToManyField which specifies an intermediary model. Use ClientGroupe's Manager instead.
    
    Request Method:     POST
    Request URL:    http://localhost/client/445/
    Exception Type:     AttributeError
    Exception Value:    Cannot set values on a ManyToManyField which specifies an intermediary model.  Use ClientGroupe's Manager instead.
    
    Exception Location:     C:\Python25\lib\site-packages\django\db\models\fields\related.py  in __set__, line 574
    Python Executable:  C:\xampp\apache\bin\apache.exe
    Python Version:     2.5.2