6

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.

Symmetric
  • 4,495
  • 5
  • 32
  • 50
  • possible duplicate of [django Cannot set values on a ManyToManyField which specifies an intermediary model. Use Manager instead](http://stackoverflow.com/questions/3091328/django-cannot-set-values-on-a-manytomanyfield-which-specifies-an-intermediary-mo) – juliocesar May 26 '14 at 20:42

5 Answers5

8

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

Community
  • 1
  • 1
ecdani
  • 361
  • 3
  • 14
  • I haven't had a chance to test this (and I ended up going down a different route), but it looks like the correct method. Cheers! – Symmetric Jun 05 '13 at 17:08
  • This doesn't give you any chance to enter `date_joined` and `invite_reason`...it just saves them empty... – ZAD-Man Aug 05 '14 at 22:52
2
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)
Leszek Zarna
  • 3,253
  • 26
  • 26
1

'For reference, I didn't end up using a class-based view, instead I did something like this:

def group_create(request):
    group_form = GroupForm(request.POST or None)
    if request.POST and group_form.is_valid():
        group = group_form.save(commit=False)
        membership_formset = MembershipFormSet(request.POST, instance=group)
        if membership_formset.is_valid():
            group.save()
            membership_formset.save()
            return redirect('success_page.html')
    else:
        # Instantiate formset with POST data if this was a POST with an invalid from,
        # or with no bound data (use existing) if this is a GET request for the edit page.
        membership_formset = MembershipFormSet(request.POST or None, instance=Group())

    return render_to_response(
        'group_create.html',
        {
            'group_form': recipe_form,
            'membership_formset': membership_formset,
        },
        context_instance=RequestContext(request),
    )

This may be a starting point for a Class-based implementation, but it's simple enough that it's not been worth my while to try to shoehorn this into the Class-based paradigm.

Symmetric
  • 4,495
  • 5
  • 32
  • 50
0

I was facing pretty the same problem just a few days ago. Django has problems to process intermediary m2m relationships.

This is the solutions what I have found useful:

1. Define new CreateView
class GroupCreateView(CreateView):
    form_class = GroupCreateForm
    model = Group
    template_name = 'forms/group_add.html'
    success_url = '/thanks'

Then alter the save method of defined form - GroupCreateForm. Save is responsible for making changes permanent to DB. I wasn't able to make this work just through ORM, so I've used raw SQL too:

1. Define new CreateView
class GroupCreateView(CreateView):


class GroupCreateForm(ModelForm):
    def save(self):
        # get data from the form
        data = self.cleaned_data
        cursor = connection.cursor()
        # use raw SQL to insert the object (in your case Group)
        cursor.execute("""INSERT INTO group(group_id, name)
                          VALUES (%s, %s);""" (data['group_id'],data['name'],))
        #commit changes to DB
        transaction.commit_unless_managed()
        # create m2m relationships (using classical object approach)
        new_group = get_object_or_404(Group, klient_id = data['group_id'])
        #for each relationship create new object in m2m entity
        for el in data['members']:
            Membership.objects.create(group = new_group, membership = el)
        # return an object Group, not boolean!
        return new_group

Note:I've changed the model a little bit, as you can see (i have own unique IntegerField for primary key, not using serial. That's how it got into get_object_or_404

Boun
  • 123
  • 1
  • 10
  • This answer does not help much. Can you show your model? Changing the pk from "id" to "group_id" does not make much sense. – Timo Sep 28 '14 at 08:00
0

Just one comment, when using CBV you need to save the form with commit=True, so the group is created and an id is given that can be used to create the memberships. Otherwise, with commit=False, the group object has no id yet and an error is risen.

kiril
  • 4,914
  • 1
  • 30
  • 40
  • Any chance of getting more info on this...? Following the accepted answer, the only way I can get to the code to save the `Membership` is by using `commit=False`, otherwise it throws the `AttributeError` shown in the question. In fact, even if I try to use `form.save()` after saving the `Membership`, I still get that `AttributeError`. However, like you say, there is no `id` for `group`, so `Membership` isn't being saved properly anyway... – ZAD-Man Aug 06 '14 at 17:47
  • What I meant is that the solution proposed was not going to work, because as you checked, the group instance is not saved to DB yet, so it has no id. What you have to do is create a new group just with a name and save it (commit=True). Now it has an id, and you can create new Membership objects. About the error, I didn't try the code so I don't know the exact reason. Maybe, the autogenerated form includes a mandatory field with members... Do you have any more details on this? – kiril Aug 07 '14 at 10:59
  • Creating a new group with new id seems to be good, but how can I enter date_joined and invite_reason as Zad-Man said? Has someone a complete solution with view, model. I think it might help to see how the "admin" app solved it. – Timo Sep 28 '14 at 08:11