Let's take a look at some of the common patterns when working with forms.
Problem: Adding form fields dynamically or changing form fields from what has been declared.
Solution: Add or change fields during initialization of the form.
Forms are usually defined in a declarative style with form fields listed as class fields. However, sometimes we do not know the number or type of these fields in advance. This calls for the form to be dynamically generated. This pattern is sometimes called Dynamic Forms or Runtime form generation.
Imagine a flight passenger check-in system, which allows for the upgrade of economy class tickets to first class. If there are any first-class seats left, there needs to be an additional option to the user if they would like to fly first class. However, this optional field cannot be declared since it will not be shown to all users. Such dynamic forms can be handled by this pattern.
Every form instance has an attribute called fields
, which is a dictionary that holds all the form fields. This can be modified at runtime. Adding or changing the fields can be done during form initialization itself.
For example, if we need to add a checkbox to a user details form only if a keyword argument named "upgrade
" is true at form initialization, then we can implement it as follows:
class PersonDetailsForm(forms.Form): name = forms.CharField(max_length=100) age = forms.IntegerField() def __init__(self, *args, **kwargs): upgrade = kwargs.pop("upgrade", False) super().__init__(*args, **kwargs) # Show first class option? if upgrade: self.fields["first_class"] = forms.BooleanField( label="Fly First Class?")
Now, we just need to pass the, PersonDetailsForm(upgrade=True)
keyword argument to make an additional Boolean input field ( a checkbox) appear.
If we use a FormView
class for this example, then we need to pass the keyword argument by overriding the get_form_kwargs
method of the view class, as shown in the following code:
class PersonDetailsEdit(generic.FormView): ... def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs["upgrade"] = True return kwargs
This pattern can be used to change any attribute of a field at runtime, such as its widget or help text. It works for model forms as well.
In many cases, a seeming need for dynamic forms can be solved using Django formsets. They are used when a form needs to be repeated in a page. A typical use case for formsets is while designing a data grid-like view to add elements row by row. This way, you do not need to create a dynamic form with an arbitrary number of rows. You just need to create a form for the row and create multiple rows using a formset_factory
function.
Problem: Forms need to be customized based on the logged-in user.
Solution: Pass the logged-in user as a keyword argument to the form's initializer.
A form can be presented in different ways based on the user. Certain users might not need to fill all the fields, while certain others might need to add additional information. In some cases, you might need to run some checks on the user's eligibility, such as verifying whether they are members of a group, to determine how the form should be constructed.
As you must have noticed, you can solve this using the solution given in the Dynamic form generation pattern. You just need to pass request.user
as a keyword argument to the form. However, we can also use mixins from the django-braces
package for a shorter and more reusable solution.
As in the previous example, we need to show an additional checkbox to the user. However, this will be shown only if the user is a member of the VIP group. Let's take a look at how PersonDetailsForm
gets simplified with the form mixin UserKwargModelFormMixin
from django-braces
:
from braces.forms import UserKwargModelFormMixin class PersonDetailsForm(UserKwargModelFormMixin, forms.Form): ... def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Are you a member of the VIP group? if self.user.groups.filter(name="VIP").exists(): self.fields["first_class"] = forms.BooleanField( label="Fly First Class?")
Notice how self.user
was automatically made available by the mixin by popping the user
keyword argument.
Corresponding to the form mixin, there is a view mixin called
UserFormKwargsMixin
, which needs to be added to the view, along with LoginRequiredMixin
to ensure that only logged-in users can access this view:
class VIPCheckFormView(LoginRequiredMixin, UserFormKwargsMixin, generic.FormView): form_class = PersonDetailsForm ...
Now, the user
argument will be passed to the PersonDetailsForm
form automatically.
Do check out other form mixins in django-braces
such as FormValidMessageMixin,
which are readymade solutions to common form-usage patterns.
Problem: Handling multiple form actions in a single view or page.
Solution: Forms can use separate views to handle form submissions or a single view can identify the form based on the Submit
button's name.
Django makes it relatively straightforward to combine multiple forms with the same action, for example, a single submit button. However, most web pages need to show several actions on the same page. For example, you might want the user to subscribe or unsubscribe from a newsletter in two distinct forms on the same page.
However, Django's FormView
is designed to handle only one form per view scenario. Many other generic class-based views also share this assumption.
There are two ways to handle multiple forms: a separate view and single view. Let's take a look at the first approach.
This is a fairly straightforward approach with each form specifying different views as their actions. For example, take the subscribe and unsubscribe forms. There can be two separate view classes to handle just the POST
method from their respective forms.
Perhaps you find the splitting views to handle forms to be unnecessary, or you find handling logically related forms in a common view to be more elegant. Either way, we can work around the limitations of generic class-based views to handle more than one form.
While using the same view class for multiple forms, the challenge is to identify which form issued the POST
action. Here, we take advantage of the fact that the name and value of the Submit
button is also submitted. If the Submit
button is named uniquely across forms, then the form can be identified while processing.
Here, we define a subscribe form using crispy forms so that we can name the submit
button as well:
class SubscribeForm(forms.Form): email = forms.EmailField() def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper(self) self.helper.layout.append(Submit('subscribe_butn', 'Subscribe'))
The UnSubscribeForm
unsubscribe form class is defined in exactly the same way (and hence is, omitted), except that its Submit
button is named unsubscribe_butn
.
Since FormView
is designed for a single form, we will use a simpler class-based view say, TemplateView
, as the base for our view. Let's take a look at the view definition and the get
method:
from .forms import SubscribeForm, UnSubscribeForm class NewsletterView(generic.TemplateView): subcribe_form_class = SubscribeForm unsubcribe_form_class = UnSubscribeForm template_name = "newsletter.html" def get(self, request, *args, **kwargs): kwargs.setdefault("subscribe_form", self.subcribe_form_class()) kwargs.setdefault("unsubscribe_form", self.unsubcribe_form_class()) return super().get(request, *args, **kwargs)
The keyword arguments to a TemplateView
class get conveniently inserted into the template context. We create instances of either form only if they don't already exist, with the help of the setdefault
dictionary method. We will soon see why.
Next, we will take a look at the POST
method, which handles submissions from either form:
def post(self, request, *args, **kwargs): form_args = { 'data': self.request.POST, 'files': self.request.FILES, } if "subscribe_butn" in request.POST: form = self.subcribe_form_class(**form_args) if not form.is_valid(): return self.get(request, subscribe_form=form) return redirect("success_form1") elif "unsubscribe_butn" in request.POST: form = self.unsubcribe_form_class(**form_args) if not form.is_valid(): return self.get(request, unsubscribe_form=form) return redirect("success_form2") return super().get(request)
First, the form keyword arguments, such as data
and files
, are populated in a form_args
dictionary. Next, the presence of the first form's Submit
button is checked in request.POST
. If the button's name is found, then the first form is instantiated.
If the form fails validation, then the response created by the GET
method with the first form's instance is returned. In the same way, we look for the second forms submit button to check whether the second form was submitted.
Instances of the same form in the same view can be implemented in the same way with form prefixes. You can instantiate a form with a prefix argument such as SubscribeForm(prefix="offers")
. Such an instance will prefix all its form fields with the given argument, effectively working like a form namespace.
Problem: Writing boilerplate for CRUD interfaces to a model is repetitive.
Solution: Use generic class-based editing views.
In most web applications, about 80 percent of the time is spent writing, creating, reading, updating, and deleting (CRUD) interfaces to a database. For instance, Twitter essentially involves creating and reading each other's tweets. Here, a tweet would be the database object that is being manipulated and stored.
Writing such interfaces from scratch can get tedious. This pattern can be easily managed if CRUD interfaces can be automatically created from the model class itself.
Django simplifies the process of creating CRUD views with a set of four generic class-based views. They can be mapped to their corresponding operations as follows:
CreateView
: This view displays a blank form to create a new objectDetailView
: This view shows an object's details by reading from the databaseUpdateView
: This view allows to update an object's details through a pre-populated formDeleteView
: This view displays a confirmation page and, on approval, deletes the objectLet's take a look at a simple example. We have a model that contains important dates, which are of interest to everyone using our site. We need to build simple CRUD interfaces so that anyone can view and modify these dates. Let's take a look at the ImportantDate
model itself:
# models.py class ImportantDate(models.Model): date = models.DateField() desc = models.CharField(max_length=100) def get_absolute_url(self): return reverse('impdate_detail', args=[str(self.pk)])
The get_absolute_url()
method is used by the CreateView
and UpdateView
classes to redirect after a successful object creation or update. It has been routed to the object's DetailView
.
The CRUD views themselves are simple enough to be self-explanatory, as shown in the following code:
# views.py from django.core.urlresolvers import reverse_lazyfrom . import forms class ImpDateDetail(generic.DetailView): model = models.ImportantDate class ImpDateCreate(generic.CreateView): model = models.ImportantDate form_class = forms.ImportantDateForm class ImpDateUpdate(generic.UpdateView): model = models.ImportantDate form_class = forms.ImportantDateForm class ImpDateDelete(generic.DeleteView): model = models.ImportantDate success_url = reverse_lazy("impdate_list")
In these generic views, the model class is the only mandatory member to be mentioned. However, in the case of DeleteView
, the success_url
function needs to be mentioned as well. This is because after deletion get_absolute_url
cannot be used anymore to find out where to redirect users.
Defining the form_class
attribute is not mandatory. If it is omitted, a ModelForm
method corresponding to the specified model will be created. However, we would like to create our own model form to take advantage of crispy forms, as shown in the following code:
# forms.py from django import forms from . import models from crispy_forms.helper import FormHelper from crispy_forms.layout import Submit class ImportantDateForm(forms.ModelForm): class Meta: model = models.ImportantDate fields = ["date", "desc"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.helper = FormHelper(self) self.helper.layout.append(Submit('save', 'Save'))
Thanks to crispy forms, we need very little HTML markup in our templates to build these CRUD forms.
The template paths, by default, are based on the view class and the model names. For brevity, we omitted the template source here. Note that we can use the same form for
CreateView
and UpdateView
.
Finally, we take a look at urls.py
, where everything is wired up together:
url(r'^impdates/create/$', pviews.ImpDateCreate.as_view(), name="impdate_create"), url(r'^impdates/(?P<pk>d+)/$', pviews.ImpDateDetail.as_view(), name="impdate_detail"), url(r'^impdates/(?P<pk>d+)/update/$', pviews.ImpDateUpdate.as_view(), name="impdate_update"), url(r'^impdates/(?P<pk>d+)/delete/$', pviews.ImpDateDelete.as_view(), name="impdate_delete"),
Django generic views are a great way to get started with creating CRUD views for your models. With a few lines of code, you get well-tested model forms and views created for you, rather than doing the boring task yourself.