Last active
August 29, 2015 14:19
-
-
Save evenicoulddoit/137ea15bfbbc43a078b4 to your computer and use it in GitHub Desktop.
IterableSelectDateWidget
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 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