Django Model Forms made easier...

Django model forms are already easy.  How could they be easier?

@decorators.with_X( request, model_class, key, url_field, parameter, required=True )
def my_view( parameter ):

We also use decorators that do x.y lookup on another parameter (i.e. lookup a particular object in the fields of another object based on ids (root_object.field.get( id=...))).  Sure, it winds up hitting the DB twice (once for the root object, once for the lookup), optimize that later, for now know that it is restricting your user to their per-row authenticated records.  We normally also do the row-level auth in the decorator so that we have a standard decorator which applies "you must have Y permission on X object" where X is the row-level auth core object.

Use a pattern in the views that works well.  Model Forms tend to be a bit of a PITA with partial validation and other nasty side effects (esp. with row-level validation patterns and initial parameters).  The pattern that seems to work correctly in our experience is:

@annoying.render_to( 'my/template.html' )
def my_view( request, parameter=None ):
if parameter is None:
parameter = parameter_class( **defaults )
if request.method == 'GET':
form = forms.ParameterModel( instance=parameter )
else:
form = forms.ParameterModel( request.POST, instance=parameter )
if form.is_valid():
form.save()
return HttpResponseRedirect( request.path )
return {
'form': form,
'parameter': parameter,
}

That is, you create your object(s) with defaults (if they were not specified) and pass them to the model forms as pre-populated (unsaved) instances (i.e. don't use initial).  If you are managing multiple forms, create the objects all together, then do your GET/POST form construct, check all the forms, and save if all pass validation and your cross-form validation has also passed.

BTW. Use Django-annoying's render_to decorator.  That really should be core.  I used to write it myself in every bloody project until they day I decided "another dependency doesn't matter".

You will *still* get subtle errors with the above model-form pattern unless you suppress some silly code in Django's model forms that is intended to allow backward compatible operation, but causes some constraints not to validate (and thus throw integrity errors at the database layer):

class AlwaysValidate( forms.ModelForm ):
    def validate_unique(self):
        try:
            self.instance.validate_unique()
        except forms.ValidationError, e:
            self._update_errors(e.message_dict)

Using that as your base class for your forms means that your models' referential integrity checks will raise validation errors (i.e. form errors) instead of runtime errors if you violate 2-field unique constraints where one of the fields is defined by e.g. your row-level-auth constraint (which is never part of your forms).

With the pattern above and the form class above, you are validating (as much as is normally needed) that the model will be okay with the parameters in the form and its current state, rather than validating solely that the forms patterns are okay.  (The fields are normally excluded from checking in order to allow you to populate the extra fields after the form is saved/checked.  Note: this implies that you must populate the objects fully before you validate the fields.  If you are doing multi-form creation, that may mean you need to modify one object's reference to another to point to a newly-saved object before you do an is_valid() on the second object's form.

Anyway, hope that's helpful for someone.

Comments

  1. teserak

    teserak on 09/27/2011 11:14 a.m. #

    Try this:

    @annoying.render_to( 'my/template.html' )
    def my_view(request, parameter=None):
    form = forms.ParameterModel(request.POST or None, instance=parameter_class(**defaults))
    if form.is_valid():
    form.save()
    return HttpResponseRedirect(request.path)
    return {
    'form': form,
    'parameter': parameter,
    }

  2. Mike Fletcher

    Mike Fletcher on 09/27/2011 10:11 p.m. #

    That would throw away parameter, though "parameter or parameter_class(**defaults)" would get around that. Thanks, nice little short-circuit.

Comments are closed.

Pingbacks

Pingbacks are closed.