Login

Iterable SelectDateWidget

Author:
evenicoulddoit
Posted:
April 24, 2015
Language:
Python
Version:
1.5
Tags:
widget selectdatewidget
Score:
0 (after 0 ratings)

I wanted to be able to restyle the inputs, which required having access to each of the select widgets. When used in a form, you can simply iterate over the field to access each element.

Example: {% for form_field in form.date %} <div class="select-wrap"> {{ form_field }} </div> {% endfor %}

  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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
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]

More like this

Comments

Please login first before commenting.