четверг, 8 мая 2008 г.

Расширение виджета для выбора даты в Django

В предыдущем посте я упомянул о виджете для выбора даты в Django. И все вроде бы хорошо, но этот виджет становится совершенно бесполезным, когда надо:

  • выбрать месяц день и год в другом порядке (например, в привычном день-месяц-год);
  • использовать трехбуквенные сокращения месяца, а не полное название
  • не выбирать день (например, май 2008)
  • не выбирать ни день, ни месяц (например, 2008 год)
  • добавить первым пустой <option>

И потому для этих случаев я смастерил очередной велосипед свой виджет, который устраняет все эти недостатки. Посмотреть как он работает можно здесь (поля Дата рождения, Год поступления, Год окончания, Период работы с, по).

Уже интересно?

Тогда получайте сам виджет:

import datetime, re
from time import strptime

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

PATTERNS = (
    ('%b', 'month'),
    ('%B', 'month'),
    ('%d', 'day'),
    ('%m', 'month'),
    ('%y', 'year'),
    ('%Y', 'year'),
)

class SelectDateWidget(Widget):
    """
    Extended version of django.newforms.extras.SelectDateWidget

    The main advantages are:
    - Widget can splits date input into custom select boxes.
    - Custom select boxes can have first empty option.
    """
    day_field = '%s_day'
    month_field = '%s_month'
    year_field = '%s_year'

    def __init__(self, *args, **kwargs):
        """
        Optional arguments:

        format_separator - separator in input_format. By default: -
        input_format     - valid date input format. By default: %B-%d-%Y
        null             - adds first empty option to all selects. By
                           default: False
        years            - list/tuple of years to use in the "year" select
                           box. By default: this year and next 9 printed.
        """
        self.attrs = kwargs.get('attrs', {})
        self.format_separator = kwargs.get('format_separator', '-')
        self.input_format = kwargs.get('input_format', '%B-%d-%Y')
        self.null = kwargs.get('null', False)

        if 'years' in kwargs:
            self.years = kwargs['years']
        else:
            year = datetime.date.today().year
            self.years = range(year, year+10)

        fields = []
        parts = self.input_format.split(self.format_separator)

        for part in parts:
            for k, v in PATTERNS:
                if part == k:
                    fields.append((k, v))

        if not fields:
            raise TypeError('Date input format "%s" is broken.' % self.input_format)

        self.fields = fields
        self.input_format = self.input_format.replace('%b', '%m').replace('%B', '%m')

    def id_for_label(self, id_):
        return id_
    id_for_label = classmethod(id_for_label)

    def render(self, name, value, attrs=None):
        try:
            year, month, day = value.year, value.month, value.day
        except AttributeError:
            year = month = day = None

            if isinstance(value, basestring):
                try:
                    t = strptime(value, self.input_format)
                    year, month, day = t[0], t[1], t[2]
                except:
                    pass

        def _choices(pattern):
            if pattern == '%b':
                choices = MONTHS_3.items()
                choices.sort()
            elif pattern == '%B':
                choices = MONTHS.items()
                choices.sort()
            elif pattern == '%d':
                choices = [(i, i) for i in range(1, 32)]
            elif pattern == '%m':
                choices = [(i, i) for i in range(1, 13)]
            elif pattern == '%y':
                choices = [(i, str(i)[-2:]) for i in self.years]
            elif pattern == '%Y':
                choices = [(i, i) for i in self.years]

            if self.null:
                choices.insert(0, (None, mark_safe('&mdash;')))

            return tuple(choices)

        id_ = self.attrs.get('id', 'id_%s' % name)
        output = []

        for i, field in enumerate(self.fields):
            pattern, field_name = field
            field = getattr(self, '%s_field' % field_name)

            sel_name = field % name
            sel_value = locals().get(field_name, None)

            if i == 0:
                local_attrs = self.build_attrs(id=id_)
            else:
                local_attrs['id'] = field % id_

            sel = Select(choices=_choices(pattern)).render(sel_name, sel_value, local_attrs)
            output.append(sel)

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

    def value_from_datadict(self, data, files, name):
        value = []

        for pattern, field_name in self.fields:
            field = getattr(self, '%s_field' % field_name)
            field_value = data.get(field % name, None)
            if field_value and field_value != 'None':
                value.append(str(field_value))

        if value:
            return '-'.join(value)

        return data.get(name, None)

p.s. Примеры использования виджета в упомянутой форме:

class CvForm(forms.Form):
    """ Some fields missed """
    g_birth_date = forms.DateField(label=_('Birth date'), initial=datetime.date.today,
        input_formats=('%d-%m-%Y',),
        widget=SelectDateWidget(input_format='%d-%B-%Y', years=range(year, year-101, -1)))
    e_from = forms.DateField(label=_('Entry year'), required=False, input_formats=('%Y',),
        widget=SelectDateWidget(input_format='%Y', years=range(year, year-51, -1), null=True))
    e_to = forms.DateField(label=_('Graduate year'), required=False, input_formats=('%Y'),
        widget=SelectDateWidget(input_format='%Y', years=range(year, year-51, -1), null=True))
    w_from = forms.DateField(label=_('Work from'), required=False, input_formats=('%m-%Y',),
        widget=SelectDateWidget(input_format='%B-%Y', years=range(year, year-51, -1), null=True))
    w_to = forms.DateField(label=_('Work to'), required=False, input_formats=('%m-%Y',),
        widget=SelectDateWidget(input_format='%B-%Y', years=range(year, year-51, -1), null=True))

8 комментариев:

lorien комментирует...

Не хватает # coding: utf-8 в начае файла, без него python ругается на строку

choices.insert(0, (None, mark_safe('—')))

, где уникодный символ вставлен.

playpauseandstop комментирует...

Не хватает # coding: utf-8 в начае файла, без него python ругается на строку
Это из-за Блоггера. В оригинале было &mdash;, а Блоггер интерпретировал это в HTML сущность.

Сейчас исправлю ;)

swi комментирует...

Спасибо большое за widget. Как раз начал ваять свой крвиой велосипед для такой же задачи и наткнулся на ваш :)

krv комментирует...

Было бы хорошо вместо пустых строк в каждый селект добавлять кастомный текст.

playpauseandstop комментирует...

@krv
да, это нужная возможность, сейчас подумаю, как ее реализовать, чтобы можно было указывать кастомный текст и единый для всех селектов, и разный.

mallox комментирует...

никому не мешает, что введённые данние не запоминаються?

Мария комментирует...

Отличный виджет, легко вставляется в форму. Но никак не могу вставить его в модели, в админку вместо дефолтного. Никто не подскажет технологию?

romankrv комментирует...

playpauseandstop писал:
"
да, это нужная возможность, сейчас подумаю, как ее реализовать, чтобы можно было указывать кастомный текст и единый для всех селектов, и разный.
"
А как обстоят дела с данной фичей. Реализована она?