Created
January 10, 2013 13:10
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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