Custom fields and widgets for Django forms
Nov 19, 2008

Every so often you run into a situation where Django's built-in form fields and widgets just don't meet your needs. I ran into this the other day when creating a credit card processing form. I wanted an easy way for the user to enter the expiration month and date of their card, but the tools provided by django only gave me a single text field and the option to use a javascript date picker. Neither of those was quite what I wanted, I just wanted two selects that would allow the user to pick the month and the year, as on pretty much every credit card form you've ever filled out online. The obvious option would be to make two separate fields on your model, one for month, and one for year. But I don't really like that option. I would much rather have both selects show up on the same line, which is not the behavior you'd get with two separate fields. So I decided to write a custom widget and field to accomplish this. It was actually surprisingly easy to do. Probably the most difficult part was coming up with a decent way to populate the selects with worthwhile options. I'm not entirely pleased with the route I took there, but it's easy enough to change later. Here's my code: from django import forms import datetime class MonthYearWidget(forms.MultiWidget): """ A widget that splits a date into Month/Year with selects. """ def __init__(self, attrs=None): months = ( ('01', 'Jan (01)'), ('02', 'Feb (02)'), ('03', 'Mar (03)'), ('04', 'Apr (04)'), ('05', 'May (05)'), ('06', 'Jun (06)'), ('07', 'Jul (07)'), ('08', 'Aug (08)'), ('09', 'Sep (09)'), ('10', 'Oct (10)'), ('11', 'Nov (11)'), ('12', 'Dec (12)'), ) year = int(datetime.date.today().year) year_digits = range(year, year+10) years = [(year, year) for year in year_digits] widgets = (forms.Select(attrs=attrs, choices=months), forms.Select(attrs=attrs, choices=years)) super(MonthYearWidget, self).__init__(widgets, attrs) def decompress(self, value): if value: return [value.month, value.year] return [None, None] def render(self, name, value, attrs=None): try: value = datetime.date(month=int(value[0]), year=int(value[1]), day=1) except: value = '' return super(MonthYearWidget, self).render(name, value, attrs) class MonthYearField(forms.MultiValueField): def compress(self, data_list): if data_list: return datetime.date(year=int(data_list[1]), month=int(data_list[0]), day=1) return datetime.date.today() Simple, no? The results end up looking like so:
blog comments powered by Disqus