-
-
Save badri/4a1be2423ce9353373e1b3f2cc67b80b to your computer and use it in GitHub Desktop.
{% extends "base.html" %} | |
{% block content %} | |
<form method="post">{% csrf_token %} | |
{{ forms.subscription }} | |
<input type="submit" value="Subscribe"> | |
</form> | |
<form method="post">{% csrf_token %} | |
{{ forms.contact }} | |
<input type="submit" value="Send"> | |
</form> | |
{% endblock content %} |
from django import forms | |
class MultipleForm(forms.Form): | |
action = forms.CharField(max_length=60, widget=forms.HiddenInput()) | |
class ContactForm(MultipleForm): | |
title = forms.CharField(max_length=150) | |
message = forms.CharField(max_length=200, widget=forms.TextInput) | |
class SubscriptionForm(MultipleForm): | |
email = forms.EmailField() |
from django.views.generic.base import ContextMixin, TemplateResponseMixin | |
from django.views.generic.edit import ProcessFormView | |
class MultiFormMixin(ContextMixin): | |
form_classes = {} | |
prefixes = {} | |
success_urls = {} | |
initial = {} | |
prefix = None | |
success_url = None | |
def get_form_classes(self): | |
return self.form_classes | |
def get_forms(self, form_classes): | |
return dict([(key, self._create_form(key, class_name)) \ | |
for key, class_name in form_classes.items()]) | |
def get_form_kwargs(self, form_name): | |
kwargs = {} | |
kwargs.update({'initial':self.get_initial(form_name)}) | |
kwargs.update({'prefix':self.get_prefix(form_name)}) | |
if self.request.method in ('POST', 'PUT'): | |
kwargs.update({ | |
'data': self.request.POST, | |
'files': self.request.FILES, | |
}) | |
return kwargs | |
def forms_valid(self, forms, form_name): | |
form_valid_method = '%s_form_valid' % form_name | |
if hasattr(self, form_valid_method): | |
return getattr(self, form_valid_method)(forms[form_name]) | |
else: | |
return HttpResponseRedirect(self.get_success_url(form_name)) | |
def forms_invalid(self, forms): | |
return self.render_to_response(self.get_context_data(forms=forms)) | |
def get_initial(self, form_name): | |
initial_method = 'get_%s_initial' % form_name | |
if hasattr(self, initial_method): | |
return getattr(self, initial_method)() | |
else: | |
return {'action': form_name} | |
def get_prefix(self, form_name): | |
return self.prefixes.get(form_name, self.prefix) | |
def get_success_url(self, form_name=None): | |
return self.success_urls.get(form_name, self.success_url) | |
def _create_form(self, form_name, form_class): | |
form_kwargs = self.get_form_kwargs(form_name) | |
form = form_class(**form_kwargs) | |
return form | |
class ProcessMultipleFormsView(ProcessFormView): | |
def get(self, request, *args, **kwargs): | |
form_classes = self.get_form_classes() | |
forms = self.get_forms(form_classes) | |
return self.render_to_response(self.get_context_data(forms=forms)) | |
def post(self, request, *args, **kwargs): | |
form_classes = self.get_form_classes() | |
form_name = request.POST.get('action') | |
return self._process_individual_form(form_name, form_classes) | |
def _process_individual_form(self, form_name, form_classes): | |
forms = self.get_forms(form_classes) | |
form = forms.get(form_name) | |
if not form: | |
return HttpResponseForbidden() | |
elif form.is_valid(): | |
return self.forms_valid(forms, form_name) | |
else: | |
return self.forms_invalid(forms) | |
class BaseMultipleFormsView(MultiFormMixin, ProcessMultipleFormsView): | |
""" | |
A base view for displaying several forms. | |
""" | |
class MultiFormsView(TemplateResponseMixin, BaseMultipleFormsView): | |
""" | |
A view for displaying several forms, and rendering a template response. | |
""" |
from django.http import HttpResponse, HttpResponseRedirect | |
from django.shortcuts import render | |
from django.urls import reverse, reverse_lazy | |
from .forms import ContactForm, SubscriptionForm | |
from .multiforms import MultiFormsView | |
def form_redir(request): | |
return render(request, 'pages/form_redirect.html') | |
def multiple_forms(request): | |
if request.method == 'POST': | |
contact_form = ContactForm(request.POST) | |
subscription_form = SubscriptionForm(request.POST) | |
if contact_form.is_valid() or subscription_form.is_valid(): | |
# Do the needful | |
return HttpResponseRedirect(reverse('form-redirect') ) | |
else: | |
contact_form = ContactForm() | |
subscription_form = SubscriptionForm() | |
return render(request, 'pages/multiple_forms.html', { | |
'contact_form': contact_form, | |
'subscription_form': subscription_form, | |
}) | |
class MultipleFormsDemoView(MultiFormsView): | |
template_name = "pages/cbv_multiple_forms.html" | |
form_classes = {'contact': ContactForm, | |
'subscription': SubscriptionForm, | |
} | |
success_urls = { | |
'contact': reverse_lazy('form-redirect'), | |
'subscription': reverse_lazy('form-redirect'), | |
} | |
def contact_form_valid(self, form): | |
title = form.cleaned_data.get('title') | |
form_name = form.cleaned_data.get('action') | |
print(title) | |
return HttpResponseRedirect(self.get_success_url(form_name)) | |
def subscription_form_valid(self, form): | |
email = form.cleaned_data.get('email') | |
form_name = form.cleaned_data.get('action') | |
print(email) | |
return HttpResponseRedirect(self.get_success_url(form_name)) |
Thank you. Very useful.
Two small additions. First the instance feature...:def get_form_kwargs(self, form_name): kwargs = {} kwargs.update({'instance': self.get_instance(form_name)}) # helps when updating records kwargs.update({'initial': self.get_initial(form_name)}) kwargs.update({'prefix': self.get_prefix(form_name)})
and the associated method
def get_instance(self, form_name): instance_method = 'get_%s_instance' % form_name if hasattr(self, instance_method): return getattr(self, instance_method)() else: return None
Then fix an issue with initial values not containing an action, when the get_initial was called, and causing 403 from _process_individual_form. The solution is to add action in the get_initial() on MultiFormMixin.
def get_initial(self, form_name): initial_method = 'get_%s_initial' % form_name if hasattr(self, initial_method): attrs = getattr(self, initial_method)() attrs['action'] = form_name return attrs else: return {'action': form_name}
Thanks again.
Hi,
I had the same issue that you mentioned in the latter part, that form_classes was being returned as an empty list. I replaced the get_initial function as you suggested, but it's still giving the same issue.
Did you do anything else to fix the problem?
@Trainwithvinny, I'm not sure if your comments are about my change to the gist or @topiaruss's suggested changes. I didn't need to modify get_initial, only get_form_kwargs
. That said, I put a value tag in each submit button on my template. e.g. <button name="action" value="create_foo">
if my form_classes contained 'create_foo': FooCreateForm
I'm getting the following error on the html page against each of the forms, can anyone help with this? I can move past the error and the webpage will generate, but it will only give me the 2 buttons not the input fields.
I'm getting up to speed with Django so might be a newb error.
Failed lookup for key [forms] in [{'True': True, 'False': False, 'None': None}, {'csrf_token': <SimpleLazyObject: 'Bbw1lraYBgwPlfyZWu9IwmzI1AbigK78jzXaGkBOhjJYPzU79USlnydVo3xaE7eR'>, 'request': <WSGIRequest: GET '/multiple_forms/'>, 'user': <SimpleLazyObject: <User: admin3>>, 'perms': <django.contrib.auth.context_processors.PermWrapper object at 0x0000017CB3840D48>, 'messages': <django.contrib.messages.storage.fallback.FallbackStorage object at 0x0000017CB3822908>, 'DEFAULT_MESSAGE_LEVELS': {'DEBUG': 10, 'INFO': 20, 'SUCCESS': 25, 'WARNING': 30, 'ERROR': 40}}, {}, {'contact_form': , 'subscription_form': }, {'block': <Block Node: content. Contents: [<TextNode: '\n
'>, <django.template.defaulttags.CsrfTokenNode object at 0x0000017CB3843288>, <TextNode: '\n '>, , <TextNode: '\n <input type="submit"'>, <django.template.defaulttags.CsrfTokenNode object at 0x0000017CB3843988>, <TextNode: '\n '>, , <TextNode: '\n <input type="submit"'>]>}]I'm just using the example at the moment;
####### forms.py - Multiple Forms Test########################
class MultipleForm(forms.Form):
action = forms.CharField(max_length=60, widget=forms.HiddenInput())
class ContactForm(MultipleForm):
title = forms.CharField(max_length=150)
message = forms.CharField(max_length=200, widget=forms.TextInput)
############################ MultiForm Views #########################
def form_redir(request):
return render(request, 'pages/form_redirect.html')
def multiple_forms(request):
if request.method == 'POST':
contact_form = ContactForm(request.POST)
subscription_form = SubscriptionForm(request.POST)
if contact_form.is_valid() or subscription_form.is_valid():
# Do the needful
return HttpResponseRedirect(reverse('form-redirect') )
else:
contact_form = ContactForm()
subscription_form = SubscriptionForm()
return render(request, 'multiple_forms.html', {
'contact_form': contact_form,
'subscription_form': subscription_form,
})
class MultipleFormsDemoView(MultiFormsView):
template_name = "multiple_forms.html"
form_classes = {'contact': ContactForm,
'subscription': SubscriptionForm,
}
success_urls = {
'contact': reverse_lazy('form-redirect'),
'subscription': reverse_lazy('form-redirect'),
}
def contact_form_valid(self, form):
title = form.cleaned_data.get('title')
form_name = form.cleaned_data.get('action')
print(title)
return HttpResponseRedirect(self.get_success_url(form_name))
def subscription_form_valid(self, form):
email = form.cleaned_data.get('email')
form_name = form.cleaned_data.get('action')
print(email)
return HttpResponseRedirect(self.get_success_url(form_name))
Thans for cool job, Lakshmi!
I mixed your idea up with generic.UpdateView.
# multiforms.py
class ModelMultiFormMixin(ModelFormMixin, MultiFormMixin):
def get_form_kwargs(self, form_name):
kwargs = {}
kwargs.update({'initial': self.get_initial(form_name)})
kwargs.update({'prefix': self.get_prefix(form_name)})
if self.request.method in ('POST', 'PUT'):
kwargs.update({
'data': self.request.POST,
'files': self.request.FILES,
})
if hasattr(self, 'object'):
kwargs.update({'instance': self.object})
return kwargs
def get_initial(self, form_name):
initial_method = 'get_%s_initial' % form_name
if hasattr(self, initial_method):
return getattr(self, initial_method)()
else:
return {'action': form_name}
def get_prefix(self, form_name):
return self.prefixes.get(form_name, self.prefix)
def get_context_data(self, **kwargs):
kwargs.setdefault('view', self)
if self.extra_context is not None:
kwargs.update(self.extra_context)
return kwargs
def get_success_url(self, form_name=None):
return self.success_urls.get(form_name, self.success_url)
def forms_valid(self, forms, form_name):
"""If the forms are valid, save the associated model."""
obj = forms.get(form_name)
obj.save()
return HttpResponseRedirect(self.get_success_url(form_name))
class BaseMultipleFormsUpdateView(ModelMultiFormMixin, ProcessMultipleFormsView):
"""
Base view for updating an existing object.
Using this base class requires subclassing to provide a response mixin.
"""
def get(self, request, *args, **kwargs):
self.object = self.get_object()
return super().get(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
return super().post(request, *args, **kwargs)
class MultiFormsUpdateView(SingleObjectTemplateResponseMixin, BaseMultipleFormsUpdateView):
pass
also we need to add:
# forms.py
class ModelMultipleForm(forms.ModelForm):
action = forms.CharField(max_length=60, widget=forms.HiddenInput())
then, now we can code like:
# view.py
class MultipleFormsDemoView(MultiFormsUpdateView): # superclass replaced
template_name = "pages/cbv_multiple_forms.html"
form_classes = {'contact': ContactForm,
'subscription': SubscriptionForm,
}
success_urls = {
'contact': reverse_lazy('form-redirect'),
'subscription': reverse_lazy('form-redirect'),
}
Thank you. Very useful.
Two small additions. First the instance feature...:def get_form_kwargs(self, form_name): kwargs = {} kwargs.update({'instance': self.get_instance(form_name)}) # helps when updating records kwargs.update({'initial': self.get_initial(form_name)}) kwargs.update({'prefix': self.get_prefix(form_name)})
and the associated method
def get_instance(self, form_name): instance_method = 'get_%s_instance' % form_name if hasattr(self, instance_method): return getattr(self, instance_method)() else: return None
Then fix an issue with initial values not containing an action, when the get_initial was called, and causing 403 from _process_individual_form. The solution is to add action in the get_initial() on MultiFormMixin.
def get_initial(self, form_name): initial_method = 'get_%s_initial' % form_name if hasattr(self, initial_method): attrs = getattr(self, initial_method)() attrs['action'] = form_name return attrs else: return {'action': form_name}
Thanks again.
Hi,
I had the same issue that you mentioned in the latter part, that form_classes was being returned as an empty list. I replaced the get_initial function as you suggested, but it's still giving the same issue.
Did you do anything else to fix the problem?
Thank you this has been very insightful 😃
One little Fix though to the form_is_valid
Change forms_valid
method to
def forms_valid(self, forms, form_name):
form_valid_method = '%s_valid' % form_name
if hasattr(self, form_valid_method):
return getattr(self, form_valid_method)(forms[form_name])
else:
form = forms.get(form_name)
form.save()
return HttpResponseRedirect(self.get_success_url(form_name))
Explanation
What has changed is form_valid_method = '%s_form_valid' % form_name
to form_valid_method = '%s_valid' % form_name
.
Assuming you have a form called user_form
the former case form_valid_method
would be form_valid_method = 'user_form_form_valid'
with that you would have ended in the else block.
Thank you. Very useful. Two small additions. First the instance feature...:
def get_form_kwargs(self, form_name): kwargs = {} kwargs.update({'instance': self.get_instance(form_name)}) # helps when updating records kwargs.update({'initial': self.get_initial(form_name)}) kwargs.update({'prefix': self.get_prefix(form_name)})
and the associated method
def get_instance(self, form_name): instance_method = 'get_%s_instance' % form_name if hasattr(self, instance_method): return getattr(self, instance_method)() else: return None
Then fix an issue with initial values not containing an action, when the get_initial was called, and causing 403 from _process_individual_form. The solution is to add action in the get_initial() on MultiFormMixin.
def get_initial(self, form_name): initial_method = 'get_%s_initial' % form_name if hasattr(self, initial_method): attrs = getattr(self, initial_method)() attrs['action'] = form_name return attrs else: return {'action': form_name}
Thanks again.
Hi!
I had the same issue with 403. Tried to use your updates, but getting an error:
TypeError: init() got an unexpected keyword argument 'instance'
What could be the problem?
@JulieGoldberg Hi, can you help me with a question?
i'm trying to save a form called vendor_form, but he also uses fields from user_form, so my valid method is "if user_form.is_valid() and vendor_form.is_valid()'
since i depend the user to create the vendor user, i'm getting null value collumn error because for some reason the user_form is not getting saved cause the action is 'vendor: vendor_form'
This is very helpful.
I do have one change that I'd strongly suggest.
When I got a validation error submitting one form, it would try to populate all the other forms from the submitted form, clearing out their values and leading to errors on forms that weren't submitted. It's better to populate all but the submitted form with their initial data when you come back with an invalid submission.
I solved this by adding in a check for
form_name in self.request.POST.get('action')
in yourget_form_kwargs
function.This won't be an issue if you rely on client-side validation for all your forms, but sometimes you need back-end validation.