Feb 20 2007

Using the Django newforms Library

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 form_for_model() 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 <table></table> and <form></form> tags, and the <input type=”submit”> 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>
                        {{ form }}
                </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 add_item() and update_item() 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.

10 Comments

  1. T

    Status field gets rendered as type=”text” instead of select even though we use a Select widget. It works fine if using python manage.py shell. Why is it so?

  2. If it works from the interactive shell but not on the application, it may be a silly typo on the line where you assign the Select widget to the field. You can also place an “assert False” statement after assigning the widget to scrutinize the status of the application. And you can add print statements and the result will be outputted to the console where you are running the development server. Even though I copied the code from a working application, I may have introduced errors in the HTML formatting for this blog. Please let me know if you find any.

    Good luck.

  3. TakynRun

    Just so you know, making a form field hidden will _not_ prevent a user from being able to change it. You either need to add additional protection, or simply create an unattached form and create the item yourself, as in your last example.

  4. Shayan Raghavjee

    T, ja, Django doesn’t appear to generate the choices as defined in the model. I don’t know if it’s quite Django-kosher but in yours views.py you can do something like:

    AddItemFormClass.base_fields['status'].widget = widgets.Select(choices=Item.STATUS_CHOICES)

  5. anonymous

    juanpaul, your article says the class is April 2-6, but the bignerdranch.com website says it is April 7-11. Can you confirm which date is correct?

  6. This blog was posted on 2007, and April 2-6 were the dates for the bootcamp then. This year (2008), the Django bootcamp will be held on April 7-11.

    I hope to see you there!

  7. Jon

    Brilliant! I’ve been trawling the web for hours looking for the clear and simple explanation you’ve provided. Your article ties numerous concepts together quickly and accurately, and is more useful than both the offical documentation and the “The Definitive Guide to django” book.

    Many Thanks.

  8. Monica

    hi, i see that you’re quite good at that, i’m just starting with python and django, and i have a problem, during making a registration form, i have this kind of bug in forms.py:
    ” cannot import name newforms”
    cause I have a line in code sth like “from django import newforms as forms” and It’s looks like it can’t import this library or sth…for any help I’ll by thankful:)

  9. Monica:

    This blog is a little old, and it will not work correctly with Django 1.1.1, the current official release. The “newforms” name has been dropped from django and you must import the library with “from django import forms”. Then the rest of the code should work OK. However, when I wrote this example, the forms library was incomplete and things had to be done in a somewhat forced way. Please read the django forms documentation at djangoproject.com for more up-to-date example.

    Good luck!

  10. rene

    ImportError at /admin/

    cannot import name newforms

    Request Method: GET
    Request URL: http://127.0.0.1:8000/admin/
    Django Version: 1.3.1
    Exception Type: ImportError
    Exception Value:

    cannot import name newforms

    Exception Location: /forms.py in , line 6
    Python Executable: /usr/bin/python
    Python Version: 2.7.3

Leave a Comment

Join the discussion. Do not worry, your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>