Class-based views for M2M relationship with interm

2020-02-04 21:14发布

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.

5条回答
祖国的老花朵
2楼-- · 2020-02-04 21:41

'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.

查看更多
▲ chillily
3楼-- · 2020-02-04 21:55
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)
查看更多
可以哭但决不认输i
4楼-- · 2020-02-04 21:57

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.

查看更多
做自己的国王
5楼-- · 2020-02-04 21:59

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

查看更多
我只想做你的唯一
6楼-- · 2020-02-04 22:06

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

查看更多
登录 后发表回答