Hi, I'm Daniel Greenfeld, and welcome to my blog. I write about Python, Django, and much more.

Overloading Django Form Fields

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.py
from django.db import models

class MyModel(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.py
from django import forms

from .models import MyModel

class MyModelForm(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)

    class Meta:
        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:

class MyModel(models.Model):

    # 50 character database field
    name = models.CharField(max_length=50, blank=True)

class MyModelForm(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:

from django import forms

from .models import MyModel

class MyModelForm(forms.ModelForm):

    def __init__(self, *args, **kwargs):
        super(MyModelForm, self).__init__(*args, **kwargs)
        # Making name required
        self.fields['name'].required = True
        self.fields['age'].required = True
        self.fields['bio'].required = True
        self.fields['profession'].required = True

    class Meta:
        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:

from django import forms

from .models import MyModel

class MyModelForm(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 form
        self.fields['profession'].help_text = "Job title here"

    class Meta:
        model = MyModel

Try it with Inheritance!

We can even do this with inheritance:

from django import forms

class BaseEmailForm(forms.Form):
    email = forms.EmailField("Email")
    email2 = forms.EmailField("Email 2")

    def clean(self, *args, **kwargs):
        email = self.cleaned_data['email']
        email2 = self.cleaned_data['email2']
        if email != email2:
            raise forms.ValidationError("Emails don't match")
        return self.cleaned_data

class ContactForm(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


Comments