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.
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
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 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
'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.
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.
来源:https://stackoverflow.com/questions/12224442/class-based-views-for-m2m-relationship-with-intermediate-model