I'm sorry to say that I missed the thread "Proposal: Forms and
BoundForms" last month.  I'm very interested in the topic, largely
because, having run out of patience with manipulators, I had written
code to handle form definition, validation, and rendering that is
similar to the proposals described in that thread.  In developing this
library, I was focusing mainly on providing useful integration between
forms and models (including "nested forms"), and customisability of
forms.  I'll describe this here, focusing mostly on the needs or
decisions which haven't been discussed in that thread already.  I hope
that some of this will be of use for the manipulator replacement work
that is in progress.

I was motivated in creating this to fulfill these needs that I had that
manipulators weren't fully suitable for:

A. To allow easy declarative form creation, as well as customisation of
the default form for a model.

B. To unify adding/changing model instances.  Adding and changing are
processed identically, with only the form defaults being different.

C. To allow a form to be easily generated and handled for a model
hierarchy -- i.e. a single model instance, and other instances related
via ManyToManyFields and ForeignKeys in either direction; and
supporting model validation.

D. To make it easy to customise the rendering of a form or form field.

E. To allow forms with validation to be used without being tied to a
model class/instance -- i.e. to work with dicts and lists.


For (A), I implemented the unbound/bound distinction with the natural
class/instance distinction:  a Form subclass defines the form structure
and validation, and an instance of the class is that form, "bound" with
data, and errors (if it has been validated).  Similarly, a Widget
subclass (a form field) defines the widget's attributes (name, size,
choices list), and a Widget instance has data and errors.  All
customisation of forms and widgets is done through subclassing, with an
alter() method providing a convenient helper for small changes.

For example, assume a UserProfile model that has 'username',
'fullname', and 'email' TextFields, and a 'group' ForeignKey.  The
automatically generated default form for this model would have a
TextWidget for the first three fields, and a SelectField for the
group_id, with a list of the available groups.

    # Never let the username be edited, and use a custom widget for the
    # email field
    class EditProfileForm(Profile.default_form):
        username = None
        email = EmailWidget.alter(maxlength=20)

    # Only let admin users edit the group
    if not request.user.is_admin():
        EditProfileForm = EditProfileForm.alter(group_id=None)

For (B): handling the request involves creating a form instance with
default data, updating that with POST data (if applicable), processing
changes, and rendering the form if unsubmitted or errors.

    # If we were adding a profile here, we'd just use Profile()
    profile = Profile.objects.get(id=whatever)
    form = EditProfileForm(profile)
    if request.POST:
        form.update_data(convert_post_data(request.POST))
        form.process_input_data()
        if not form.has_errors():
            return HTTPResponseRedirect("/profile/")
    return render_to_response("profile.html", {'form': form})

Since the base Form class is ignorant of models (E), and only uses a
dict to store its data, there is a ModelForm subclass that extracts
data from a model instance (and related instances) to fill in the
widgets declared in the form.  A default_form descriptor is added to
the models.Model class to generate a form automatically -- but for
convenience, if a default_form class is declared in the model, it will
be used instead (there were some technical hoops to jump through with
this, but I'll gloss over that).

(C), integration with models, is the trickiest part. as implemented,
there are four distinct types of widget, each corresponding to a
different data structure or relation type:

A simple widget corresponds to a simple data type (int, string) or, for
a model instance, a TextField, IntegerField, etc. -- or the
fieldname_id attribute for a ForeignKey relation.

A widget with its has_form attribute set corresponds to a dict, or the
fieldname attribute for a ForeignKey relation to another model; the
SubForm Widget subclass handles this with another Form class:

    class EditProfileForm(Profile.default_form):
        home_address = SubForm.alter(form=Address.default_form)
    # Now we can render form.address.street in a template, and so on

A widget with its has_list attribute set corresponds to a list, or a
ManyToMany or reverse ForeignKey relation to another model, when
selecting from the existing instances, and not editing them inline.  A
multi-select list widget handles these.

Finally, a widget with both has_list and has_form corresponds to a list
of dicts, or a ManyToMany or reverse ForeignKey relation, edited
inline.  A SubFormList widget is used for this.

As implemented, these could be nested as needed.  In a particular case
which was part of my motivation for this, I had something like the
following for editing a product, its variants, and their attributes
(which distinguish them from other variants) all nested in one form:

    class ProductVariantAdminForm(ProductVariant.default_form)
        attributes = SubFormList(form=Attribute.default_form)

    class ProductAdminForm(Product.default_form):
        product_variants = SubFormList(form=ProductVariantAdminForm)

This provided two levels of nesting automatically.

Another critical issue with model integration was validation.
Validation must be handled both at the form level (before converting
from bare strings to Python objects) and at the model level.  If a
field fails form validation, it should not try and validate it at the
model level; and any errors in model validation should be saved into
the form errors, so they can be displayed alongside the widgets they
refer to.

I didn't come up with a solution to this that was satisfactory;  I had
a two-step process whereby, if all form validation passed, then model
validation was done; and if that also all passed, then the model was
saved.  This was hacky, and resulted in a poor UI experience.  A better
solution would do as much validation as possible in one step, so as to
show the errors to the user together; this might be obtainable by
implementing something like the FormEncode Schema, and combining the
two levels by appropriately chaining the form's schema and the model's
schema.

Finally, (D) -- each Widget has a template attribute which was the
default template for that widget (using XHTML) -- this was the bare
widget only (e.g. "<input type='text'...>".  Subclasses that only need
to change the rendering could override this.  For the common case of
displaying a widget with a label and any errors, a template tag {%
wrap_widget form.somewidget %} did the job neatly, with a similar tag
providing a shortcut for rendering an entire form.

My original plan was to propose this whole package as a candidate for
inclusion in Django as a replacement for manipulators; however, due to
other demands, I never got around to tidying it up or writing it up to
my satisfaction for this.

Andrew


--~--~---------~--~----~------------~-------~--~----~
You received this message because you are subscribed to the Google Groups 
"Django developers" group.
To post to this group, send email to django-developers@googlegroups.com
To unsubscribe from this group, send email to [EMAIL PROTECTED]
For more options, visit this group at 
http://groups.google.com/group/django-developers
-~----------~----~----~----~------~----~------~--~---

Reply via email to