114

Is it possible to have multiple models included in a single ModelForm in django? I am trying to create a profile edit form. So I need to include some fields from the User model and the UserProfile model. Currently I am using 2 forms like this

class UserEditForm(ModelForm):

    class Meta:
        model = User
        fields = ("first_name", "last_name")

class UserProfileForm(ModelForm):

    class Meta:
        model = UserProfile
        fields = ("middle_name", "home_phone", "work_phone", "cell_phone")

Is there a way to consolidate these into one form or do I just need to create a form and handle the db loading and saving myself?

Serjik
  • 10,543
  • 8
  • 61
  • 70
Jason Webb
  • 7,938
  • 9
  • 40
  • 49
  • Possible duplicate of [Django: multiple models in one template using forms](http://stackoverflow.com/questions/569468/django-multiple-models-in-one-template-using-forms) – Ciro Santilli OurBigBook.com May 12 '16 at 22:31
  • This is a special case (using `ModelForm`) of [python - django submit two different forms with one submit button - Stack Overflow](https://stackoverflow.com/questions/18489393/django-submit-two-different-forms-with-one-submit-button) – user202729 Aug 13 '21 at 12:20

6 Answers6

117

You can just show both forms in the template inside of one <form> html element. Then just process the forms separately in the view. You'll still be able to use form.save() and not have to process db loading and saving yourself.

In this case you shouldn't need it, but if you're going to be using forms with the same field names, look into the prefix kwarg for django forms. (I answered a question about it here).

Community
  • 1
  • 1
Zach
  • 18,594
  • 18
  • 59
  • 68
  • This is a good piece of advice, but there are cases this is not applicable, eg. custom model form for a formset. – Wtower May 17 '15 at 21:16
  • 12
    What would be a straightforward way to make a class based view capable of showing more than one form and a template that then combines them into the same `
    ` element?
    – jozxyqk May 21 '15 at 11:47
  • 4
    But how? Usually a `FormView` only has a single `form_class` assigned to it. – erikbstack Oct 07 '16 at 17:42
  • @erikbwork You should not use a FormView for this case. Just Subclass `TemplateView` and implement the same logic as the FormView, but with multiple forms. – moppag Nov 16 '18 at 13:15
11

You can try to use this pieces of code:

class CombinedFormBase(forms.Form):
    form_classes = []

    def __init__(self, *args, **kwargs):
        super(CombinedFormBase, self).__init__(*args, **kwargs)
        for f in self.form_classes:
            name = f.__name__.lower()
            setattr(self, name, f(*args, **kwargs))
            form = getattr(self, name)
            self.fields.update(form.fields)
            self.initial.update(form.initial)

    def is_valid(self):
        isValid = True
        for f in self.form_classes:
            name = f.__name__.lower()
            form = getattr(self, name)
            if not form.is_valid():
                isValid = False
        # is_valid will trigger clean method
        # so it should be called after all other forms is_valid are called
        # otherwise clean_data will be empty
        if not super(CombinedFormBase, self).is_valid() :
            isValid = False
        for f in self.form_classes:
            name = f.__name__.lower()
            form = getattr(self, name)
            self.errors.update(form.errors)
        return isValid

    def clean(self):
        cleaned_data = super(CombinedFormBase, self).clean()
        for f in self.form_classes:
            name = f.__name__.lower()
            form = getattr(self, name)
            cleaned_data.update(form.cleaned_data)
        return cleaned_data

Example Usage:

class ConsumerRegistrationForm(CombinedFormBase):
    form_classes = [RegistrationForm, ConsumerProfileForm]

class RegisterView(FormView):
    template_name = "register.html"
    form_class = ConsumerRegistrationForm

    def form_valid(self, form):
        # some actions...
        return redirect(self.get_success_url())
Miao ZhiCheng
  • 617
  • 7
  • 9
5

I used django betterforms's MultiForm and MultiModelForm in my project. The code can be improved, though. For example, it's dependent on django.six, which isn't supported by 3.+, but all of these can easily be fixed

This question has appeared several times in StackOverflow, so I think it's time to find a standardized way of coping with this.

J Eti
  • 192
  • 4
  • 13
  • 1
    that doesn't work for me. giving error related to python2. maybe not useful anymore – K14 Jan 11 '22 at 19:38
4

erikbwork and me both had the problem that one can only include one model into a generic Class Based View. I found a similar way of approaching it like Miao, but more modular.

I wrote a Mixin so you can use all generic Class Based Views. Define model, fields and now also child_model and child_field - and then you can wrap fields of both models in a tag like Zach describes.

class ChildModelFormMixin: 
    ''' extends ModelFormMixin with the ability to include ChildModelForm '''
    child_model = ""
    child_fields = ()
    child_form_class = None

    def get_child_model(self):
        return self.child_model

    def get_child_fields(self):
        return self.child_fields

    def get_child_form(self):
        if not self.child_form_class:
            self.child_form_class = model_forms.modelform_factory(self.get_child_model(), fields=self.get_child_fields())
        return self.child_form_class(**self.get_form_kwargs())

    def get_context_data(self, **kwargs):
        if 'child_form' not in kwargs:
            kwargs['child_form'] = self.get_child_form()
        return super().get_context_data(**kwargs)

    def post(self, request, *args, **kwargs):
        form = self.get_form()
        child_form = self.get_child_form()

        # check if both forms are valid
        form_valid = form.is_valid()
        child_form_valid = child_form.is_valid()

        if form_valid and child_form_valid:
            return self.form_valid(form, child_form)
        else:
            return self.form_invalid(form)

    def form_valid(self, form, child_form):
        self.object = form.save()
        save_child_form = child_form.save(commit=False)
        save_child_form.course_key = self.object
        save_child_form.save()

        return HttpResponseRedirect(self.get_success_url())

Example Usage:

class ConsumerRegistrationUpdateView(UpdateView):
    model = Registration
    fields = ('firstname', 'lastname',)
    child_model = ConsumerProfile
    child_fields = ('payment_token', 'cart',)

Or with ModelFormClass:

class ConsumerRegistrationUpdateView(UpdateView):
    model = Registration
    fields = ('firstname', 'lastname',)
    child_model = ConsumerProfile
    child_form_class = ConsumerProfileForm

Done. Hope that helps someone.

LGG
  • 101
  • 1
  • 5
  • In this `save_child_form.course_key = self.object`, what is `.course_key`? – Adam Starrh Jul 14 '17 at 18:15
  • I think course_key is the related model, in my case that is "user" as in UserProfile.user which is a backref, maybe that field name should be customisable if it was to be a re-usable mixin. But I am still having another issue where the child form is not actually populated with initial data, all the fields from User are pre-populated but not for UserProfile. I might have to fix that first. – robvdl Jan 09 '19 at 03:47
  • 1
    The issue why the child form doesn't get populated is because in the get_child_form method, it calls `return self.child_form_class(**self.get_form_kwargs())` but it gets the wrong model instance in `kwargs['instance']`, e.g. instance is the main model and not the child model. To fix you need to save kwargs into a variable first `kwargs = self.get_form_kwargs()` then update `kwargs['initial']` with the correct model instance before calling `return self.child_form_class(**kwargs)`. In my case that was `kwargs['instance'] = kwargs['instance'].profile` if this makes sense. – robvdl Jan 09 '19 at 04:13
  • Unfortunately on save it will still crash in two places, one where self.object is not there yet in form_valid, so it throws an AttributeError, and another place instance is not there. I'm not sure if this solution was fully tested before being posted so it might be better to go with the other answer using CombinedFormBase. – robvdl Jan 09 '19 at 04:37
  • 1
    What is `model_forms`? – Granny Aching Sep 12 '19 at 18:24
2

You probably should take a look at Inline formsets. Inline formsets are used when your models are related by a foreign key.

John Percival Hackworth
  • 11,395
  • 2
  • 29
  • 38
  • 1
    Inline formsets are used when you need to work with a one to many relationship. Such as a company where you add employees. I am trying combine 2 tables into one single form. It is a one to one relationship. – Jason Webb May 05 '10 at 14:02
  • The use of an Inline formset would work, but would likely by less than ideal. You could also create a Model that handles the relation for you, and then use a single form. Just having a single page with 2 forms as suggested in http://stackoverflow.com/questions/2770810/muliple-models-in-a-single-django-modelform/2774732#2774732 would work. – John Percival Hackworth May 05 '10 at 16:00
2

You can check my answer here for a similar problem.

It talks about how to combine registration and user profile into one form, but it can be generalized to any ModelForm combination.

Community
  • 1
  • 1
Mitar
  • 6,756
  • 5
  • 54
  • 86