Using the Django newforms Library

Juan Pablo Claude's Headshot
Juan Pablo Claude

The original Django tools for creating HTML forms and validating user supplied data (forms, manipulators, and validators) are currently being replaced by the newforms library, which is expected to be completed for version 1.0. The newforms library will be a nice change to Django, as it is much more elegant and easier to use than the oldforms library. Unfortunately, the inclusion of the newforms library will be backwards incompatible, so the development team is going to include both libraries in Django 1.0 to ease the transition, and then completely drop oldforms from the framework in later versions.

Thus, current Django developers are encouraged to embrace the newforms library as soon as possible, and new developers are discouraged from spending time learning the oldforms API altogether. This all sounds great, except that the newforms documentation is far from complete at this time. This article's goal is to give you enough information so that you can get started using the library now.

If you want to learn all about Django, I'll be teaching the Django Bootcamp at Big Nerd Ranch, April 2 - 6.

The transition path

If you install the latest development version of Django, it will contain both the newforms and oldforms libraries. There is also a forms module that indirectly imports the oldforms library, so that your legacy code will still work untouched. If you are writing an application using the old forms library you can import it as always:

from django import forms

If you will be using the newforms library, you are encouraged to import it in the following way:

from django import newforms as forms

so that when the newforms library is renamed to "forms" in the future, you will not have to change your code.

The model

For the examples we will be discussing, we will use the following model class:

from django.db import models

class Item(models.Model):
        STATUS_CHOICES = (
                ('stk', 'In stock'),
                ('bac', 'Back ordered'),
                ('dis', 'Discontinued'),
                ('nav', 'Not available'),
                )
        serial_number = models.CharField(maxlength=15)
        name = models.CharField(maxlength=100)
        description = models.TextField(blank=True)
        date_added = models.DateField(auto_now_add=True)
        date_removed = models.DateField(blank =True, null=True)
        date_backordered = models.DateField(blank=True, null=True)
        comments = models.TextField(blank=True)
        status = models.CharField(maxlength=3, choices=STATUS_CHOICES, default='stk')

The date_added field will automatically set the date when an Item is created, and we have listed some choices for the status field.

Using newforms

One of the neat things about newforms is that you can create them from specific model classes or their instances:

from django import newforms as forms
from yourproject.yourapplication.models import Item

ItemFormClass = forms.models.form_for_model(Item)   # This creates a form *class* for Item
form = ItemFormClass()                              # Then you instantiate the form class

Note how the formformodel() method created a class for us, and we have to make an instance to use the form. You can look at the HTML generated if you simply print the form in a shell session:

>>> print form
<tr><th><label for="id_serial_number">Serial number:</label></th><td>
     <input id="id_serial_number" type="text" name="serial_number" maxlength="15" /></td></tr>
<tr><th><label for="id_name">Name:</label></th><td>
     <input id="id_name" type="text" name="name" maxlength="100" /></td></tr>
<tr><th><label for="id_description">Description:</label></th><td>
     <textarea name="description" id="id_description"></textarea></td></tr>
<tr><th><label for="id_date_added">Date added:</label></th><td>
     <input type="text" name="date_added" id="id_date_added" /></td></tr>
<tr><th><label for="id_date_removed">Date removed:</label></th><td>
     <input type="text" name="date_removed" id="id_date_removed" /></td></tr>
<tr><th><label for="id_date_backordered">Date backordered:</label></th><td>
     <input type="text" name="date_backordered" id="id_date_backordered" /></td></tr>
<tr><th><label for="id_comments">Comments:</label></th><td>
     <textarea name="comments" id="id_comments"></textarea></td></tr>
<tr><th><label for="id_status">Status:</label></th><td>
     <input id="id_status" type="text" name="status" maxlength="3" /></td></tr>

By default, the form is laid-out as a table, and labels and id's are created for each field in the form. This behavior can be easily changed to other lay-out conventions and tagging schemes. The current Django documentation explains all of these features quite nicely, so I will skip further details here. Also note that the form HTML is not embedded within

and
tags, and the tag is missing. You have to supply those in your own form skeleton. As an example, see the listing of Add_Item.html template below:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
        "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
        <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
        <title>Add Item</title>
</head>

<body>
        <form action="." method="post">
                <table>

                </table>
                <input type="submit" value="Add Item" />
        </form>
</body>
</html>

Now we are in a position to create a new Item entry with the following view:

from django.shortcuts import render_to_response
from django.http import HttpResponse, HttpResponseRedirect, Http404
from yourproject.yourapplication.models import Item
from django import newforms as forms

def add_item(request):
        AddItemFormClass = forms.form_for_model(Item)   # Create the form class

        if request.POST:
                form = AddItemFormClass(request.POST)       # Instantiate and load POST data
                if form.is_valid():                         # Validate data
                        form.save()                             # Add the item
                        return HttpResponseRedirect('/index')
        else:
                form = AddItemFormClass()                   # Instantiate empty form

        return render_to_response('Add_Item.html', {'form': form})

Note how we can initialize an AddItemFormClass instance with POST data or empty. Also, if there are validation errors, the form will automatically be redisplayed with errors listed above the appropriate fields. Finally, saving the form automatically creates a new instance of Item and saves it to the database. Cool!

However, not all is well. The Status field in our form is a text input and we must manually type a choice, such as 'bac'. We need to change this to a popup menu to select a valid choice with a meaningful tag. Thankfully, we can easily correct this using a widget. Widgets are ways of displaying fields in HTML, and we can change the default widget for any field of our AddItemFormClass:

from django.shortcuts import render_to_response
from django.http import HttpResponse, HttpResponseRedirect, Http404
from NewForms.warehouse.models import Item
from django import newforms as forms
from django.newforms import widgets

def add_item(request):
        AddItemFormClass = forms.form_for_model(Item)   # Create the form class
        AddItemFormClass.base_fields['status'].widget = widgets.Select(choices=Item.STATUS_CHOICES)

        if request.POST:
                form = AddItemFormClass(request.POST)    # Instantiate and load POST data
                if form.is_valid():                      # Validate data
                        form.save()                          # Add the item
                        return HttpResponseRedirect('/index')
        else:
                form = AddItemFormClass()                # Instantiate empty

        return render_to_response('Add_Item.html', {'form': form})

You can look at django/newforms/widgets.py to find other available widgets.

For updates of Item entries, we want to pre-populate the form with data. How do we do that? The answer is to create the form class from an instance, not a model class.

def update_item(request, item_id):
        current_item = Item.objects.get(id=item_id)                # Get the Item instance
        AddItemFormClass = forms.form_for_instance(current_item)   # Create the form class
        AddItemFormClass.base_fields['status'].widget = widgets.Select(choices=Item.STATUS_CHOICES)

        if request.POST:
                form = AddItemFormClass(request.POST)       # Instantiate and load POST data
                if form.is_valid():                         # Validate data
                        form.save()                             # Save the item
                        return HttpResponseRedirect('/index')
        else:
                form = AddItemFormClass()                   # Instantiate empty

        return render_to_response('Add_Item.html', {'form': form})

The two view functions additem() and updateitem() are quite similar and you probably would prefer to combine them into a single view. For the sake of simplicity and brevity, that task will be left as an exercise.

So far, we have easily written two views to add new Item entries to our database and to modify current ones. But this has been an all or nothing proposition up to this point: the form displays all the fields in the model. Frequently you will want to modify only some of the fields and leave others hidden. For example, our date_added field automatically sets the date, and we may not want the user to fiddle with that.

def update_description(request, item_id):
        current_item = Item.objects.get(id=item_id)                # Get the Item instance
        AddItemFormClass = forms.form_for_instance(current_item)   # Create the form class
        AddItemFormClass.base_fields['serial_number'].widget = widgets.HiddenInput()
        AddItemFormClass.base_fields['name'].widget = widgets.HiddenInput()
        AddItemFormClass.base_fields['date_added'].widget = widgets.HiddenInput()
        AddItemFormClass.base_fields['date_removed'].widget = widgets.HiddenInput()
        AddItemFormClass.base_fields['date_backordered'].widget = widgets.HiddenInput()
        AddItemFormClass.base_fields['comments'].widget = widgets.HiddenInput()
        AddItemFormClass.base_fields['status'].widget = widgets.HiddenInput()

        if request.POST:
                form = AddItemFormClass(request.POST)     # Instantiate and load POST data
                if form.is_valid():                       # Validate data
                        form.save()                           # Save the item
                        return HttpResponseRedirect('/index')
        else:
                form = AddItemFormClass()                 # Instantiate empty

        return render_to_response('Add_Item.html', {'form': form})

By using the widget HiddenInput, we have left all fields out of the form except for the description, which can be edited.

You can even trick the form into not asking for a required field, and then add the data yourself, programmatically:

def add_item_without_name(request):
        AddItemFormClass = forms.form_for_model(Item)                  # Create the form class
        AddItemFormClass.base_fields['name'].widget = widgets.HiddenInput()   # Hide name field
        AddItemFormClass.base_fields['name'].required = False    # Make name field not required
        AddItemFormClass.base_fields['status'].widget = widgets.Select(choices=Item.STATUS_CHOICES)

        if request.POST:
                form = AddItemFormClass(request.POST)   # Instantiate and load POST data
                if form.is_valid():                     # Validate data
                        newItem = form.save(commit=False)   # Create the item
                        newItem.name = 'To be determined'   # Add the data required by the model
                        newItem.save()
                        return HttpResponseRedirect('/index')
        else:
                form = AddItemFormClass()               # Instantiate empty

        return render_to_response('Add_Item.html', {'form': form})

Even though the name field is required by the model class, we tricked the form into not validating it by setting that field to not required. But if we were to save the new Item at this point, we would have a database error, so we create an Item instance with newItem = form.save(commit=False), where commit=False prevents writing to the DB. After newItem is created, we set a name for it and save the valid Item to the database.

Finally, what if you want to have a completely custom form, not attached to any specific database model? In that case you manually define a form class with any field specifications required:

class CustomForm(forms.Form):
        serial_number = forms.CharField(max_length=15)
        status = forms.CharField(max_length=3, widget=widgets.Select(choices=Item.STATUS_CHOICES))

This custom form will allow us to add an Item to the database with minimal information:

def add_minimal(request):

        if request.POST:
                form = CustomForm(request.POST)     # Instantiate and load POST data
                if form.is_valid():                 # Validate data
                        newItem = Item(serial_number=form.clean_data['serial_number'],
                                                        name='To be determined',
                                                        description='No description',
                                                        comments='No comments',
                                                        status=form.clean_data['status'])
                        newItem.save()

                        return HttpResponseRedirect('/NewForms/index')
        else:
                form = CustomForm()                 # Instantiate empty

        return render_to_response('Add_Item.html', {'form': form})

In this case, we just use the custom form to validate the user data, and just get that valid data to create a new Item instance and save it in the old fashioned way. If you want to show some data on the form, you could use the "initial" field argument when defining the CustomForm class or later using the base_fields dictionary.

class CustomForm(forms.Form):
        serial_number = forms.CharField(max_length=15, initial='Acme_wazmo_123')
        status = forms.CharField(max_length=3,
                             widget=widgets.Select(choices=Item.STATUS_CHOICES),
                             initial='stk')

I hope this article will provide enough information to get you going with newforms in a useful way. If you want to learn more, do not miss the new Django Bootcamp next April. I look forward to seeing many of you there.

Happy coding.

Recent Comments

comments powered by Disqus