Login

Month / Year dropdown widget

Author:
gregb
Posted:
August 16, 2009
Language:
Python
Version:
1.1
Score:
1 (after 1 ratings)

This is an adaption of

django.forms.extras.widgets.SelectDateWidget

which has no day dropdown - it still produces a date but with the day set to 1.

Example use

class myForm(forms.Form):
    # ...
    date = forms.DateField(
        required=False,
        widget=MonthYearWidget(years=xrange(2004,2010))
    )
 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
import datetime
import re

from django.forms.widgets import Widget, Select
from django.utils.dates import MONTHS
from django.utils.safestring import mark_safe

__all__ = ('MonthYearWidget',)

RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$')

class MonthYearWidget(Widget):
    """
    A Widget that splits date input into two <select> boxes for month and year,
    with 'day' defaulting to the first of the month.
    
    Based on SelectDateWidget, in 
    
    django/trunk/django/forms/extras/widgets.py
    
    
    """
    none_value = (0, '---')
    month_field = '%s_month'
    year_field = '%s_year'

    def __init__(self, attrs=None, years=None, required=True):
        # years is an optional list/tuple of years to use in the "year" select box.
        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)

    def render(self, name, value, attrs=None):
        try:
            year_val, month_val = value.year, value.month
        except AttributeError:
            year_val = month_val = None
            if isinstance(value, basestring):
                match = RE_DATE.match(value)
                if match:
                    year_val, month_val, day_val = [int(v) for v in match.groups()]

        output = []

        if 'id' in self.attrs:
            id_ = self.attrs['id']
        else:
            id_ = 'id_%s' % name

        month_choices = MONTHS.items()
        if not (self.required and value):
            month_choices.append(self.none_value)
        month_choices.sort()
        local_attrs = self.build_attrs(id=self.month_field % id_)
        s = Select(choices=month_choices)
        select_html = s.render(self.month_field % name, month_val, local_attrs)
        output.append(select_html)

        year_choices = [(i, i) for i in self.years]
        if not (self.required and value):
            year_choices.insert(0, self.none_value)
        local_attrs['id'] = self.year_field % id_
        s = Select(choices=year_choices)
        select_html = s.render(self.year_field % name, year_val, local_attrs)
        output.append(select_html)

        return mark_safe(u'\n'.join(output))

    def id_for_label(self, id_):
        return '%s_month' % id_
    id_for_label = classmethod(id_for_label)

    def value_from_datadict(self, data, files, name):
        y = data.get(self.year_field % name)
        m = data.get(self.month_field % name)
        if y == m == "0":
            return None
        if y and m:
            return '%s-%s-%s' % (y, m, 1)
        return data.get(name, None)

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 10 months, 2 weeks ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 10 months, 3 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 5 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 6 months ago
  5. Help text hyperlinks by sa2812 1 year, 6 months ago

Comments

leosh (on February 16, 2011):

There's a bug in this snippet. Line 56 should read:

    if not (self.required and month_val):

And line 65 should read:

    if not (self.required and year_val):

Otherwise you get strange behavior when only one of them is filled in.

#

danny_adair (on February 19, 2013):

The second last line

    return '%s-%s-%s' % (y, m, 1)

will only work if you didn't customize your DATE_FORMAT/DATE_INPUT_FORMATS (the former is usually the first item of the latter, i.e. the "preferred" input format). In order for this to always work, add

    from django.conf import settings

at the top, and replace that second last line with:

    return datetime.date(int(y), int(m), 1).strftime(
        settings.DATE_INPUT_FORMATS[0]
    )

P.S.: django.utils.formats.localize() is for rendering output, not input, and expects a setting in the format of DATE_FORMAT, i.e. without percentage signs... That's why I'm using time.strptime()

P.P.S.: Theoretically, the int() conversion could be try'd but the widget doesn't allow free text input so I find it ok to leave it out

#

danny_adair (on February 19, 2013):

"That's why I'm using time.strptime()" - uhm, make that "date.strftime()" :-)

#

pxg (on August 20, 2015):

I've added a Python 3 version here https://djangosnippets.org/snippets/10522/ (it was too long to put in the comments).

#

Please login first before commenting.