Customizing NewForms
In my last post I covered a very basic introduction to getting NewForms working in your application. The purpose of that post was just to make sure we were all on the same page. For a much more detailed explanation, I highly recommend James Bennett’s post on NewForms, part 1.
If you’ve followed along then at this point you should have a functioning add/edit form that posts back your information and saves it in the database. This works great as long as you want every field in your model to appear on the form. Generally that’s not the case. A lot of times there are fields, like user
, that you want to set in the view before saving the form data. You don’t want the user to be able to specify the user, you want to get this information from the currently logged in user information.
To customize your NewForms output there are several different approaches. I’m going to touch on a couple of them, but keep in mind there are many ways to achieve the same results. We’re going to look at the following five ways to modify the fields that appear in the NewForms output:
- Make the field not editable
- Modify it Inline
- Using the Fields Parameter
- Modifying with a Callback
- Using a NewForm Class
Make the Field not Editable
The quickest and easiest solution to remove a field from your NewForms output is to just set the field option editable=False
in your model class. This will cause the form field to not be displayed in the output. For instance:
class Entry(models.Model):
pub_date = models.DateTimeField()
title = models.CharField(maxlength=100)
description = models.TextField()
user = models.ForeignKey(User)
last_updated = models.DateTimeField(editable=False)
In the Entry model above we have set the last_updated
field to be not editable, by setting the editable
field option to False
. Although this will cause the NewForm output to not contain the last_updated
field, usually this is not an acceptable solution because it has the added side effect of also removing the field from the Administration pages.
A case where this might make sense is one similar to the above where you have a field like last_updated
, which holds the date/time the record was last updated. In that case, you just need to override the model’s save()
method and update the field accordingly.
Modifying it Inline
Back to our original hypothetical. Let us assume that we want to store the logged in user information when the user signs our guessbook, but obviously we don’t want to allow the user to type in their User ID. So instead, let’s modify the form so that it doesn’t display the user field, but instead have it get auto-populated in the view.
There are three changes we need to make to accomplish this:
1. Modify the base_fields dict to hide the user field
2. If required, modify the user field to not be required
3. Modify the save routine to set the user information before saving the record.
The following modified view accomplishes all of the above:
from django.shortcuts import render_to_response, get_object_or_404
from django.http import HttpResponseRedirect
from django import newforms as forms
from django.newforms import widgets
from guestbook.models import Entry
def add_edit_entry(request, id=None):
if id is None:
EntryForm = forms.form_for_model(Entry)
else:
entry = get_object_or_404(Entry, pk=id)
EntryForm = forms.form_for_instance(entry)
EntryForm.base_fields['user'].widget = widgets.HiddenInput()
EntryForm.base_fields['user'].required = False
if request.method == 'POST':
form = EntryForm(request.POST)
if form.is_valid():
entry = form.save(commit=False)
entry.user = request.user
entry.save()
return HttpResponseRedirect("/guestbook")
else:
form = EntryForm()
return render_to_response('guestbook/add_edit_entry.html', {'form': form})
Let’s decipher what’s going on in the above code. We start off by modifying the base_fields
dict to hide the user field. To accomplish this we need to set the form field’s widget to the HiddenInput
widget. This causes the form to be rendered in the NewForm output but it’s type is hidden, so it’s invisible to the end user. We’ve also modified its required attribute to False
. Note that we need to make the field not required otherwise the form.is_valid()
method will return False
, not allowing us to set the user information ourself.
The next change we needed to make is to modify the form.save()
method by setting the parameter commit
to False
. This causes the form.save()
method to not save the form data directly to the database, but instead return a new model instance with the form data. By doing this we now have the opportunity to modify the model before saving it.
At this point we just need to set the entry.user
to the current request user information and then finally call the entry.save()
method which will save our form information to the database.
This method of modifying the form information is one approach to the problem of customizing your form. It can be used to further modify other fields, their properties, or set customized widgets, etc…
The problem with this approach is three-fold. First it quickly becomes unwieldy. Secondly it’s not very reusable. We would need to do the same thing in another view if we wanted similar behavior. Finally and most importantly you make some compromises in security by doing so.
For instance, note that we have set the user widget to a HiddenInput()
widget. This still renders the user field to the web page, but renders it as a hidden input field. Any fields that appear on a form are available to be compromised by the client. You should never trust information that comes from a user form, because it can easily be manipulated. If the user
field is not something we want on the form, then the best approach is to not have it appear at all, hidden or not. The next approach will do that for us.
Using the Fields Parameter
Both the form_for_model
and form_for_instance
methods accept a fields parameter, which takes a list of field names to display on the form. By specifying just the fields we want in that list the form will be rendered accordingly. This has the added benefit of not displaying the fields we don’t want as hidden field elements, leading to better security. For instance:
from django.shortcuts import render_to_response, get_object_or_404
from django.http import HttpResponseRedirect
from django import newforms as forms
from guestbook.models import Entry
def add_edit_entry(request, id=None):
fields = ('pub_date', 'title', 'description')
if id is None:
EntryForm = forms.form_for_model(Entry, fields=fields)
else:
entry = get_object_or_404(Entry, pk=id)
EntryForm = forms.form_for_instance(entry, fields=fields)
if request.method == 'POST':
form = EntryForm(request.POST)
if form.is_valid():
entry = form.save(commit=False)
entry.user = request.user
entry.save()
return HttpResponseRedirect("/guestbook")
else:
form = EntryForm()
return render_to_response('guestbook/add_edit_entry.html', {'form': form})
Notice that all we added was a fields list containing the fields we want, and passed that into the form_for_model
and form_for_instance
methods. The result is much cleaner now and the excluded fields will not appear in the NewForm output.
There’s been talk about adding an exclude
parameter (or something similar), since the usual case is that you want most of the form but just want to exclude one or two fields. It would be a nice addition, but at the same time it’s important that form_for_model
and form_for_instance
don’t get unwieldy, since they are just “quick and easy” shortcuts and not meant to be the be all end all for NewForms work.
Another approach to this is to use the formfield callback.
Modifying with a Callback
A formfield callback is a function that we define that gets called each time a new form field is being created. The formfield callback can also be used to do other things to the form field at the time of creation, such as overriding the default form field type.
from django.shortcuts import render_to_response, get_object_or_404
from django.http import HttpResponseRedirect
from django import newforms as forms
from django.newforms import widgets
from guestbook.models import Entry
def my_callback(field, **kwargs):
if field.name == 'user':
return None
else:
return field.formfield(**kwargs)
def add_edit_entry(request, id=None):
if id is None:
EntryForm = forms.form_for_model(Entry, formfield_callback=my_callback)
else:
entry = get_object_or_404(Entry, pk=id)
EntryForm = forms.form_for_instance(entry, formfield_callback=my_callback)
if request.method == 'POST':
form = EntryForm(request.POST)
if form.is_valid():
entry = form.save(commit=False)
entry.user = request.user
entry.save()
return HttpResponseRedirect("/guestbook")
else:
form = EntryForm()
return render_to_response('guestbook/add_edit_entry.html', {'form': form})
Note that we simple created a formfield callback function and assigned it to the formfield_callback
parameter of the form_for_model
and form_for_instance
methods.
To exclude the field from the form definition all we have to do is return None
for the specified field. This will cause that field to not be part of the form’s field definition. Notice that we need to handle the case where the field is not the field(s) we’re trying to exclude. This is because all fields get passed into the formfield callback. You can read more about this in the Django Documentation.
All of the above might seem like a lot of work and leaves you with an implementation that is not very reusable or DRY. Well you’re right, and the quicker you learn that the better. That’s why there are form classes.
Using a NewForm Class
Customizing your form by creating a form class is the “endorsed” way to customize the look and feel of your forms—for good reason. It gives you the most flexibility and in the long run it is the cleanest way to approach the task. That said, there seems to be incredible resistance by some in creating a form class. Creating the form_for_model
and form_for_instance
convenience methods make using NewForms immediately accessible but on the other hand it has also put blinders on some programmers causing them to be resistant to more beneficial approaches.
To create a custom form class we need to simply inherit from the forms
class and define the form in a similar manner as you define your models. I’m not going to go into the detail of creating a form class because it’s covered well in the offical documentation on newforms. Again, I also highly recommend James Bennett’s post on NewForms, part 1.
So where does this information go? Your custom form class can go wherever you want since it’s just python code. Some people prefer to put it in their view.py
file directly, others prefer to put it in its own forms.py
file. I prefer the latter, but it really doesn’t matter where you put it. (Believe it or not this is an often asked question.) For example here is one of my applications’ directory structure:
Here’s a simple form class for our Guestbook add / edit entry form:
from django import newforms as forms
from models import Entry
from django.newforms.extras import widgets
class EntryForm(forms.Form):
pub_date = forms.DateField(label=u'Publish date', widget=widgets.SelectDateWidget())
title = forms.CharField(max_length=100)
description = forms.CharField()
def save(self, id=None, commit=True):
if id is None:
instance = Entry()
else:
instance = Entry.objects.get(pk=id)
return forms.models.save_instance(self, instance, commit=commit)
To exclude the fields we don’t want we just need to exclude the field from the NewForms class definition. Once we have our NewForms class defined we can use it in our view as follows:
from django.shortcuts import render_to_response, get_object_or_404
from django.http import HttpResponseRedirect
from django import newforms as forms
from guestbook.models import Entry
from guestbook.forms import EntryForm
def add_edit_entry(request, id=None):
if request.method == 'POST':
form = EntryForm(request.POST)
if form.is_valid():
entry = form.save(id=id, commit=False)
entry.user = request.user
entry.save()
return HttpResponseRedirect("/guestbook")
else:
if id is None:
form = EntryForm()
else:
entry = get_object_or_404(Entry, pk=id)
form = EntryForm(entry.__dict__)
return render_to_response('guestbook/add_edit_entry.html', {'form': form})
As I stated above, using a form class gives you the most flexibility and provide the best option for reuse throughout your code.
We covered a lot of different approaches and I’m sure there many more creative solutions that people have come up with. Hopefully I’ve given you a couple more options for handling the customization of your NewForm output. The important thing is to be aware of the benefits and trade offs of each approach, and choose wisely.