Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Class-based views for M2M relationship with intermediate model

I have a M2M relationship between two Models which uses an intermediate model. For the sake of discussion, let's use the example from the manual:

class Person(models.Model):
    name = models.CharField(max_length=128)

    def __unicode__(self):
        return self.name

class Group(models.Model):
    name = models.CharField(max_length=128)
    members = models.ManyToManyField(Person, through='Membership')

    def __unicode__(self):
        return self.name

class Membership(models.Model):
    person = models.ForeignKey(Person)
    group = models.ForeignKey(Group)
    date_joined = models.DateField()
    invite_reason = models.CharField(max_length=64)

I'd like to make use of Django's Class-based views, to avoid writing CRUD-handling views. However, if I try to use the default CreateView, it doesn't work:

class GroupCreate(CreateView):
    model=Group

This renders a form with all of the fields on the Group object, and gives a multi-select box for the members field, which would be correct for a simple M2M relationship. However, there is no way to specify the date_joined or invite_reason, and submitting the form gives the following AttributeError:

"Cannot set values on a ManyToManyField which specifies an intermediary model. Use Membership's Manager instead."

Is there a neat way to override part of the generic CreateView, or compose my own custom view to do this with mixins? It feels like this should be part of the framework, as the Admin interface atomatically handles M2M relationships with intermediates using inlines.

like image 385
Symmetric Avatar asked Sep 01 '12 02:09

Symmetric


2 Answers

You must extend CreateView:

from django.views.generic import CreateView

class GroupCreate(CreateView):
    model=Group

and override the form_valid():

from django.views.generic.edit import ModelFormMixin
from django.views.generic import CreateView

class GroupCreate(CreateView):
    model = Group

    def form_valid(self, form):
        self.object = form.save(commit=False)
        for person in form.cleaned_data['members']:
            membership = Membership()
            membership.group = self.object
            membership.person = person
            membership.save()
        return super(ModelFormMixin, self).form_valid(form)

As the documentation says, you must create new memberships for each relation between group and person.

I saw the form_valid override here: Using class-based UpdateView on a m-t-m with an intermediary model

like image 131
ecdani Avatar answered Nov 01 '22 01:11

ecdani


class GroupCreate(CreateView):
    model = Group

    def form_valid(self, form):
        self.object = form.save(commit=False)

        ### delete current mappings
        Membership.objects.filter(group=self.object).delete()

        ### find or create (find if using soft delete)
        for member in form.cleaned_data['members']:
            x, created = Membership.objects.get_or_create(group=self.object, person=member)
            x.group = self.object
            x.person = member
            #x.alive = True # if using soft delete
            x.save()
        return super(ModelFormMixin, self).form_valid(form)
like image 2
Leszek Zarna Avatar answered Nov 01 '22 01:11

Leszek Zarna