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)
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)
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
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
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
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.
2 comments:
Nice article. Running the latest Django trunk I found out you need to use max_length instead of maxlength when specifying field lengths. Just FYI.
Thanks. I had the correct model there already but a formatting mistake in the HTML was making it hard to see.
Post a Comment