Django

Code

root/django/branches/new-admin/docs/forms.txt

Revision 1361, 17.8 kB (checked in by rjwittams, 3 years ago)

Merged to trunk r1359

Line 
1 ===============================
2 Forms, fields, and manipulators
3 ===============================
4
5 Once you've got a chance to play with Django's admin interface, you'll probably
6 wonder if the fantastic form validation framework it uses is available to user
7 code. It is, and this document explains how the framework works.
8
9     .. admonition:: A note to the lazy
10
11         If all you want to do is present forms for a user to create and/or
12         update a given object, don't read any further. Instead, click thyself
13         to the `generic views`_ documentation. The following exercises are
14         for those interested in how Django's form framework works and those
15         needing to do more than simple creation/updating.
16
17 We'll take a top-down approach to examining Django's form validation framework,
18 because much of the time you won't need to use the lower-level APIs. Throughout
19 this document, we'll be working with the following model, a "place" object::
20
21     PLACE_TYPES = (
22         (1, 'Bar'),
23         (2, 'Restaurant'),
24         (3, 'Movie Theater'),
25         (4, 'Secret Hideout'),
26     )
27
28     class Place(meta.Model):
29         name = meta.CharField(maxlength=100),
30         address = meta.CharField(maxlength=100, blank=True),
31         city = meta.CharField(maxlength=50, blank=True),
32         state = meta.USStateField(),
33         zip_code = meta.CharField(maxlength=5, blank=True),
34         place_type = meta.IntegerField(choices=PLACE_TYPES)
35         class META:
36             admin = meta.Admin()
37
38         def __repr__(self):
39             return self.name
40
41 Defining the above class is enough to create an admin interface to a ``place``,
42 but what if you want to allow public users to submit places?
43
44 Manipulators
45 ============
46
47 The highest-level interface for object creation and modification is the
48 **Manipulator** framework. A manipulator is a utility class tied to a given
49 model that "knows" how to create or modify instances of that model and how to
50 validate data for the object. Manipulators come in two flavors:
51 ``AddManipulators`` and ``ChangeManipulators``. Functionally they are quite
52 similar, but the former knows how to create new instances of the model, while
53 the later modifies existing instances.  Both types of classes are automatically
54 created when you define a new class::
55
56     >>> from django.models.places import places
57     >>> places.AddManipulator
58     <class django.models.places.PlaceManipulatorAdd at 0x4c1540>
59     >>> places.ChangeManipulator
60     <class django.models.places.PlaceManipulatorChange at 0x4c1630>
61
62 Using the ``AddManipulator``
63 ----------------------------
64
65 We'll start with the ``AddManipulator``.  Here's a very simple view that takes
66 POSTed data from the browser and creates a new ``Place`` object::
67
68     from django.core.exceptions import Http404
69     from django.core.extensions import render_to_response
70     from django.utils.httpwrappers import HttpResponse, HttpResponseRedirect
71     from django.models.places import places
72     from django.core import formfields
73
74     def naive_create_place(request):
75         """A naive approach to creating places; don't actually use this!"""
76         # Create the AddManipulator.
77         manipulator = places.AddManipulator()
78
79         # Make a copy of the POSTed data so that do_html2python can
80         # modify it in place (request.POST is immutable).
81         new_data = request.POST.copy()
82
83         # Convert the request data (which will all be strings) into the
84         # appropriate Python types for those fields.
85         manipulator.do_html2python(new_data)
86
87         # Save the new object.
88         new_place = manipulator.save(new_data)
89
90         # It worked!
91         return HttpResponse("Place created: %s" % new_place)
92
93 The ``naive_create_place`` example works, but as you probably can tell, this
94 view has a number of problems:
95
96     * No validation of any sort is performed. If, for example, the ``name`` field
97       isn't given in ``request.POST``, the save step will cause a database error
98       because that field is required. Ugly.
99
100     * Even if you *do* perform validation, there's still no way to give that
101       information to the user is any sort of useful way.
102
103     * You'll have to separately create a form (and view) that submits to this
104       page, which is a pain and is redundant.
105
106 Let's dodge these problems momentarily to take a look at how you could create a
107 view with a form that submits to this flawed creation view::
108
109     def naive_create_place_form(request):
110         """Simplistic place form view; don't actually use anything like this!"""
111         # Create a FormWrapper object that the template can use. Ignore
112         # the last two arguments to FormWrapper for now.
113         form = formfields.FormWrapper(places.AddManipulator(), {}, {})
114         return render_to_response('places/naive_create_form', {'form': form})
115
116 (This view, as well as all the following ones, has the same imports as in the
117 first example above.)
118
119 The ``formfields.FormWrapper`` object is a wrapper that templates can
120 easily deal with to create forms. Here's the ``naive_create_form`` template::
121
122     {% extends "base" %}
123
124     {% block content %}
125     <h1>Create a place:</h1>
126
127     <form method="post" action="../do_new/">
128     <p><label for="id_name">Name:</label> {{ form.name }}</p>
129     <p><label for="id_address">Address:</label> {{ form.address }}</p>
130     <p><label for="id_city">City:</label> {{ form.city }}</p>
131     <p><label for="id_state">State:</label> {{ form.state }}</p>
132     <p><label for="id_zip_code">Zip:</label> {{ form.zip_code }}</p>
133     <p><label for="id_place_type">Place type:</label> {{ form.place_type }}</p>
134     <input type="submit" />
135     </form>
136     {% endblock %}
137
138 Before we get back to the problems with these naive set of views, let's go over
139 some salient points of the above template::
140
141     * Field "widgets" are handled for you: ``{{ form.field }}`` automatically
142       creates the "right" type of widget for the form, as you can see with the
143       ``place_type`` field above.
144
145     * There isn't a way just to spit out the form. You'll still need to define
146       how the form gets laid out. This is a feature: Every form should be
147       designed differently. Django doesn't force you into any type of mold.
148       If you must use tables, use tables. If you're a semantic purist, you can
149       probably find better HTML than in the above template.
150
151     * To avoid name conflicts, the ``id``s of form elements take the form
152       "id_*fieldname*".
153
154 By creating a creation form we've solved problem number 3 above, but we still
155 don't have any validation. Let's revise the validation issue by writing a new
156 creation view that takes validation into account::
157
158     def create_place_with_validation(request):
159         manipulator = places.AddManipulator()
160         new_data = request.POST.copy()
161
162         # Check for validation errors
163         errors = manipulator.get_validation_errors(new_data)
164         if errors:
165             return render_to_response('places/errors', {'errors': errors})
166         else:
167             manipulator.do_html2python(request.POST)
168             new_place = manipulator.save(request.POST)
169             return HttpResponse("Place created: %s" % new_place)
170
171 In this new version, errors will be found -- ``manipulator.get_validation_errors``
172 handles all the validation for you -- and those errors can be nicely presented
173 on an error page (templated, of course)::
174
175     {% extends "base" %}
176
177     {% block content %}
178
179     <h1>Please go back and correct the following error{{ errors|pluralize }}:</h1>
180     <ul>
181         {% for e in errors.items %}
182         <li>Field "{{ e.0 }}": {{ e.1|join:", " }}</li>
183         {% endfor %}
184     </ul>
185
186     {% endblock %}
187
188 Still, this has its own problems:
189
190     * There's still the issue of creating a separate (redundant) view for the
191       submission form.
192
193     * Errors, though nicely presented, are on a separate page, so the user will
194       have to use the "back" button to fix errors. That's ridiculous and unusable.
195
196 The best way to deal with these issues is to collapse the two views -- the form
197 and the submission -- into a single view.  This view will be responsible for
198 creating the form, validating POSTed data, and creating the new object (if the
199 data is valid). An added bonus of this approach is that errors and the form will
200 both be available on the same page, so errors with fields can be presented in
201 context.
202
203 .. admonition:: Philosophy::
204
205     Finally, for the HTTP purists in the audience (and the authorship), this
206     nicely matches the "true" meanings of HTTP GET and HTTP POST: GET fetches
207     the form, and POST creates the new object.
208
209 Below is the finished view::
210
211     def create_place(request):
212         manipulator = places.AddManipulator()
213
214         if request.POST:
215             # If data was POSTed, we're trying to create a new Place.
216             new_data = request.POST.copy()
217
218             # Check for errors.
219             errors = manipulator.get_validation_errors(new_data)
220
221             if not errors:
222                 # No errors. This means we can save the data!
223                 manipulator.do_html2python(new_data)
224                 new_place = manipulator.save(new_data)
225
226                 # Redirect to the object's "edit" page. Always use a redirect
227                 # after POST data, so that reloads don't accidently create
228                 # duplicate entires, and so users don't see the confusing
229                 # "Repost POST data?" alert box in their browsers.
230                 return HttpResponseRedirect("/places/edit/%i/" % new_place.id)
231         else:
232             # No POST, so we want a brand new form without any data or errors.
233             errors = new_data = {}
234
235         # Create the FormWrapper, template, context, response.
236         form = formfields.FormWrapper(manipulator, new_data, errors)
237         return render_to_response('places/create_form', {'form': form})
238
239 and here's the ``create_form`` template::
240
241     {% extends "base" %}
242
243     {% block content %}
244     <h1>Create a place:</h1>
245
246     {% if form.has_errors %}
247     <h2>Please correct the following error{{ errors|pluralize }}:</h2>
248     {% endif %}
249
250     <form method="post" action=".">
251     <p>
252         <label for="id_name">Name:</label> {{ form.name }}
253         {% if form.name.errors %}*** {{ form.name.errors|join:", " }}{% endif %}
254     </p>
255     <p>
256         <label for="id_address">Address:</label> {{ form.address }}
257         {% if form.address.errors %}*** {{ form.address.errors|join:", " }}{% endif %}
258     </p>
259     <p>
260         <label for="id_city">City:</label> {{ form.city }}
261         {% if form.city.errors %}*** {{ form.city.errors|join:", " }}{% endif %}
262     </p>
263     <p>
264         <label for="id_state">State:</label> {{ form.state }}
265         {% if form.state.errors %}*** {{ form.state.errors|join:", " }}{% endif %}
266     </p>
267     <p>
268         <label for="id_zip_code">Zip:</label> {{ form.zip_code }}
269         {% if form.zip_code.errors %}*** {{ form.zip_code.errors|join:", " }}{% endif %}
270     </p>
271     <p>
272         <label for="id_place_type">Place type:</label> {{ form.place_type }}
273         {% if form.place_type.errors %}*** {{ form.place_type.errors|join:", " }}{% endif %}
274     </p>
275     <input type="submit" />
276     </form>
277     {% endblock %}
278
279 The second two arguments to ``FormWrapper`` (``new_data`` and ``errors``)
280 deserve some mention.
281
282 The first is any "default" data to be used as values for the fields. Pulling
283 the data from ``request.POST``, as is done above, makes sure that if there are
284 errors, the values the user put in aren't lost. If you try the above example,
285 you'll see this in action.
286
287 The second argument is the error list retrieved from
288 ``manipulator.get_validation_errors``.  When passed into the ``FormWrapper``,
289 this gives each field an ``errors`` item (which is a list of error messages
290 associated with the field) as well as a ``html_error_list`` item, which is a
291 ``<ul>`` of error messages. The above template uses these error items to
292 display a simple error message next to each field.
293
294 Using the ``ChangeManipulator``
295 -------------------------------
296
297 The above has covered using the ``AddManipulator`` to create a new object. What
298 about editing an existing one? It's shockingly similar to creating a new one::
299
300     def edit_place(request, place_id):
301         # Get the place in question from the database and create a
302         # ChangeManipulator at the same time.
303         try:
304             manipulator = places.ChangeManipulator(place_id)
305         except places.PlaceDoesNotExist:
306             raise Http404
307
308         # Grab the Place object in question for future use.
309         place = manipulator.original_object
310
311         if request.POST:
312             new_data = request.POST.copy()
313             errors = manipulator.get_validation_errors(new_data)
314             if not errors:
315                 manipulator.do_html2python(new_data)
316                 manipulator.save(new_data)
317
318                 # Do a post-after-redirect so that reload works, etc.
319                 return HttpResponseRedirect("/places/edit/%i/" % place.id)
320         else:
321             errors = {}
322             # This makes sure the form accurate represents the fields of the place.
323             new_data = place.__dict__
324
325         form = formfields.FormWrapper(manipulator, new_data, errors)
326         return render_to_response('places/edit_form', {'form': form, 'place': place})
327
328 The only real differences are:
329
330     * We create a ``ChangeManipulator`` instead of an ``AddManipulator``.
331       The argument to a ``ChangeManipulator`` is the ID of the object
332       to be changed. As you can see, the initializer will raise an
333       ``ObjectDoesNotExist`` exception if the ID is invalid.
334
335     * ``ChangeManipulator.original_object`` stores the instance of the
336       object being edited.
337
338     * We set ``new_data`` to the original object's ``__dict__``. This makes
339       sure the form fields contain the current values of the object.
340       ``FormWrapper`` does not modify ``new_data`` in any way, and templates
341       cannot, so this is perfectly safe.
342
343     * The above example uses a different template, so create and edit can be
344       "skinned" differently if needed, but the form chunk itself is completely
345       identical to the one in the create form above.
346
347 The astute programmer will notice the add and create functions are nearly
348 identical and could in fact be collapsed into a single view. This is left as an
349 exercise for said programmer.
350
351 (However, the even-more-astute programmer will take heed of the note at the top
352 of this document and check out the `generic views`_ documentation if all she
353 wishes to do is this type of simple create/update.)
354
355 Custom forms and manipulators
356 =============================
357
358 All the above is fine and dandy if you just want to use the automatically
359 created manipulators. But the coolness doesn't end there: You can easily create
360 your own custom manipulators for handling custom forms.
361
362 Custom manipulators are pretty simple. Here's a manipulator that you might use
363 for a "contact" form on a website::
364
365     from django.core import formfields
366
367     urgency_choices = (
368         (1, "Extremely urgent"),
369         (2, "Urgent"),
370         (3, "Normal"),
371         (4, "Unimportant"),
372     )
373
374     class ContactManipulator(formfields.Manipulator):
375         def __init__(self):
376             self.fields = (
377                 formfields.EmailField(field_name="from", is_required=True),
378                 formfields.TextField(field_name="subject", length=30, maxlength=200, is_required=True),
379                 formfields.SelectField(field_name="urgency", choices=urgency_choices),
380                 formfields.LargeTextField(field_name="contents", is_required=True),
381             )
382
383 A certain similarity to Django's models should be apparent. The only required
384 method of a custom manipulator is ``__init__`` which must define the fields
385 present in the manipulator.  See the ``django.core.formfields`` module for
386 all the form fields provided by Django.
387
388 You use this custom manipulator exactly as you would use an auto-generated one.
389 Here's a simple function that might drive the above form::
390
391     def contact_form(request):
392         manipulator = ContactManipulator()
393         if request.POST:
394             new_data = request.POST.copy()
395             errors = manipulator.get_validation_errors(new_data)
396             if not errors:
397                 manipulator.do_html2python(new_data)
398
399                 # Send e-mail using new_data here...
400
401                 return HttpResponseRedirect("/contact/thankyou/")
402         else:
403             errors = new_data = {}
404         form = formfields.FormWrapper(manipulator, new_data, errors)
405         return render_to_response('contact_form', {'form': form})
406
407 Validators
408 ==========
409
410 One useful feature of manipulators is the automatic validation. Validation is
411 done using a simple validation API: A validator is a callable that raises a
412 ``ValidationError`` if there's something wrong with the data.
413 ``django.core.validators`` defines a host of validator functions, but defining
414 your own couldn't be easier::
415
416     from django.core import validators, formfields
417
418     class ContactManipulator(formfields.Manipulator):
419         def __init__(self):
420             self.fields = (
421                 # ... snip fields as above ...
422                 formfields.EmailField(field_name="to", validator_list=[self.isValidToAddress])
423             )
424
425         def isValidToAddress(self, field_data, all_data):
426             if not field_data.endswith("@example.com"):
427                 raise ValidationError("You can only send messages to example.com e-mail addresses.")
428
429 Above, we've added a "to" field to the contact form, but required that the "to"
430 address end with "@example.com" by adding the ``isValidToAddress`` validator to
431 the field's ``validator_list``.
432
433 The arguments to a validator function take a little explanation.  ``field_data``
434 is the value of the field in question, and ``all_data`` is a dictionary of all
435 the data being validated.  Note that at the point validators are called all
436 data will still be strings (as ``do_html2python`` hasn't been called yet).
437
438 Also, because consistency in user interfaces is important, we strongly urge you
439 to put punctuation at the end of your validation messages.
440
441 .. _`generic views`: http://www.djangoproject.com/documentation/generic_views/
Note: See TracBrowser for help on using the browser.