Skip to content

Instantly share code, notes, and snippets.

@evenicoulddoit
Last active August 29, 2015 14:19
Show Gist options
  • Save evenicoulddoit/137ea15bfbbc43a078b4 to your computer and use it in GitHub Desktop.
Save evenicoulddoit/137ea15bfbbc43a078b4 to your computer and use it in GitHub Desktop.
IterableSelectDateWidget
import datetime
import re
from django.conf import settings
from django.forms.extras import SelectDateWidget
from django.forms.widgets import Widget, Select
from django.utils import datetime_safe, six
from django.utils.dates import MONTHS
from django.utils.formats import get_format
from django.utils.safestring import mark_safe
RE_DATE = re.compile(
r'(?P<year>[1-9]\d{3})-(?P<month>[1-9]\d?)-(?P<day>[1-9]\d?)'
)
def _parse_date_fmt():
"""
Parse the date format in the settings for a year, month, and day.
"""
fmt = get_format('DATE_FORMAT')
escaped = False
for char in fmt:
if escaped:
escaped = False
elif char == '\\':
escaped = True
elif char in 'Yy':
yield 'year'
elif char in 'bEFMmNn':
yield 'month'
elif char in 'dj':
yield 'day'
class IterableSelectDateWidget(Widget, object):
"""
A widget for selecting dates with sub-widgets for the year/month/day.
"""
none_value = (0, '---')
month_field = '{0}_month'
day_field = '{0}_day'
year_field = '{0}_year'
def __init__(self, attrs=None, years=None, required=True):
"""
Initialise the widget and sub-widgets.
"""
self.attrs = attrs or {}
self.required = required
if years:
self.years = years
else:
this_year = datetime.date.today().year
self.years = range(this_year, this_year+10)
self._subwidgets = [
self.create_individual(component)
for component in _parse_date_fmt()
]
def create_individual(self, component_name):
"""
Create an individual widget.
"""
id_template = getattr(self, "{0}_field".format(component_name))
choices = []
if not self.required:
choices.insert(0, self.none_value)
if component_name == "day":
choices.extend([(i, i) for i in range(1, 32)])
elif component_name == "month":
choices.extend(list(six.iteritems(MONTHS)))
elif component_name == "year":
choices.extend([(i, i) for i in self.years])
return SelectDateSubWidget(
component_name, choices, id_template, self.attrs
)
def render(self, name, value, attrs=None):
"""
Render the combined widget.
"""
return mark_safe("\n".join((
widget.render(name, value, attrs) for widget in self._subwidgets
)))
def subwidgets(self, name, value, attrs=None):
"""
Return a generator for the rendered sub-widgets.
"""
for subwidget in self._subwidgets:
yield subwidget.render(name, value, attrs)
def id_for_label(self, id_):
"""
Return the first sub-widget's ID.
"""
try:
return '{0}_{1}'.format(id_, self._subwidgets[0].component)
except IndexError:
return '{0}_month'.format(id_)
def value_from_datadict(self, data, files, name):
"""
Parse the given data for the individual values, and combine them.
"""
y = data.get(self.year_field.format(name))
m = data.get(self.month_field.format(name))
d = data.get(self.day_field.format(name))
if y == m == d == "0":
return None
if y and m and d:
if settings.USE_L10N:
input_format = get_format('DATE_INPUT_FORMATS')[0]
try:
date_value = datetime.date(int(y), int(m), int(d))
except ValueError:
return '{0}-{1}-{2}'.format(y, m, d)
else:
date_value = datetime_safe.new_date(date_value)
return date_value.strftime(input_format)
else:
return '{0}-{1}-{2}'.format(y, m, d)
return data.get(name, None)
def _has_changed(self, initial, data):
"""
Attempt to parse the given date to determine whether its changed.
"""
try:
input_format = get_format('DATE_INPUT_FORMATS')[0]
data = datetime_safe.datetime.strptime(data, input_format).date()
except (TypeError, ValueError):
pass
return super(IterableSelectDateWidget, self)._has_changed(
initial, data
)
class SelectDateSubWidget(Select, object):
"""
An individual component (year/month/day) for the select date widget.
"""
def __init__(self, component, choices, id_template, attrs):
super(SelectDateSubWidget, self).__init__(choices=choices, attrs=attrs)
self.attrs = attrs
self.component = component
self.id_template = id_template
def render(self, name, value, attrs=None):
"""
Render the individual select widget.
"""
value = self._parse_value(value)
id_ = attrs['id'] if "id" in self.attrs else "id_{0}".format(name)
local_attrs = self.build_attrs(id=self.id_template.format(id_))
return super(SelectDateSubWidget, self).render(
self.id_template.format(name), value, local_attrs
)
def _parse_value(self, value):
"""
Get the sub-value for the given component (day/month/year).
"""
try:
return getattr(value, self.component)
except AttributeError:
if isinstance(value, six.string_types):
if settings.USE_L10N:
try:
input_format = get_format('DATE_INPUT_FORMATS')[0]
v = datetime.datetime.strptime(value, input_format)
return getattr(v, self.component)
except ValueError:
pass
else:
match = RE_DATE.match(value)
if match:
return match.groupdict()[self.component]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment