You are here: Home Tech Django forms with ManyToManyFields

Django forms with ManyToManyFields

by Chris Shenton last modified May 12, 2009 06:48 PM
Saving the form data with the many-to-many relations is subtle if you're not using save() in the normal way: your relations aren't committed to the DB.

I have a model with a couple ManyToManyFields:

class Container(FileDropContent):
    days        = models.IntegerField(_('Lifetime'),            default=FILEDROP_DAYS, help_text=_('Default days until contained files will expire'))
    path        = models.CharField(_('path'),                   max_length=255, default=None)
    parent      = models.ForeignKey('self',                     related_name='folders', blank=True, null=True) # allow blank for Django admin FileDrop
    coowners    = models.ManyToManyField(User,                  related_name='coowner_of_filedrop',)
    members     = models.ManyToManyField(User,                  related_name='member_of_filedrop',)

and generate a form from it:

class FileDropForm(forms.ModelForm):
    class Meta:
        model = Container
        fields = ['title', 'days', 'description', 'coowners', 'members'] 

There are some fields mentioned in the form that aren't shown in the Model, as the model is inheriting from some abstract base classes, but that's not the issue here

The form gets generated within my template and automagically populates the 'coowners' and 'members' from my Django Users table.  I can multiselect them and submit the form to my view. This is where the confusion begins.

Since I have some fields that I want to give my model that are not gathered from the form (user and IP address tracking), I have to first save the form with commit disabled, then populate my extra fields, then save to the DB. The problem is that my 'coowner' and 'members' fields don't get saved to the DB.  It does work if I create my object through Django Admin, so I'm being naive about something.

Inserting debugging shows that the fields are multiselected in the incoming form HTML and are available (as lists) in the form.cleaned_data. If I try and do something like:

filedrop.coowners = form.cleaned_data['coowners']

before the filedrop.save() it complains 

'Container' instance needs to have a primary key value before a many-to-many relationship can be used.

This finally lead me to Django docs about this use case: using uncommitted save() with many-to-many relations, and this solution:

def filedrop_create(request):
    if request.method == "POST":
        form = FileDropForm(request.POST)
        if form.is_valid():
            filedrop            = form.save(commit=False)
            filedrop.parent     = None
            filedrop.creator    = request.user
            filedrop.ip         = request.META.get('REMOTE_ADDR', "0.0.0.0")
            filedrop.save()
            form.save_m2m()
            return HttpResponseRedirect(reverse("filedrop_detail", args=[filedrop.pk]))    

It seems a little awkward but makes sense: you have to save the filedrop object so that the DB has a primary key that it can associate the User objects with. It just seems goofy and wet that I have to have three different "save" mechanisms. Hopefully this hint will save you some tiem.

Share this: