Django Form Inheritance

Data exploration is a major part of the systems I build.
And I found that the best approach is to provide search tools, all over the place, for all data: equipment deployed, personnel, activities, quantities, consumables, phone numbers… everything. There is no such a thing as too much information when you are in Project Control.

The consequence is that the number of forms used in the system is staggering.
However, careful analysis will show that some features are shared among them:

  • A text search field
  • A paging choice field (to select the number of items per page)
  • A date range field

Since I hate to write similar code twice, I decided to use inheritance to accomplish most of the work, using three base classes: SearchFromBasePagedFormBase and DateRangeFormBase.
Note: I always end the name in -Base when I intend to use the class as parent only. Just being explicit, nothing major.

So first we get on with the text filter:

    class SearchFormBase(forms.Form):

        def __init__(self, *args, **kwargs):
            super(SearchFormBase, self).__init__(*args, **kwargs)
            self.fields['q'] = forms.CharField(max_length=30, required=False, label='')

Very simple: we just add an optional char field on the initialization of the form object.
Why call the field q? Well, most of use are used to see it in the query string, so… Plus, I can avoid changing it for every different search.

Now get define the paging part, used by most filters to decide how many items per page are displayed.
Why? Because it is just rude to assume that everyone will be happy with 20 items per page. Some people do like choices. Most don’t, I know.

    class PagedFormBase(forms.Form):

        def __init__(self, *args, **kwargs):
            clipp = kwargs.get('clipp')
            if clipp is None:
                clipp = consts.CHOICES_LIST_IPP
            else:
                del kwargs['clipp']
            super(PagedFormBase, self).__init__(*args, **kwargs)
            self.fields['i'] = forms.ChoiceField(widget=widgets.RadioSelect, choices=clipp,\
                                                 initial=clipp[1][0], label='Items/Page')
            self.fields['i'].widget.attrs['class'] = 'ipp'

This one is a bit more complex, as it can take an iterable of tuples as argument to use as the choices list.
Why? Well, the data being displayed can be quite diverse in form, so giving always the same options is not a good idea.
Also, one of them is very complex and if someone tries to display 100 items per page it can get really slow. Performance counts.
Note: The argument is optional, and if none is provided we default to a constant (which is actually most cases.) And do remember to remove arguments so that the base class (forms in this case) doesn’t get confused.

And now the difficult one: the date range.

    class DateRangeFormBase(forms.Form):

        def __init__(self, *args, **kwargs):
            date_range = kwargs.get('date_range')
            if date_range is None:
                date_range = consts.LIST_DATE_RANGE
            else:
                del kwargs['date_range']
            super(DateRangeFormBase, self).__init__(*args, **kwargs)
            self.fields['fd'] = forms.DateField(required=False, label='From date',\
                                                initial=get_initial_date(1+date_range),\
                                                widget=SelectDateWidget(years=get_years_list(2010)))
            self.fields['td'] = forms.DateField(required=False, label='To date',\
                                                initial=get_initial_date(1),\
                                                widget=SelectDateWidget(years=get_years_list(2010)))

        def clean(self):
            cleaned_data = self.cleaned_data
            if 'fd' in cleaned_data and 'td' in cleaned_data:
                fd = cleaned_data['fd']
                td = cleaned_data['td']
                if td < fd:
                    self._errors['td'] = self.error_class(('Invalid date: final date cannot be less than start',))
                    del cleaned_data['td']
            return cleaned_data

Actually, not that difficult: pretty much the same as the previous one, plus a clean method.

So what now?
Well, those forms are pretty useless as they are, because they were created to be combined.
Here we’ll define the most basic form used by the searches: the PagedSearchForm. This form is so basic that it will also be inherited by other form classes, but due to the fact that it will be used directly as well the name does not end in -Base. Well, I am consistent.

    class PagedSearchForm(SearchFormBase, PagedFormBase):

        def __init__(self, *args, **kwargs):
            super(PagedSearchForm, self).__init__(*args, **kwargs)

And that’s how you define a form class that includes a text search field and a paging radio button field. Was that easy or what?
Well, that’s another reason you can like Python: multiple inheritance. Not so useless.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s