Skip to content

Instantly share code, notes, and snippets.

@mattions
Created January 10, 2013 13:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattions/4501948 to your computer and use it in GitHub Desktop.
Save mattions/4501948 to your computer and use it in GitHub Desktop.
Widgets to deal with time and dates, created putting together different solutions. Just storing them here if I ever need them.
import re
import django.forms
from django.forms.widgets import Widget, Select
from django.utils.safestring import mark_safe
from django.forms.widgets import MultiWidget
from django.forms.extras.widgets import SelectDateWidget
__all__ = ('SelectTimeWidget', 'SplitSelectDateTimeWidget')
#CHOICES = (("slot1", "10:00-10:30"), ("slot2", "10:30-11:30"))
#class SelectTimeSlotRadio(forms.RadioSelect, choices=CHOICES):
# def render(self, name, value, attrs=None):
#
# Attempt to match many time formats:
# Example: "12:34:56 P.M." matches:
# ('12', '34', ':56', '56', 'P.M.', 'P', '.', 'M', '.')
# ('12', '34', ':56', '56', 'P.M.')
# Note that the colon ":" before seconds is optional, but only if seconds are omitted
time_pattern = r'(\d\d?):(\d\d)(:(\d\d))? *([aApP]\.?[mM]\.?)?$'
RE_TIME = re.compile(time_pattern)
# The following are just more readable ways to access re.matched groups:
HOURS = 0
MINUTES = 1
SECONDS = 3
MERIDIEM = 4
class SelectTimeWidget(Widget):
"""
A Widget that splits time input into <select> elements.
Allows form to show as 24hr: <hour>:<minute>:<second>, (default)
or as 12hr: <hour>:<minute>:<second> <am|pm>
Also allows user-defined increments for minutes/seconds
"""
hour_field = '%s_hour'
minute_field = '%s_minute'
second_field = '%s_second'
meridiem_field = '%s_meridiem'
twelve_hr = False # Default to 24hr.
use_seconds = True
def __init__(self, attrs=None,
hour_step=1, minute_step=1, second_step=1,
min_hour=0, max_hour=23,
twelve_hr=False, use_seconds=True):
"""
hour_step, minute_step, second_step are optional step values for
for the range of values for the associated select element
min_hour and max_hour are the minimum and maximum values that will be
allowed in the hour field. Currently these should only be used with a 24-hr
clock, and will be ignored if twelve_hr is True.
twelve_hr: If True, forces the output to be in 12-hr format (rather than 24-hr)
use_seconds: If False, doesn't show seconds select element and stores seconds = 0.
"""
self.attrs = attrs or {}
self.use_seconds = use_seconds
self.twelve_hr = twelve_hr
if twelve_hr:
self.meridiem_val = 'a.m.' # Default to Morning (A.M.)
self.hours = range(1, 13, hour_step)
else: # 24hr, with stepping.
self.hours = range(min_hour, max_hour + 1, hour_step)
self.minutes = range(0, 60, minute_step)
if use_seconds:
self.seconds = range(0, 60, second_step)
def render(self, name, value, attrs=None):
try: # try to get time values from a datetime.time object (value)
hour_val, minute_val, second_val = value.hour, value.minute, value.second
if self.twelve_hr:
if hour_val >= 12:
self.meridiem_val = 'p.m.'
else:
self.meridiem_val = 'a.m.'
except AttributeError:
hour_val = minute_val = second_val = 0
if isinstance(value, basestring):
match = RE_TIME.match(value)
if match:
time_groups = match.groups();
hour_val = int(time_groups[HOURS]) % 24 # force to range(0-24)
minute_val = int(time_groups[MINUTES])
if time_groups[SECONDS] is None:
second_val = 0
else:
second_val = int(time_groups[SECONDS])
# check to see if meridiem was passed in
if time_groups[MERIDIEM] is not None:
self.meridiem_val = time_groups[MERIDIEM]
else: # otherwise, set the meridiem based on the time
if self.twelve_hr:
if hour_val >= 12:
self.meridiem_val = 'p.m.'
else:
self.meridiem_val = 'a.m.'
else:
self.meridiem_val = None
# If we're doing a 12-hr clock, there will be a meridiem value, so make sure the
# hours get printed correctly
if self.twelve_hr and self.meridiem_val:
if self.meridiem_val.lower().startswith('p') and hour_val > 12 and hour_val < 24:
hour_val = hour_val % 12
elif self.twelve_hr and hour_val == 0:
hour_val = 12
output = []
if 'id' in self.attrs:
id_ = self.attrs['id']
else:
id_ = 'id_%s' % name
# For times to get displayed correctly, the values MUST be converted to unicode
# When Select builds a list of options, it checks against Unicode values
hour_val = u"%.2d" % hour_val
minute_val = u"%.2d" % minute_val
second_val = u"%.2d" % second_val
hour_choices = [("%.2d"%i, "%.2d"%i) for i in self.hours]
local_attrs = self.build_attrs(id=self.hour_field % id_)
select_html = Select(choices=hour_choices).render(self.hour_field % name, hour_val, local_attrs)
output.append(select_html)
minute_choices = [("%.2d"%i, "%.2d"%i) for i in self.minutes]
local_attrs['id'] = self.minute_field % id_
select_html = Select(choices=minute_choices).render(self.minute_field % name, minute_val, local_attrs)
output.append(select_html)
if self.use_seconds:
second_choices = [("%.2d"%i, "%.2d"%i) for i in self.seconds]
local_attrs['id'] = self.second_field % id_
select_html = Select(choices=second_choices).render(self.second_field % name, second_val, local_attrs)
output.append(select_html)
if self.twelve_hr:
# If we were given an initial value, make sure the correct meridiem gets selected.
if self.meridiem_val is not None and self.meridiem_val.startswith('p'):
meridiem_choices = [('p.m.','p.m.'), ('a.m.','a.m.')]
else:
meridiem_choices = [('a.m.','a.m.'), ('p.m.','p.m.')]
local_attrs['id'] = local_attrs['id'] = self.meridiem_field % id_
select_html = Select(choices=meridiem_choices).render(self.meridiem_field % name, self.meridiem_val, local_attrs)
output.append(select_html)
return mark_safe(u'\n'.join(output))
def id_for_label(self, id_):
return '%s_hour' % id_
id_for_label = classmethod(id_for_label)
def value_from_datadict(self, data, files, name):
# if there's not h:m:s data, assume zero:
h = data.get(self.hour_field % name, 0) # hour
m = data.get(self.minute_field % name, '00') # minute
s = data.get(self.second_field % name, '00') # second
meridiem = data.get(self.meridiem_field % name, None)
#NOTE: if meridiem is None, assume 24-hr
if meridiem is not None:
if meridiem.lower().startswith('p') and int(h) != 12:
h = (int(h)+12)%24
elif meridiem.lower().startswith('a') and int(h) == 12:
h = 0
if (int(h) == 0 or h) and m and s:
return '%s:%s:%s' % (h, m, s)
return data.get(name, None)
class SplitSelectDateTimeWidget(MultiWidget):
"""
MultiWidget = A widget that is composed of multiple widgets.
This class combines SelectTimeWidget and SelectDateWidget so we have something
like SpliteDateTimeWidget (in django.forms.widgets), but with Select elements.
"""
def __init__(self, attrs=None, hour_step=1, minute_step=1, second_step=1,
min_hour=0, max_hour=23, twelve_hr=False, use_seconds=False,
years=None):
""" pass all these parameters to their respective widget constructors..."""
widgets = (SelectDateWidget(attrs=attrs, years=years),
SelectTimeWidget(attrs=attrs, hour_step=hour_step, minute_step=minute_step, second_step=second_step,
min_hour=min_hour, max_hour=max_hour, twelve_hr=twelve_hr))
super(SplitSelectDateTimeWidget, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
return [value.date(), value.time().replace(microsecond=0)]
return [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.
"""
rendered_widgets.insert(-1, '<br/>')
return u''.join(rendered_widgets)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment