Friday 27 February 2009

Feed URL change

Quite a while back the feed URL for this blog changed back to the default Blogger one. I've been using FeedBurner from day one but Blogger initially didn't have the ability to redirect to a different URL for your feed.

I've noticed that few subscribers are still directly using the old FeedBurner URL. When you find the time please update your reader to use the canonical URL:

http://wadofstuff.blogspot.com/feeds/posts/default

I don't expect the old one to go away any time soon but better to be safe than sorry.

Django Full Serializers - Part I

Introduction

The wadofstuff.django.serializers python module extends Django's built-in serializers, adding 3 new capabilities inspired by the Ruby on Rails JSON serializer. These parameters allow the developer more control over how their models are serialized. The additional capabilities are:
  • excludes - a list of fields to be excluded from serialization. The excludes list takes precedence over the fields argument.
  • extras - a list of non-model field properties or callables to be serialized.
  • relations - a list or dictionary of model related fields to be followed and serialized.
De/Serialization Formats

At the moment the module only supports serializing to JSON and Python. It will also only deserialize data that is in the original Django format. i.e. it won't deserialize the results of using the excludes, extras, or relations options.

Source

The source for the serialization module can be obtained here.

Examples

Project Settings

You must add the following to your project's settings.py to be able to use the JSON serializer.
SERIALIZATION_MODULES = {
'json': 'wadofstuff.django.serializers.json'
}
Backwards Compatibility

The Wad of Stuff serializers are 100% compatible with the Django serializers when serializing a model.
>>> from django.contrib.auth.models import Group
>>> from django.core import serializers
>>> print serializers.serialize('json', Group.objects.all(), indent=4)
[
{
"pk": 2,
"model": "auth.group",
"fields": {
"name": "session",
"permissions": [
19
]
}
}
]
Excludes
>>> print serializers.serialize('json', Group.objects.all(), indent=4, excludes=('permissions',))
[
{
"pk": 2,
"model": "auth.group",
"fields": {
"name": "session"
}
}
]
Extras

The extras option allows the developer to serialize properties of a model that are not fields. These properties may be almost any standard python attribute or method. The only limitation being that you may only serialize methods that do not require any arguments.

For demonstration purposes in this example I monkey patch the Group model to have a get_absolute_url() method.
>>> def get_absolute_url(self):
... return u'/group/%s' % self.name
...
>>> Group.get_absolute_url = get_absolute_url
>>> print serializers.serialize('json', Group.objects.all(), indent=4, extras=('__unicode__','get_absolute_url'))
[
{
"pk": 2,
"model": "auth.group",
"extras": {
"get_absolute_url": "/group/session",
"__unicode__": "session"
},
"fields": {
"name": "session",
"permissions": [
19
]
}
}
]
Stay tuned for the second part of this article where I demonstrate the ability to serialize related fields such as ForeignKeys and ManyToManys.

Tuesday 10 February 2009

Django: DRY Custom Model Forms and Fields

With the release of Django 1.0, the ability to add a list of validators to a model field was removed. This was replaced with the clean*() methods in the new forms classes. Unfortunately when you write your own form subclasses you may end up repeating a lot of the parameters in your form field definitions that you had already declared in your model fields such as max_length, required, help_text, etc. So in the spirit of DRY I worked out a way to avoid this.

A few months back I was updating an old Django site to use new the latest 1.0 release and needed to re-implement the functionality of the following, slightly contrived, pre-1.0 model:
from django.db import models
from django.core.validators import MatchesRegularExpression, isNotOnlyDigits

class Acronym(models.Model):
acronym = models.CharField(maxlength=3, unique=True, db_index=True,
validator_list=[isNotOnlyDigits,
MatchesRegularExpression(r'^[A-Z0-9]+$',
error_message='This field may only contain uppercase letters and' \
' numbers.')],
help_text='A three letter acronym.')
definition = models.CharField(maxlength=64)
The new model looked like this after I fixed up all of the parameters:
from django.db import models

class Acronym(models.Model):
acronym = models.CharField(max_length=3, unique=True, db_index=True,
help_text='A three letter acronym.')
definition = models.CharField(max_length=64)
The problem above is that the extra validation is now not being done and needs to be reimplemented either in a new form or form field class.

My first attempt probably looked like this:
import re
from django import forms
from models import Acronym

TLA_RE = re.compile(r'^[A-Z0-9]+$')

class NaiveAcronymForm(forms.ModelForm):
acronym = forms.RegexField(TLA_RE, max_length=3, required=True,
label='Code', help_text='A three letter acronym.')
class Meta:
model = Acronym
The amount of repetition is already becoming obvious and the code above only provides half the solution. We also need to implement the isNotOnlyDigits validation.
import re
from django import forms
from models import Acronym

TLA_RE = re.compile(r'^[A-Z0-9]+$')

class NaiveAcronymForm(forms.ModelForm):
acronym = forms.RegexField(TLA_RE, max_length=3, required=True,
label='Code', help_text='A three letter acronym.')

def clean_acronym(self):
if self.cleaned_data['acronym'].isdigit():
raise forms.ValidationError(u"This value can't be comprised soley of digits."

class Meta:
model = Acronym
I got to this point in my own code and thought that there must be a way to leverage all the work already being done by Django to generate a form field from a model field. When I make a change to my model fields I want those changes to automatically flow on to my forms.

The solution I came up with was this:
import re
from django import forms
from models import Acronym

TLA_RE = re.compile(r'^[A-Z0-9]+$')

class AcronymField(forms.RegexField):
"""Form field for three letter acronym."""
default_error_messages = {
'invalid': u'This field may only contain uppercase letters and ' \
'numbers.',
'notonlydigits': u'''This value can't be comprised solely of digits.'''
}

def __init__(self, *args, **kwargs):
"""Initialize the field with the acronym regex."""
super(AcronymField, self).__init__(TLA_RE, min_length=3, *args,
**kwargs)

def clean(self, value):
"""Ensure acronym matches regex and is not only digits."""
value = super(AcronymField, self).clean(value)
if value.isdigit():
raise forms.ValidationError(self.error_messages['notonlydigits'])
return value

class AcronymModelForm(forms.ModelForm):
"""Form for Acronym model."""
def __init__(self, *args, **kwargs):
"""Programmatically declare fields."""
super(AcronymModelForm, self).__init__(*args, **kwargs)
field = self.Meta.model._meta.get_field('acronym')
self.fields['acronym'] = field.formfield(form_class=AcronymField)

class Meta:
model = Acronym
How does this work? First of all, AcronymField is subclassed from forms.RegexField and the __init__() method overriden. It then calls the parent class's __init__() with my extra parameters, the regexp and min_length, as well as passing the original positional and keyword arguments. This ensures that any extra parameters from the model such as required, label, max_length, and help_text are preserved. I also override the clean() method and again call the superclass's clean() method before checking that the value is not only digits.

Next I subclass AcronymForm from forms.ModelForm and override the __init__() method again. Here the superclass's __init__() method is called then the acronym form field is replaced with one that uses the new AcronymField class. It is within the call to field.formfield(...) (see django.db.models.fields.Field class for the gory details) that all the values for required, label, and help_text are taken from the model field and the form field instance created.

While this results in more lines of code for a model form definition it ultimately results in less presentation and validation errors due to model fields and form fields getting out of sync. This approach could probably be generalized even further to a subclass of ModelForm but I've only had to use it for a handful of models so I leave that as an exercise for the reader.

Update: fixed formatting mistake so you can more clearly see the before and after models.