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[1-9]\d{3})-(?P[1-9]\d?)-(?P[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]