Aaron Williamson

(Who?)

A better DateTime widget for Django

by Aaron Williamson

I’m convinced that every Django developer has struggled with how to present DateTime fields to users.  We all know and love the widgets used in the Django admin, and emboldened by the Django developers,1 every new developer tries to just use those.  But each one discovers quickly that it’s not that simple — you have to link in CSS from the admin site that will screw with your layout, and even after you get it working on your test site, it will not deploy correctly to production.  So let me begin by saying:

Do not use the Django admin’s DateTime widget.

I spent the weekend working out how to do a split DateTime widget properly, and I’m pretty happy with the result, so I’ll share it here.  It uses a text field with a jQuery UI calendar picker for the date, and a simple widget for the time.  Here’s what it looks like:

And here’s the code:

fields.py from time import strptime, strftime from django import forms from django.db import models from django.forms import fields from conflux.widgets import JqSplitDateTimeWidget class JqSplitDateTimeField(fields.MultiValueField): widget = JqSplitDateTimeWidget def __init__(self, *args, **kwargs): """ Have to pass a list of field types to the constructor, else we won't get any data to our compress method. """ all_fields = ( fields.CharField(max_length=10), fields.CharField(max_length=2), fields.CharField(max_length=2), fields.ChoiceField(choices=[('AM','AM'),('PM','PM')]) ) super(JqSplitDateTimeField, self).__init__(all_fields, *args, **kwargs) def compress(self, data_list): """ Takes the values from the MultiWidget and passes them as a list to this function. This function needs to compress the list into a single object to save. """ if data_list: if not (data_list[0] and data_list[1] and data_list[2] and data_list[3]): raise forms.ValidationError("Field is missing data.") input_time = strptime("%s:%s %s"%(data_list[1], data_list[2], data_list[3]), "%I:%M %p") datetime_string = "%s %s" % (data_list[0], strftime('%H:%M', input_time)) print "Datetime: %s"%datetime_string return datetime_string return None widgets.py from django import forms from django.db import models from django.template.loader import render_to_string from django.forms.widgets import Select, MultiWidget, DateInput, TextInput from time import strftime class JqSplitDateTimeWidget(MultiWidget): def __init__(self, attrs=None, date_format=None, time_format=None): date_class = attrs['date_class'] time_class = attrs['time_class'] del attrs['date_class'] del attrs['time_class'] time_attrs = attrs.copy() time_attrs['class'] = time_class date_attrs = attrs.copy() date_attrs['class'] = date_class widgets = (DateInput(attrs=date_attrs, format=date_format), TextInput(attrs=time_attrs), TextInput(attrs=time_attrs), Select(attrs=attrs, choices=[('AM','AM'),('PM','PM')])) super(JqSplitDateTimeWidget, self).__init__(widgets, attrs) def decompress(self, value): if value: d = strftime("%Y-%m-%d", value.timetuple()) hour = strftime("%I", value.timetuple()) minute = strftime("%M", value.timetuple()) meridian = strftime("%p", value.timetuple()) return (d, hour, minute, meridian) else: return (None, None, None, None) def format_output(self, rendered_widgets): """ Given a list of rendered widgets (as strings), it inserts an HTML linebreak between them. Returns a Unicode string representing the HTML for the whole lot. """ return "Date: %s<br/>Time: %s:%s %s" % (rendered_widgets[0], rendered_widgets[1], rendered_widgets[2], rendered_widgets[3]) class Media: css = { } js = ( "js/jqsplitdatetime.js", ) /media/js/jqsplitdatetime.js $(function() { $(".datepicker").datepicker({ dateFormat: 'yy-mm-dd' }); });

To use the field in a form, put something like the following into your form definition:

some_date_field = JqSplitDateTimeField(widget=JqSplitDateTimeWidget(attrs={'date_class':'datepicker','time_class':'timepicker'})

Finally, you need to put the jQuery UI code somewhere. Go get a custom jQuery UI package (I used all of UI Core and Interactions, the Datepicker widget, and Effects Core — your may want more or less depending on where else you’re using JQuery and JQuery UI), put the necessary files (the jQuery UI css and js, the jQuery js) somewhere accessible to your form. I’m using jQuery UI throughout my site, so I’ve got them in my base.html:

<link type="text/css" href="/media/css/ui-conflux/jquery-ui-1.8.custom.css" rel="Stylesheet" /> <script src="/media/js/jquery-1.4.2.min.js" type="text/javascript"></script> <script src="/media/js/jquery-ui-1.8.custom.min.js" type="text/javascript"></script>

But if you only need jQuery UI for this form, it might make sense to put these in the media class of the JqSplitDateTimeWidget along with jqsplitdatetime.js:

class Media: css = { "css/ui-custom/jquery-ui-1.8.custom.css" } js = ( "js/jqsplitdatetime.js", "js/jquery-1.4.2.min.js", "js/jquery-ui-1.8.custom.min.js", )

A couple of caveats: this widget is not currently very customizable/internationalizable. It only deals with 12-hour time and I should probably pass the date format in as an argument. But it does the trick for what I need, and what a lot of U.S. developers will need, and these things are easily added (I’d love a patch!).

  1. “If you like the widgets that the Django Admin application uses, feel free to use them in your own application! They’re all stored in django.contrib.admin.widgets.” – The Django Form Media documentation []

Comments are closed.