10

Following Kevin Dias instructions in this article, I try to generate one form for two related models. This seems to work for one-to-many relations, however I run into problems using many-to-many relations.

Here is some example code for a user-role management:

#models.py
from django.db import models

class Role(models.Model): # for each role there can be multiple users
    role_name=models.CharField(max_length=20)

class User(models.Model): # each user can have multiple roles
    name=models.CharField(max_length=20)
    role=models.ManyToManyField(Role, through='UserRole')

class UserRole(models.Model): # table to store which user has which roles
    role=models.ForeignKey(Role)
    user=models.ForeignKey(User)

# forms.py
from django.forms import ModelForm
from django.forms.models import inlineformset_factory

from rightmanagement.models import Role, User

class UserForm(ModelForm):
    class Meta:
        model = User

RoleFormSet = inlineformset_factory(User, Role) # this is probably the line that causes the problem

# views.py
from django.http import HttpResponseRedirect
from rightmanagement.models import User
from rightmanagement.forms import RoleFormSet, UserForm

# Create view
from django.views.generic import CreateView
class UserCreate(CreateView):
    model = User
    form_class = UserForm

    def get(self, request, *args, **kwargs):
        """
        Handles GET requests and instantiates blank versions of the form
        and its inline formsets.
        """
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        role_form = RoleFormSet()
        return self.render_to_response(
            self.get_context_data(form=form,
                                  role_form=role_form))

    def post(self, request, *args, **kwargs):
        """
        Handles POST requests, instantiating a form instance and its inline
        formsets with the passed POST variables and then checking them for
        validity.
        """
        self.object = None
        form_class = self.get_form_class()
        form = self.get_form(form_class)
        role_form = RoleFormSet(self.request.POST)
        if (form.is_valid() and role_form.is_valid()):
            return self.form_valid(form, role_form)
        else:
            return self.form_invalid(form, role_form)

    def form_valid(self, form, role_form):
        """
        Called if all forms are valid. Creates a Recipe instance along with
        associated Ingredients and Instructions and then redirects to a
        success page.
        """
        self.object = form.save()
        role_form.instance = self.object
        role_form.save()
        return HttpResponseRedirect(self.get_success_url())

    def form_invalid(self, form, role_form):
        """
        Called if a form is invalid. Re-renders the context data with the
        data-filled forms and errors.
        """
        return self.render_to_response(
            self.get_context_data(form=form,
                                  role_form=role_form))

These settings lead to the error message <class 'rightmanagement.models.Role'> has no ForeignKey to <class 'rightmanagement.models.User'>.

Doing some research I found this: Django inlineformset_factory and ManyToMany fields. It seems like inline formsets are only for ForeignKey but not for ManyToManyField. Also the docs can be interpreted like this.

However, I think in this particular case a foreign key instead of a many-to-many relation wouldn't make any sense. How would a pendant to Django's built-in inline formset look like for many-to-many relations? The aim would be to build a form that allows either the assignment of the user to roles that already exist or create new roles and assign them to the user if they do not exist yet.

J-a-n-u-s
  • 1,469
  • 17
  • 20
speendo
  • 13,045
  • 22
  • 71
  • 107

1 Answers1

11

As you're probably aware, you can't edit many-to-many relationships with inline formsets. You can, however, edit the through model. So for your inline formset, you just need to set the model to the through model, like so:

RoleFormSet = inlineformset_factory(UserRole, User.role.through)
Drewness
  • 5,004
  • 4
  • 32
  • 50
  • 1
    thank you! Doing it like this I don't get the error message anymore. However, can I add *new roles* in a form? After all, the model `Role` would neither be included in `forms.py` nor in `views.py`. – speendo Mar 07 '14 at 00:31
  • 2
    `User.role.though` should be `User.role.through` – Nathan Jones Aug 20 '18 at 23:33
  • 1
    It seems that this doesn't work anymore in the latest version of Django. Depending on how I mix them in the formset, I get a missing field error or missing foreign key errors – takje Aug 20 '21 at 13:46