- Author:
- bradmontgomery
- Posted:
- November 18, 2008
- Language:
- Python
- Version:
- 1.0
- Score:
- 4 (after 4 ratings)
This snippet defines a Widget that is very similar to the SelectDateWidget located in django.forms.extras.widgets. The main difference however is that it works with Times instead of Dates.
The SelectTimeWidget supports both 24-hr and 12-hr formats, and flexible time increments for hours, minutes and seconds. Sample usage is illustrated below:
# Specify a basic 24-hr time Widget (the default)
t = forms.TimeField(widget=SelectTimeWidget())
# Force minutes and seconds to be displayed in increments of 10
t = forms.TimeField(widget=SelectTimeWidget(minute_step=10, second_step=10))
# Use a 12-hr time format, which will display a 4th select
# element containing a.m. and p.m. options)
t = forms.TimeField(widget=SelectTimeWidget(twelve_hr=True))
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | import re
from django.forms.extras.widgets import SelectDateWidget
from django.forms.widgets import Widget, Select, MultiWidget
from django.utils.safestring import mark_safe
__all__ = ('SelectTimeWidget', 'SplitSelectDateTimeWidget')
# 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.
def __init__(self, attrs=None, hour_step=None, minute_step=None, second_step=None, twelve_hr=False):
"""
hour_step, minute_step, second_step are optional step values for
for the range of values for the associated select element
twelve_hr: If True, forces the output to be in 12-hr format (rather than 24-hr)
"""
self.attrs = attrs or {}
if twelve_hr:
self.twelve_hr = True # Do 12hr (rather than 24hr)
self.meridiem_val = 'a.m.' # Default to Morning (A.M.)
if hour_step and twelve_hr:
self.hours = range(1,13,hour_step)
elif hour_step: # 24hr, with stepping.
self.hours = range(0,24,hour_step)
elif twelve_hr: # 12hr, no stepping
self.hours = range(1,13)
else: # 24hr, no stepping
self.hours = range(0,24)
if minute_step:
self.minutes = range(0,60,minute_step)
else:
self.minutes = range(0,60)
if second_step:
self.seconds = range(0,60,second_step)
else:
self.seconds = range(0,60)
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 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)
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, 0) # minute
s = data.get(self.second_field % name, 0) # 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)
|
More like this
- Template tag - list punctuation for a list of items by shapiromatron 11 months, 2 weeks ago
- JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 11 months, 3 weeks ago
- Serializer factory with Django Rest Framework by julio 1 year, 6 months ago
- Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 7 months ago
- Help text hyperlinks by sa2812 1 year, 7 months ago
Comments
I am personally using the code found on Github; there are other forks out there too, just search Google for "SelectDateWidget Django".
Being relatively new to Django, it took me a few to figure out how to get this applied to my model and working right in the admin. For those of you that are in the same boat, here's how I got it working:
Put the widget on your system (I put mine in a widgets folder because I had other widget files) and then create (or add to) forms.py:
And in my admin.py:
If you have any inlines, don't forget to add
form = GameForm
to those also.I hope that helps someone. :)
BTW, it would be cool if this snippet had an option to change the default a.m./p.m. value (i.e. show p.m. by default would be nice).
#
Thanks for this widget! I'm using it in conjunction with your SplitSelectDateTimeWidget.
I did notice a bug - I am using 24 hour time and my initial values are for 00:00:00, but it kept rendering as 12:00:00. It seems that on line 106 it will change hour_val to 12 regardless of whether you are using the 12 hour clock. I changed it from:
to
and that seems to have solved my problem.
I've been learning django for about two months - this snippet has been a great help!
#
Ran into the same issue as Skrubly, I indented lines 106/107 one over to make it work.
#
How can i make it to accept null values? My time field can be null , but by default it shows 12:00:00
#
I have the same question as siddharth91. I tried making the default value null but it keeps storing 12:00:00 and shows midnight on my app. Great snippet otherwise. I am incorporating it in my work project. Can anyone please help me out at the earliest? Thanks in advance!
#
Any suggestions?
#
For those of you having the same problem as me, please refer ramens mod version of the above timepicker on github. Please refer the following link: https://github.com/ramen/django-select-time-widget
Thank me later
#
Please login first before commenting.