Показаны сообщения с ярлыком widgets. Показать все сообщения
Показаны сообщения с ярлыком widgets. Показать все сообщения

13 февраля 2009 г.

Простой и работающий JSONField для Django

Не успел, я починить PickleField для Django, мне понадобилось создать JSONField. Задача оказалась решенной на раз/два, плюс ко всему добавился маленький и полезный виджет для JSONField'а, который показывает красиво отформатированный JSON в textarea. Сразу скажу, что contribute_to_class метод чуть менне, чем полностью, скопипастен с снипетта 377.

from django.conf import settings
from django.forms.widgets import Textarea
from django.db.models import SubfieldBase, TextField
from django.utils import simplejson


class JSONField(TextField):
    __metaclass__ = SubfieldBase

    def contribute_to_class(self, cls, name):
        super(JSONField, self).contribute_to_class(cls, name)

        def get_json(model):
            return self.get_db_prep_value(getattr(model, self.attname))
        setattr(cls, 'get_%s_json' % self.name, get_json)

        def set_json(model, json):
            setattr(model, self.attname, self.to_python(json))
        setattr(cls, 'set_%s_json' % self.name, set_json)

    def formfield(self, **kwargs):
        kwargs['widget'] = JSONWidget(attrs={'class': 'vLargeTextField'})
        return super(JSONField, self).formfield(**kwargs)

    def get_db_prep_value(self, value):
        return simplejson.dumps(value)

    def to_python(self, value):
        if not isinstance(value, basestring):
            return value

        try:
            return simplejson.loads(value, encoding=settings.DEFAULT_CHARSET)
        except ValueError, e:
            # If string could not parse as JSON it's means that it's Python
            # string saved to JSONField.
            return value

class JSONWidget(Textarea):
    """
    Prettify dumps of all non-string JSON data.
    """
    def render(self, name, value, attrs=None):
        if not isinstance(value, basestring) and value is not None:
            value = simplejson.dumps(value, indent=4, sort_keys=True)
        return super(JSONWidget, self).render(name, value, attrs)

Ну и пару примеров использования, напоследок:

>>> # Пусть у нас есть простая модель
>>> class Sample(models.Model):
...    name = models.CharField(max_length=16)
...    data = JSONField()

>>> # Создадим модель и сохраним в data поле настройки для ShadowBox
>>> sb = Sample.objects.create(name='ShadowBox',
...                            data={'autoDimensions': True,
...                                  'overlayOpacity': 0.5,
...                                  'skipSetup': True})

>>> # Теперь мы хотим напечатать эти настройки где-то в шаблоне
>>> sb.get_data_json()
'{"overlayOpacity": 0.5, "autoDimensions": true, "skipSetup": true}'

>>> # Вспомним, что нам вообщем-то не зачем кастомное значение для overlayOpacity
>>> del sb.data['overlayOpacity']
>>> sb.save()

>>> # И опять напечатаем настройки в шаблоне
>>> sb.get_data_json()
'{"autoDimensions": true, "skipSetup": true}'

зы. ShadowBox - это лучший лайтбокс для любого JavaScript фреймворка, имо.

29 мая 2008 г.

jQuery AutocompleteWidget для Django

По моему скромному мнению, сейчас найти правильный сниппет, показывающий как реализовать автозаполнение на том или ином языке программирования с использованием того или иного JavaScript фреймворка - пустяковое дело.

Так что будем считать, что сегодня просто сама по себе настала очередь связки jQuery + Django. Реализация виджета автозаполнения в этой связке - проста до ужаса. Особенно, когда используется великолепный jQuery Autocomplete plugin.

Вся идея реализации состоит в следующем:

  1. Определяем какие данные будут использоваться в автозаполнении (локальные или удаленные)
  2. Если данные удаленные, пишем view и добавляем этот view в urlpatterns
  3. Получаем автозаполняемое поле

Но чтобы потом в дальнейшем по сто раз не дописывать этот виджет я чуточку его модифицировал. И посему Вы можете указать какие поля являются зависимыми от текущего (это например, очень полезно в связке страна - город, ведь незачем выбрав страну Украина, автодополнять город Амстердам). Это раз, а два - Вы можете указать какие именно опции плагина вам необходимы для этого поля.

Ну что ж, перейдем к программному коду. Для начала сам виджет:

from django.core.urlresolvers import reverse, NoReverseMatch
from django.newforms import HiddenInput, TextInput
from django.utils import simplejson
from django.utils.safestring import mark_safe

class AutocompleteWidget(TextInput):
    """
    Autocomplete widget to use with jquery-autocomplete plugin.

    Widget can use for static and dynamic (AJAX-liked) data. Also
    you can relate some fields and it's values'll posted to autocomplete
    view.

    Widget support all jquery-autocomplete options that dumped to JavaScript
    via django.utils.simplejson.

    **Note** You must init one of ``choices`` or ``choices_url`` attribute.
    Else widget raises TypeError when rendering.
    """
    def __init__(self, attrs=None, choices=None, choices_url=None, options=None, related_fields=None):
        """
        Optional arguments:
        -------------------

            * ``choices`` - Static autocomplete choices (similar to choices
            used in Select widget).

            * ``choices_url`` - Path to autocomplete view or autocomplete
            url name.

            * ``options`` - jQuery autocomplete plugin options. Auto dumped
            to JavaScript via SimpleJSON

            * ``related_fields`` - Fields that relates to current (value
            of this field will sended to autocomplete view via POST)
        """
        self.attrs = attrs or {}
        self.choice, self.choices, self.choices_url = None, choices, choices_url
        self.options = options or {}

        if related_fields:
            extra = {}
            if isinstance(related_fields, str):
                related_fields = list(related_fields)

            for field in related_fields:
                extra[field] = "%s_value" % field

            self.extra = extra
        else:
            self.extra = {}

    def render(self, name, value=None, attrs=None):
        if not self.choices and not self.choices_url:
            raise TypeError('One of "choices" or "choices_url" keyword argument must be supplied obligatory.')

        if self.choices and self.choices_url:
            raise TypeError('Only one of "choices" or "choices_url" keyword argument can be supplied.')

        choices = ''

        if self.choices:
            self.set_current_choice(value)
            choices = simplejson.dumps([unicode(v) for k, v in self.choices], ensure_ascii=False)
            html_code = HiddenInput().render(name, value=value)
            name += '_autocomplete'
        else:
            html_code = ''

        if self.choices_url:
            try:
                choices = simplejson.dumps(reverse(str(self.choices_url)))
            except NoReverseMatch:
                choices = simplejson.dumps(self.choices_url)

        if self.options or self.extra:
            if 'extraParams' in self.options:
                self.options['extraParams'].update(self.extra)
            else:
                self.options['extraParams'] = self.extra

            options = ', ' + simplejson.dumps(self.options, indent=4, sort_keys=True)
            extra = []

            for k, v in self.extra.items():
                options = options.replace(simplejson.dumps(v), v)
                extra.append(u"function %s() { return $('#id_%s').val(); }\n" % (v, k))

            extra = u''.join(extra)
        else:
            extra, options = '', ''

        final_attrs = self.build_attrs(attrs)
        html_code += super(AutocompleteWidget, self).render(name, self.choice or value, attrs)

        html_code += u"""
<script type="text/javascript"><!--
    %s$('#%s').autocomplete(%s%s);
--></script>
""" % (extra, final_attrs['id'], choices, options)

        return mark_safe(html_code)

    def set_current_choice(self, data):
        if not self.choices:
            raise ValueError('"choices" attribute was not defined yet.')

        for k, v in self.choices:
            if k == data:
                self.choice = v
                break

    def value_from_datadict(self, data, files, name):
        if not self.choices:
            return super(AutocompleteWidget, self).value_from_datadict(data, files, name)

        autocomplete_name = name + '_autocomplete'

        if not autocomplete_name in data:
            self.set_current_choice(data[name])
            return data[name]

        for k, v in self.choices:
            if v == data[autocomplete_name]:
                self.set_current_choice(k)
                return k

Теперь примеры его использования:

forms.py
from django import newforms as forms
from django.utils.translation import ugettext as _

from myproject.widgets import AutocompleteWidget

SPORTS_CHOICES = (
    ('basketball', _('Basketball')),
    ('football', _('Football')),
    ('hockey', _('Hockey')),
)

class SampleForm(forms.Form):
    name = forms.CharField(label=_('Name'))
    country = forms.CharField(label=_('Country'),
        widget=AutocompleteWidget(choices_url='autocomplete_countries', related_fields=('city',)))
    city = forms.CharField(label=_('City),
        widget=AutocompleteWidget(choices_url='autocomplete_cities', related_fields=('country',)))
    sports = forms.ChoiceField(label=_('Sports'), choices=SPORTS_CHOICES,
        widget=AutocompleteWidget(options={'minChars': 0, 'autoFill': True, 'mustMatch': True}))

urls.py
from django.conf.urls.defaults import *

urlpatterns = patterns('',
    url('/path/to/cities/autocomplete/', 'views.autocomplete_cities', name='autocomplete_cities'),
    url('/path/to/countries/autocomplete/', 'views.autocomplete_countries', name='autocomplete_countries'),
)

views.py
from myproject.models import City

def autocomplete_cities(request):
    def results_to_string(results):
        if results:
            for r in results:
                yield '%s|%s\n' % (r.name, r.pk)

    city = request.REQUEST.get('q', None)
    country = request.REQUEST.get('country', None)

    if city:
        cities = City.objects.filter(city__istartswith=city)
    else:
        cities = City.objects.all()

    if country:
        cities = cities.filter(country__name=country)

    cities = cities[:int(request.REQUEST.get('limit', 15))]
    return results_to_string(cities, mimetype='text/plain')

А вот как это выглядит ВРЛ:

  • Автозаполнение поля "City" (удаленные данные)
  • Автозаполнение поля "Country" (удаленные данные)
  • Связь при автозаполнении между полями "Country" и "Сity"
  • Автозаполнение поля "Sports" (локальные данные)

За сим прощаюсь и поздравляю всех с наступающим долгожданным летом!

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))

Виджет для выбора даты в Django

Лежит в django.newforms.extras.widgets.SelectDateWidget

Пример использования:

import datetime

from django import newforms as forms
from django.newforms.extras.widgets import SelectDateWidget
from django.utils.translation import ugettext as _

year = datetime.date.today().year

class SampleForm(forms.Form):
    default_date = forms.DateField(label=_('Default date'), initial=datetime.date.today,
        help_text=_('Today date in text input.'))
    birth_date = forms.DateField(label=_('Birth date'), initial=datetime.date.today,
        help_text=_('Today date in 3 selects (each for day, month and year) with 100 latest years'),
        widget=SelectDateWidget(years=range(year, year-100, -1)))
    future_date = forms.DateField(label=_('Future date'), initial=datetime.date.today,
        help_text=_('Today date in 3 selects (each for day, month and year).'))

без применения всяких стилей эта форма будет выглядеть следующим образом: