Reputation: 8020
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?
Upvotes: 115
Views: 95940
Reputation: 302
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.
Upvotes: 5
Reputation: 111
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.
Upvotes: 4
Reputation: 657
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())
Upvotes: 13
Reputation: 6980
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.
Upvotes: 2
Reputation: 19082
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).
Upvotes: 118
Reputation: 11531
You probably should take a look at Inline formsets. Inline formsets are used when your models are related by a foreign key.
Upvotes: 2