mirror of https://github.com/django/django.git
457 lines
19 KiB
Plaintext
457 lines
19 KiB
Plaintext
|
===============================
|
||
|
Forms, fields, and manipulators
|
||
|
===============================
|
||
|
|
||
|
Once you've got a chance to play with Django's admin interface, you'll probably
|
||
|
wonder if the fantastic form validation framework it uses is available to user
|
||
|
code. It is, and this document explains how the framework works.
|
||
|
|
||
|
.. admonition:: A note to the lazy
|
||
|
|
||
|
If all you want to do is present forms for a user to create and/or
|
||
|
update a given object, don't read any further but instead click thyself
|
||
|
over to the `generic views`_ documentation. The following exercises are
|
||
|
for those interested in how Django's form framework works and those
|
||
|
needing to do more than simple create/update.
|
||
|
|
||
|
We'll take a top-down approach to examining Django's form validation framework
|
||
|
since much of the time you won't need to use the lower-level APIs. Throughout
|
||
|
this document, we'll be working with the following model, a "place" object::
|
||
|
|
||
|
PLACE_TYPES = (
|
||
|
(1, 'Bar'),
|
||
|
(2, 'Restaurant'),
|
||
|
(3, 'Movie Theater'),
|
||
|
(4, 'Secret Hideout'),
|
||
|
)
|
||
|
|
||
|
class Place(meta.Model):
|
||
|
fields = (
|
||
|
meta.CharField('name', 'name', maxlength=100),
|
||
|
meta.CharField('address', 'address', maxlength=100, blank=True),
|
||
|
meta.CharField('city', 'city', maxlength=50, blank=True),
|
||
|
meta.USStateField('state', 'state'),
|
||
|
meta.CharField('zip_code', 'zip code', maxlength=5, blank=True),
|
||
|
meta.IntegerField('place_type', 'place type', choices=PLACE_TYPES)
|
||
|
)
|
||
|
|
||
|
def __repr__(self):
|
||
|
return self.name
|
||
|
|
||
|
Defining the above class is enough to create an admin interface to a ``place``,
|
||
|
but what if you want to allow public users to submit places?
|
||
|
|
||
|
Manipulators
|
||
|
============
|
||
|
|
||
|
The highest-level interface for object creation and modification is the
|
||
|
**Manipulator** framework. A manipulator is a utility class tied to a given
|
||
|
model that "knows" how to create or modify instances of that model and how to
|
||
|
validate data for the object. Manipulators come in two flavors:
|
||
|
``AddManipulators`` and ``ChangeManipulators``. Functionally they are quite
|
||
|
similar, but the former knows how to create new instances of the model, while
|
||
|
the later modifies existing instances. Both types of classes are automatically
|
||
|
created when you define a new class::
|
||
|
|
||
|
>>> from django.models.places import places
|
||
|
>>> places.AddManipulator
|
||
|
<class django.models.places.PlaceManipulatorAdd at 0x4c1540>
|
||
|
>>> places.ChangeManipulator
|
||
|
<class django.models.places.PlaceManipulatorChange at 0x4c1630>
|
||
|
|
||
|
Using the ``AddManipulator``
|
||
|
----------------------------
|
||
|
|
||
|
We'll start with the ``AddManipulator``. Here's a very simple view that takes
|
||
|
POSTed data from the browser and creates a new ``Place`` object::
|
||
|
|
||
|
from django.core import template_loader
|
||
|
from django.core.exceptions import Http404
|
||
|
from django.core.extensions import DjangoContext as Context
|
||
|
from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
|
||
|
from django.models.places import places
|
||
|
from django.core import formfields
|
||
|
|
||
|
def naive_create_place(request):
|
||
|
"""A naive approach to creating places; don't actually use this!"""
|
||
|
# Create the AddManipulator
|
||
|
manipulator = places.AddManipulator()
|
||
|
|
||
|
# Make a copy of the POSTed data so that do_html2python can
|
||
|
# modify it in place (request.POST is immutable)
|
||
|
new_data = request.POST.copy()
|
||
|
|
||
|
# Convert the request data (which will all be strings) into the
|
||
|
# appropriate Python types for those fields
|
||
|
manipulator.do_html2python(new_data)
|
||
|
|
||
|
# Save the new object
|
||
|
new_place = manipulator.save(new_data)
|
||
|
|
||
|
# It worked!
|
||
|
return HttpResponse("Place created: %s" % new_place)
|
||
|
|
||
|
The ``naive_create_place`` example works (somewhat), but as you probably can
|
||
|
tell, there's all sorts of problems (some more subtle than others) with this view:
|
||
|
|
||
|
* No validation of any sort is performed; if, for example, the ``name`` field
|
||
|
isn't given in ``request.POST``, the save step will cause a database error
|
||
|
because that field is required. Ugly.
|
||
|
|
||
|
* Even if you *do* perform validation, there's still no way to give that information
|
||
|
to the user is any sort of useful way.
|
||
|
|
||
|
* You'll have to separate create a form (and view) that submits to this page, which is
|
||
|
a pain and is redundant.
|
||
|
|
||
|
Let's dodge these problems momentarily to take a look at how you could create a
|
||
|
view with a form that submits to this flawed creation view::
|
||
|
|
||
|
def naive_create_place_form(request):
|
||
|
"""Simplistic place form view; don't actually use anything like this!"""
|
||
|
# Create a FormWrapper object which the template can use; more
|
||
|
# on what the second two arguments to FormWrapper do later.
|
||
|
form = formfields.FormWrapper(places.AddManipulator(), {}, {})
|
||
|
|
||
|
# Create a template, context, and response
|
||
|
t = template_loader.get_template('places/naive_create_form')
|
||
|
c = Context(request, {'form' : form})
|
||
|
return HttpResponse(t.render(c))
|
||
|
|
||
|
(This view, as well as all the following ones, have the same imports as the
|
||
|
first example above does.)
|
||
|
|
||
|
The ``formfields.FormWrapper`` object is a wrapper that templates can
|
||
|
easily deal with to create forms; here's the ``naive_create_form`` template::
|
||
|
|
||
|
{% extends "base" %}
|
||
|
|
||
|
{% block content %}
|
||
|
<h1>Create a place:</h1>
|
||
|
|
||
|
<form method="post" action="../do_new/">
|
||
|
<p><label for="id_name">Name:</label> {{ form.name }}</p>
|
||
|
<p><label for="id_address">Address:</label> {{ form.address }}</p>
|
||
|
<p><label for="id_city">City:</label> {{ form.city }}</p>
|
||
|
<p><label for="id_state">State:</label> {{ form.state }}</p>
|
||
|
<p><label for="id_zip_code">Zip:</label> {{ form.zip_code }}</p>
|
||
|
<p><label for="id_place_type">Place type:</label> {{ form.place_type }}</p>
|
||
|
<input type="submit" />
|
||
|
</form>
|
||
|
{% endblock %}
|
||
|
|
||
|
Before we get back to the problems with these naive set of views, let's go over
|
||
|
some salient points of the above template::
|
||
|
|
||
|
* Field "widgets" are handled for you: ``{{ form.field }}`` automatically
|
||
|
creates the "right" type of widget for the form, as you can see with the
|
||
|
``place_type`` field above.
|
||
|
|
||
|
* There isn't a way just to spit out the form; you'll still need to define
|
||
|
how the form gets laid out. This is a feature: every form needs to be
|
||
|
designed differently; Django doesn't force you into any type of mould.
|
||
|
If you must use tables, use tables; if you're a semantic purist you can
|
||
|
probably find better HTML than the above template.
|
||
|
|
||
|
* To avoid name conflicts, the ``id``s of form elements take the form
|
||
|
"id_*fieldname*".
|
||
|
|
||
|
By creating a creation form we've solved problem number 3 above, but we still don't
|
||
|
have any validation; if you enter bad data into any of the . Let's revise the validation
|
||
|
issue by writing a new creation view that takes into account validation::
|
||
|
|
||
|
def create_place_with_validation(request):
|
||
|
manipulator = places.AddManipulator()
|
||
|
new_data = request.POST.copy()
|
||
|
|
||
|
# Check for validation errors
|
||
|
errors = manipulator.get_validation_errors(new_data)
|
||
|
if errors:
|
||
|
t = template_loader.get_template('places/errors')
|
||
|
c = Context(request, {'errors' : errors}
|
||
|
return HttpResponse(t.render(c))
|
||
|
else:
|
||
|
manipulator.do_html2python(request.POST)
|
||
|
new_place = manipulator.save(request.POST)
|
||
|
return HttpResponse("Place created: %s" % new_place)
|
||
|
|
||
|
In this new version, errors will be found -- ``manipulator.get_validation_errors``
|
||
|
handles all the validation for you -- and those errors can be nicely presented
|
||
|
on an error page (templated, of course)::
|
||
|
|
||
|
{% extends "base" %}
|
||
|
|
||
|
{% block content %}
|
||
|
|
||
|
<h1>Please go back and correct the following error{{ errors|pluralize }}:</h1>
|
||
|
<ul>
|
||
|
{% for e in errors.items %}
|
||
|
<li>Field "{{ e.0 }}": {{ e.1|join:", " }}</li>
|
||
|
{% endfor %}
|
||
|
</ul>
|
||
|
|
||
|
{% endblock %}
|
||
|
|
||
|
Still, this now has its own problems:
|
||
|
|
||
|
* There's still the issue of creating a seperate (redundant) view for the
|
||
|
submission form.
|
||
|
|
||
|
* Errors, though nicely presented are on a seperate page, so the user will have
|
||
|
to use the "back" button to fix errors -- not exactly usable!
|
||
|
|
||
|
The best way to deal with these issues is to collapse the two views -- the form and the
|
||
|
submission -- into a single view. This view will be responsible for creating the
|
||
|
form, validating POSTed data, and creating the new object (should it the data be
|
||
|
valid). An added bonus of this approach is that errors and the form will both
|
||
|
be available on the same page, so errors with fields can be presented in context.
|
||
|
|
||
|
.. admonition:: Philosophy::
|
||
|
|
||
|
Finally, for the HTTP purists in the audience (and the authorship), this
|
||
|
nicely matches the "true" meanings of HTTP-GET and HTTP-POST: GET fetches
|
||
|
the form, POST creates the new object.
|
||
|
|
||
|
Below is the finished view::
|
||
|
|
||
|
def create_place(request):
|
||
|
manipulator = places.AddManipulator()
|
||
|
|
||
|
if request.POST:
|
||
|
# If data was POSTed, we're trying to create a new Place
|
||
|
new_data = request.POST.copy()
|
||
|
|
||
|
# Check for errors
|
||
|
errors = manipulator.get_validation_errors(new_data)
|
||
|
|
||
|
if not errors:
|
||
|
# No errors -- this means we can save the data!
|
||
|
manipulator.do_html2python(new_data)
|
||
|
new_place = manipulator.save(new_data)
|
||
|
|
||
|
# Redirect to the object's "edit" page (so that reloads
|
||
|
# don't accidentally create duplicate entries)
|
||
|
return HttpResponseRedirect("/places/edit/%i/" % new_place.id)
|
||
|
else:
|
||
|
# No POST, so we want a brand new form without any data or errors
|
||
|
errors = new_data = {}
|
||
|
|
||
|
# Create the FormWrapper, template, context, response
|
||
|
form = formfields.FormWrapper(manipulator, new_data, errors)
|
||
|
t = template_loader.get_template("places/create_form")
|
||
|
c = Context(request, {
|
||
|
'form' : form,
|
||
|
})
|
||
|
return HttpResponse(t.render(c))
|
||
|
|
||
|
and here's the ``create_form`` template::
|
||
|
|
||
|
{% extends "base" %}
|
||
|
|
||
|
{% block content %}
|
||
|
<h1>Create a place:</h1>
|
||
|
|
||
|
{% if form.has_errors %}
|
||
|
<h2>Please correct the following error{{ errors|pluralize }}:</h2>
|
||
|
{% endif %}
|
||
|
|
||
|
<form method="post" action=".">
|
||
|
<p>
|
||
|
<label for="id_name">Name:</label> {{ form.name }}
|
||
|
{% if form.name.errors %}*** {{ form.name.errors|join:", " }}{% endif %}
|
||
|
</p>
|
||
|
<p>
|
||
|
<label for="id_address">Address:</label> {{ form.address }}
|
||
|
{% if form.address.errors %}*** {{ form.address.errors|join:", " }}{% endif %}
|
||
|
</p>
|
||
|
<p>
|
||
|
<label for="id_city">City:</label> {{ form.city }}
|
||
|
{% if form.city.errors %}*** {{ form.city.errors|join:", " }}{% endif %}
|
||
|
</p>
|
||
|
<p>
|
||
|
<label for="id_state">State:</label> {{ form.state }}
|
||
|
{% if form.state.errors %}*** {{ form.state.errors|join:", " }}{% endif %}
|
||
|
</p>
|
||
|
<p>
|
||
|
<label for="id_zip_code">Zip:</label> {{ form.zip_code }}
|
||
|
{% if form.zip_code.errors %}*** {{ form.zip_code.errors|join:", " }}{% endif %}
|
||
|
</p>
|
||
|
<p>
|
||
|
<label for="id_place_type">Place type:</label> {{ form.place_type }}
|
||
|
{% if form.place_type.errors %}*** {{ form.place_type.errors|join:", " }}{% endif %}
|
||
|
</p>
|
||
|
<input type="submit" />
|
||
|
</form>
|
||
|
{% endblock %}
|
||
|
|
||
|
The second two arguments to ``FormWrapper`` (``new_data`` and ``errors``)
|
||
|
deserve some mention.
|
||
|
|
||
|
The first is any "default" data to be used as values for the fields; pulling the
|
||
|
data from ``request.POST`` as is done above makes sure that if there are errors,
|
||
|
the values the user put in aren't lost. If you try the above example, you'll see
|
||
|
this in action.
|
||
|
|
||
|
The second argument is the error list retrieved from
|
||
|
``manipulator.get_validation_errors``. When passed into the ``FormWrapper``, this gives
|
||
|
each field an ``errors`` item (which is a list of error messages associated with the
|
||
|
field) as well as a ``html_error_list`` item which is a ``<ul>`` of error messages.
|
||
|
The above template uses these error items to display a simple error message next
|
||
|
to each field.
|
||
|
|
||
|
Using the ``ChangeManipulator``
|
||
|
-------------------------------
|
||
|
|
||
|
So: the above has covered using the ``AddManipulator`` to create a new object;
|
||
|
what about editing an existing one? It's rather shockingly similar to creating
|
||
|
a new one::
|
||
|
|
||
|
def edit_place(request, place_id):
|
||
|
# Get the place in question from the database and create a ChangeManipulator
|
||
|
# at the same time
|
||
|
try:
|
||
|
manipulator = places.ChangeManipulator(place_id)
|
||
|
except places.PlaceDoesNotExist:
|
||
|
raise Http404
|
||
|
|
||
|
# Grab the Place object is question for future use
|
||
|
place = manipulator.original_object
|
||
|
|
||
|
if request.POST:
|
||
|
new_data = request.POST.copy()
|
||
|
errors = manipulator.get_validation_errors(new_data)
|
||
|
if not errors:
|
||
|
manipulator.do_html2python(new_data)
|
||
|
manipulator.save(new_data)
|
||
|
|
||
|
# Do a post-after-redirect so that reload works, etc.
|
||
|
return HttpResponseRedirect("/places/edit/%i/" % place.id)
|
||
|
else:
|
||
|
errors = {}
|
||
|
# This makes sure the form accurate represents the fields of the place.
|
||
|
new_data = place.__dict__
|
||
|
|
||
|
form = formfields.FormWrapper(manipulator, new_data, errors)
|
||
|
t = template_loader.get_template("places/edit_form")
|
||
|
c = Context(request, {
|
||
|
'form' : form,
|
||
|
'place' : place,
|
||
|
})
|
||
|
return HttpResponse(t.render(c))
|
||
|
|
||
|
The only real differences here are:
|
||
|
|
||
|
* A ``ChangeManipulator`` instead of an ``AddManipulator`` is created;
|
||
|
The argument to any ``ChangeManipulator`` is the id of the object
|
||
|
to be changed. As you can see, the initializer will raise an
|
||
|
``ObjectDoesNotExist`` exception if the id is invalid.
|
||
|
|
||
|
* ``ChangeManipulator.original_object`` stores the instance of the
|
||
|
object being edited.
|
||
|
|
||
|
* We set ``new_data`` to the original object's ``__dict__``; this makes
|
||
|
sure that the form fields contain the current values of the object.
|
||
|
``FormWrapper`` does not modify ``new_data`` in any way, and templates
|
||
|
cannot, so this is perfectly safe.
|
||
|
|
||
|
* The above example uses a different template so that create and edit can
|
||
|
be "skinned" differently if needed, but the form chunk itself is
|
||
|
completely identical to the one in the create form above.
|
||
|
|
||
|
The astute programmer will notice that the add and create functions are nearly
|
||
|
identical and could in fact be collapsed into a single view; this is left
|
||
|
as an exercise for said programmer.
|
||
|
|
||
|
(However, the even-more-astute programmer will take heed of the note at the top
|
||
|
of this document and check out the `generic views`_ documentation if all she
|
||
|
wishes to do is this type of simple create/update).
|
||
|
|
||
|
Custom forms and manipulators
|
||
|
=============================
|
||
|
|
||
|
All the above is fine and dandy if you want to just use the automatically created
|
||
|
manipulators, but the coolness doesn't end there: you can easily create your
|
||
|
own custom manipulators for handling custom forms.
|
||
|
|
||
|
Custom manipulators are pretty simple; here's a manipulator that you might use
|
||
|
for a "contact" form on a website::
|
||
|
|
||
|
from django.core import formfields
|
||
|
|
||
|
urgency_choices = (
|
||
|
(1, "Extremely urgent"),
|
||
|
(2, "Urgent"),
|
||
|
(3, "Normal"),
|
||
|
(4, "Unimportant"),
|
||
|
)
|
||
|
|
||
|
class ContactManipulator(formfields.Manipulator):
|
||
|
def __init__(self):
|
||
|
self.fields = (
|
||
|
formfields.EmailField(field_name="from", is_required=True),
|
||
|
formfields.TextField(field_name="subject", length=30, maxlength=200, is_required=True),
|
||
|
formfields.IntegerField(field_name="urgency", choices=urgency_choices),
|
||
|
formfields.LargeTextField(field_name="contents", is_required=True),
|
||
|
)
|
||
|
|
||
|
A certain similarity to Django's models should be apparent. The only required
|
||
|
method of a custom manipulator is ``__init__`` which must define the fields
|
||
|
present in the manipulator. See the ``django.core.formfields`` module for
|
||
|
all the form fields provided by Django.
|
||
|
|
||
|
You use this custom manipulator exactly as you would use an auto-generated one;
|
||
|
here's a simple function that might drive the above form::
|
||
|
|
||
|
def contact_form(request):
|
||
|
manipulator = ContactFormManipulator()
|
||
|
if request.POST:
|
||
|
new_data = request.POST.copy()
|
||
|
errors = manipulator.get_validation_errors(new_data)
|
||
|
if not errors:
|
||
|
manipulator.do_html2python(new_data)
|
||
|
|
||
|
# send email using new_data here...
|
||
|
|
||
|
return HttpResponseRedirect("/contact/thankyou/")
|
||
|
else:
|
||
|
errors = new_data = {}
|
||
|
form = formfields.FormWrapper(manipulator, new_data, errors)
|
||
|
t = template_loader.get_template("contact_form")
|
||
|
c = Context(request, {
|
||
|
'form' : form,
|
||
|
})
|
||
|
return HttpResponse(t.render(c))
|
||
|
|
||
|
Validators
|
||
|
==========
|
||
|
|
||
|
One extremely useful feature of manipulators is the automatic validation it
|
||
|
performs. Validation is done using a simple validation API: a validator is
|
||
|
simple a callable that raises a ``ValidationError`` if there's something wrong
|
||
|
with the data. ``django.core.validators`` defines a whole host of validator
|
||
|
functions, but defining your own couldn't be easier::
|
||
|
|
||
|
from django.core import validators, formfields
|
||
|
|
||
|
class ContactManipulator(formfields.Manipulator):
|
||
|
def __init__(self):
|
||
|
self.fields = (
|
||
|
# ... snip fields as above ...
|
||
|
formfields.EmailField(field_name="to", validator_list=[self.isValidToAddress])
|
||
|
)
|
||
|
|
||
|
def isValidToAddress(self, field_data, all_data):
|
||
|
if not field_data.endswith("@example.com"):
|
||
|
raise ValidationError("You can only send messages to example.com email addresses")
|
||
|
|
||
|
Above, we've added a "to" field to the contact form, but required that the
|
||
|
"to" address end with "@example.com" by adding the ``isValidToAddress``
|
||
|
validator to the field's ``validator_list``.
|
||
|
|
||
|
The arguments to a validator function take a little explanation. ``field_data``
|
||
|
is the value of the field in question, and ``all_data`` is a dict of all the
|
||
|
data being validated. Note that at the point validators are called all data
|
||
|
will still be strings (as ``do_html2python`` hasn't been called yet).
|
||
|
|
||
|
.. _`generic views`: http://www.djangoproject.com/documentation/generic_views/
|