Comments:"Overloading Django Form Fields"
URL:http://pydanny.com/overloading-form-fields.html
Wednesday, March 27, 2013 (permalink)
One of the patterns we get positive feedback for mentioning in our book is overloading form fields.
The problem this pattern handles is the use case of when we have a model with a field(s) that allows for blank values, how do we force users to enter values?
For example, assuming the following model:
# myapp/models.pyfromdjango.dbimportmodelsclassMyModel(models.Model):name=models.CharField(max_length=50,blank=True)age=models.IntegerField(blank=True,null=True)profession=models.CharField(max_length=100,blank=True)bio=models.TextField(blank=True)
How do we make all those fields (name, age, profession, bio) required without modifying the database?
This is the way I used to do it:
# myapp/forms.pyfromdjangoimportformsfrom.modelsimportMyModelclassMyModelForm(forms.ModelForm):name=forms.CharField(max_length=100,required=True)age=forms.IntegerField(required=True)profession=forms.CharField(required=True)bio=forms.TextField(required=True)classMeta:model=MyModel
See the problems with this approach?
MyModelForm is nearly a copy of MyModel, and was in fact created by copy/pasting model and then modifying it. In software engineering parlance, it violates the principal of Don't Repeat Yourself (DRY) and is fertile ground for introducing bugs.
MyModelForm has a bug!
Can you spot the bug?
The code example below illuminates where I purposefully/gleefully placed an error:
classMyModel(models.Model):# 50 character database fieldname=models.CharField(max_length=50,blank=True)classMyModelForm(forms.ModelForm):# Most people don't write tests to check for field length.# 100 character form field - probably not spotted until deployed.# Easy error to make when violating DRY since the model can change# and leave the form definition behind.name=forms.CharField(max_length=100,required=True)
Bugs like this happen either because developers are human and make mistakes, or because the model evolves over time and the forms are left behind. This is a serious maintenance issue, and one that will bite you or the developers who end up maintaining code you've written.
Can you spot the second bug? ;-)
How do we fix this?
A Better Way
In instantiated Django forms, fields are kept in a dict-like object. Which means, instead of writing forms in a way that duplicates the model, a better way is to explicitly modify only what we want to modify:
fromdjangoimportformsfrom.modelsimportMyModelclassMyModelForm(forms.ModelForm):def__init__(self,*args,**kwargs):super(MyModelForm,self).__init__(*args,**kwargs)# Making name requiredself.fields['name'].required=Trueself.fields['age'].required=Trueself.fields['bio'].required=Trueself.fields['profession'].required=TrueclassMeta:model=MyModel
Other field attributes
This isn't just limited to the required attribute. It can also be applied to help_text, label, choices, widgets, or any other form field attribute:
fromdjangoimportformsfrom.modelsimportMyModelclassMyModelForm(forms.ModelForm):def__init__(self,*args,**kwargs):super(MyModelForm,self).__init__(*args,**kwargs)# snip the other fields for the sake of brevity# Adding content to the formself.fields['profession'].help_text="Job title here"classMeta:model=MyModel
Try it with Inheritance!
We can even do this with inheritance:
fromdjangoimportformsclassBaseEmailForm(forms.Form):email=forms.EmailField("Email")email2=forms.EmailField("Email 2")defclean(self,*args,**kwargs):email=self.cleaned_data['email']email2=self.cleaned_data['email2']ifemail!=email2:raiseforms.ValidationError("Emails don't match")returnself.cleaned_dataclassContactForm(BaseEmailForm):message=forms.CharField()def__init__(self,*args,**kwargs):super(ContactForm,self).__init__(*args,**kwargs):self.fields['email2'].label="Confirm your email"self.fields['email2'].help_text="We want to be sure!"
Summary
From the perspective of general software development, it's always a good thing to avoid repeating yourself. This might seem like as much or in some cases even more typing, but it's a lot better than making an embarrassing/costly mistake.
From the perspective of a Python developer our approach more closely matches the Zen of Python. This is because we only modify the field properties that need to be modified, the approach specified is more explicit.
Today's reading is Matt Harrison's Guide to Learning Iteration and Generators in Python