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.