Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Added formfields/manipulators docs; added a few notes to the FAQ

git-svn-id: http://code.djangoproject.com/svn/django/trunk@303 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit 8bd30b01e401e7ed8ef87bf42bec2614e8d96570 1 parent 768c17e
@jacobian jacobian authored
Showing with 490 additions and 0 deletions.
  1. +34 −0 docs/faq.txt
  2. +456 −0 docs/forms.txt
View
34 docs/faq.txt
@@ -101,10 +101,22 @@ any capital-M Methodologies; we do what "feels" right. If you squint the right
way, you can call Django's ORM the "Model", the view functions the "View", and
the dynamically-generated API the "Controller" -- but not really.
+In fact, you might say that Django is a "MTV" framework -- that is, Model,
+Template, and View make much more sense to us.
+
So, although we've been strongly influenced by MVC -- especially in the
separation-of-data-from-logic department -- we've also strayed from the path
where it makes sense.
+<Framework X> does <feature Y> -- why doesn't Django?
+-----------------------------------------------------
+
+We're well aware that there are other awesome web frameworks out there, and
+we're not adverse to borrowing ideas where appropriate. However, Django was
+developed precisely because we were unhappy with the status quo, so please be
+aware that "because <Framework X>" does it is not going to be sufficient reason
+to add a given feature to Django.
+
Do you have any of those nifty "screencast" things?
---------------------------------------------------
@@ -224,6 +236,28 @@ but we recognize that choosing a template language runs close to religion.
There's nothing about Django that requires using the template language, so
if you're attached to ZPT, Cheetah, or whatever, feel free to use those.
+How do I use image and file fields?
+-----------------------------------
+
+Using a ``FileField`` or an ``ImageField`` in a model takes a few steps:
+
+ #. In your settings file, define ``MEDIA_ROOT`` as the full path to
+ a directory where you'd like Django to store uploaded files (for
+ performance these files are not stored in the database). Define
+ ``MEDIA_URL`` as the base public URL of that directory. Make
+ sure that this directory is writable by the web user.
+
+ #. Add the ``FileField`` or ``ImageField`` to your model, making sure
+ to define the ``upload_to`` option to tell Django what subdirectory
+ of ``MEDIA_ROOT`` to upload files to.
+
+ #. All that will be stored in your database is a path to the file
+ (relative to ``MEDIA_ROOT``). You'll must likely want to use
+ the convenience ``get_<fieldname>_url`` function provided by
+ Django (that is, if your ``ImageField`` is called ``mug_shot``,
+ you can get the absolute URL to your image in a template with
+ ``{{ object.get_mug_shot_url }}``.
+
The database API
================
View
456 docs/forms.txt
@@ -0,0 +1,456 @@
+===============================
+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/

0 comments on commit 8bd30b0

Please sign in to comment.
Something went wrong with that request. Please try again.