From 8bd30b01e401e7ed8ef87bf42bec2614e8d96570 Mon Sep 17 00:00:00 2001 From: Jacob Kaplan-Moss Date: Sun, 24 Jul 2005 19:02:22 +0000 Subject: [PATCH] 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 --- docs/faq.txt | 34 ++++ docs/forms.txt | 456 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 490 insertions(+) create mode 100644 docs/forms.txt diff --git a/docs/faq.txt b/docs/faq.txt index 097f22ed4f..717edfa779 100644 --- a/docs/faq.txt +++ b/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. + does -- 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 " 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__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 ================ diff --git a/docs/forms.txt b/docs/forms.txt new file mode 100644 index 0000000000..af3f18cbb8 --- /dev/null +++ b/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 + + >>> places.ChangeManipulator + + +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 %} +

Create a place:

+ +
+

{{ form.name }}

+

{{ form.address }}

+

{{ form.city }}

+

{{ form.state }}

+

{{ form.zip_code }}

+

{{ form.place_type }}

+ +
+ {% 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 %} + +

Please go back and correct the following error{{ errors|pluralize }}:

+
    + {% for e in errors.items %} +
  • Field "{{ e.0 }}": {{ e.1|join:", " }}
  • + {% endfor %} +
+ + {% 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 %} +

Create a place:

+ + {% if form.has_errors %} +

Please correct the following error{{ errors|pluralize }}:

+ {% endif %} + +
+

+ {{ form.name }} + {% if form.name.errors %}*** {{ form.name.errors|join:", " }}{% endif %} +

+

+ {{ form.address }} + {% if form.address.errors %}*** {{ form.address.errors|join:", " }}{% endif %} +

+

+ {{ form.city }} + {% if form.city.errors %}*** {{ form.city.errors|join:", " }}{% endif %} +

+

+ {{ form.state }} + {% if form.state.errors %}*** {{ form.state.errors|join:", " }}{% endif %} +

+

+ {{ form.zip_code }} + {% if form.zip_code.errors %}*** {{ form.zip_code.errors|join:", " }}{% endif %} +

+

+ {{ form.place_type }} + {% if form.place_type.errors %}*** {{ form.place_type.errors|join:", " }}{% endif %} +

+ +
+ {% 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 ``
    `` 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/