From 728b6fd9ca8624271f072d5f4618dc3fd38e87f1 Mon Sep 17 00:00:00 2001 From: Loic Bistuer Date: Mon, 26 Jan 2015 10:28:57 +0700 Subject: [PATCH] Fixed #24219 -- Moved SelectDateWidget together with the other widgets and deprecated django.forms.extras. Thanks Berker Peksag and Tim Graham for the reviews. --- django/forms/extras/__init__.py | 11 +- django/forms/extras/widgets.py | 150 +---------------------- django/forms/widgets.py | 156 ++++++++++++++++++++++-- docs/internals/deprecation.txt | 2 + docs/ref/forms/widgets.txt | 15 ++- docs/releases/1.7.txt | 4 +- docs/releases/1.8.txt | 7 +- docs/releases/1.9.txt | 4 + tests/forms_tests/tests/test_widgets.py | 3 +- tests/i18n/forms.py | 3 +- tests/i18n/tests.py | 11 +- 11 files changed, 186 insertions(+), 180 deletions(-) diff --git a/django/forms/extras/__init__.py b/django/forms/extras/__init__.py index 1026bce5e59..1f411a50689 100644 --- a/django/forms/extras/__init__.py +++ b/django/forms/extras/__init__.py @@ -1,3 +1,12 @@ -from django.forms.extras.widgets import SelectDateWidget +import warnings + +from django.forms.widgets import SelectDateWidget +from django.utils.deprecation import RemovedInDjango21Warning __all__ = ['SelectDateWidget'] + + +warnings.warn( + "django.forms.extras is deprecated. You can find " + "SelectDateWidget in django.forms.widgets instead.", + RemovedInDjango21Warning, stacklevel=2) diff --git a/django/forms/extras/widgets.py b/django/forms/extras/widgets.py index 0d63b177f77..e53cb4069ed 100644 --- a/django/forms/extras/widgets.py +++ b/django/forms/extras/widgets.py @@ -1,149 +1 @@ -""" -Extra HTML Widget classes -""" -from __future__ import unicode_literals - -import datetime -import re - -from django.forms.widgets import Widget, Select -from django.utils import datetime_safe -from django.utils.dates import MONTHS -from django.utils.encoding import force_str -from django.utils.safestring import mark_safe -from django.utils.formats import get_format -from django.utils import six -from django.conf import settings - -__all__ = ('SelectDateWidget',) - -RE_DATE = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$') - - -def _parse_date_fmt(): - 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 SelectDateWidget(Widget): - """ - A Widget that splits date input into three boxes. + + This also serves as an example of a Widget that has more than one HTML + element and hence implements value_from_datadict. + """ + none_value = (0, '---') + month_field = '%s_month' + day_field = '%s_day' + year_field = '%s_year' + + date_re = re.compile(r'(\d{4})-(\d\d?)-(\d\d?)$') + + def __init__(self, attrs=None, years=None, months=None, empty_label=None): + self.attrs = attrs or {} + + # Optional list or tuple of years to use in the "year" select box. + if years: + self.years = years + else: + this_year = datetime.date.today().year + self.years = range(this_year, this_year + 10) + + # Optional dict of months to use in the "month" select box. + if months: + self.months = months + else: + self.months = MONTHS + + # Optional string, list, or tuple to use as empty_label. + if isinstance(empty_label, (list, tuple)): + if not len(empty_label) == 3: + raise ValueError('empty_label list/tuple must have 3 elements.') + + self.year_none_value = (0, empty_label[0]) + self.month_none_value = (0, empty_label[1]) + self.day_none_value = (0, empty_label[2]) + else: + if empty_label is not None: + self.none_value = (0, empty_label) + + self.year_none_value = self.none_value + self.month_none_value = self.none_value + self.day_none_value = self.none_value + + @staticmethod + def _parse_date_fmt(): + 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' + + def render(self, name, value, attrs=None): + try: + year_val, month_val, day_val = value.year, value.month, value.day + except AttributeError: + year_val = month_val = day_val = None + if isinstance(value, six.string_types): + if settings.USE_L10N: + try: + input_format = get_format('DATE_INPUT_FORMATS')[0] + v = datetime.datetime.strptime(force_str(value), input_format) + year_val, month_val, day_val = v.year, v.month, v.day + except ValueError: + pass + else: + match = self.date_re.match(value) + if match: + year_val, month_val, day_val = [int(v) for v in match.groups()] + html = {} + choices = [(i, i) for i in self.years] + html['year'] = self.create_select(name, self.year_field, value, year_val, choices, self.year_none_value) + choices = list(six.iteritems(self.months)) + html['month'] = self.create_select(name, self.month_field, value, month_val, choices, self.month_none_value) + choices = [(i, i) for i in range(1, 32)] + html['day'] = self.create_select(name, self.day_field, value, day_val, choices, self.day_none_value) + + output = [] + for field in self._parse_date_fmt(): + output.append(html[field]) + return mark_safe('\n'.join(output)) + + def id_for_label(self, id_): + for first_select in self._parse_date_fmt(): + return '%s_%s' % (id_, first_select) + else: + return '%s_month' % id_ + + def value_from_datadict(self, data, files, name): + y = data.get(self.year_field % name) + m = data.get(self.month_field % name) + d = data.get(self.day_field % 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 '%s-%s-%s' % (y, m, d) + else: + date_value = datetime_safe.new_date(date_value) + return date_value.strftime(input_format) + else: + return '%s-%s-%s' % (y, m, d) + return data.get(name, None) + + def create_select(self, name, field, value, val, choices, none_value): + if 'id' in self.attrs: + id_ = self.attrs['id'] + else: + id_ = 'id_%s' % name + if not self.is_required: + choices.insert(0, none_value) + local_attrs = self.build_attrs(id=field % id_) + s = Select(choices=choices) + select_html = s.render(field % name, val, local_attrs) + return select_html diff --git a/docs/internals/deprecation.txt b/docs/internals/deprecation.txt index 250ef1f6d32..50782b363c2 100644 --- a/docs/internals/deprecation.txt +++ b/docs/internals/deprecation.txt @@ -18,6 +18,8 @@ details on these changes. * The ``weak`` argument to ``django.dispatch.signals.Signal.disconnect()`` will be removed. +* The ``django.forms.extras`` package will be removed. + .. _deprecation-removed-in-2.0: 2.0 diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 242a77110f4..55b29c131e8 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -49,11 +49,10 @@ Setting arguments for widgets Many widgets have optional extra arguments; they can be set when defining the widget on the field. In the following example, the -:attr:`~django.forms.extras.widgets.SelectDateWidget.years` attribute is set -for a :class:`~django.forms.extras.widgets.SelectDateWidget`:: +:attr:`~django.forms.SelectDateWidget.years` attribute is set for a +:class:`~django.forms.SelectDateWidget`:: from django import forms - from django.forms.extras.widgets import SelectDateWidget BIRTH_YEAR_CHOICES = ('1980', '1981', '1982') FAVORITE_COLORS_CHOICES = (('blue', 'Blue'), @@ -61,7 +60,7 @@ for a :class:`~django.forms.extras.widgets.SelectDateWidget`:: ('black', 'Black')) class SimpleForm(forms.Form): - birth_year = forms.DateField(widget=SelectDateWidget(years=BIRTH_YEAR_CHOICES)) + birth_year = forms.DateField(widget=forms.SelectDateWidget(years=BIRTH_YEAR_CHOICES)) favorite_colors = forms.MultipleChoiceField(required=False, widget=forms.CheckboxSelectMultiple, choices=FAVORITE_COLORS_CHOICES) @@ -752,8 +751,6 @@ Composite widgets Similar to :class:`SplitDateTimeWidget`, but uses :class:`HiddenInput` for both date and time. -.. currentmodule:: django.forms.extras.widgets - ``SelectDateWidget`` ~~~~~~~~~~~~~~~~~~~~ @@ -807,3 +804,9 @@ Composite widgets # A custom empty label with tuple field1 = forms.DateField(widget=SelectDateWidget( empty_label=("Choose Year", "Choose Month", "Choose Day")) + + .. versionchanged:: 1.9 + + This widget used to be located in the ``django.forms.extras.widgets`` + package. It is now defined in ``django.forms.widgets`` and like the + other widgets it can be imported directly from ``django.forms``. diff --git a/docs/releases/1.7.txt b/docs/releases/1.7.txt index 63aa7722b8f..bb3ba41a91d 100644 --- a/docs/releases/1.7.txt +++ b/docs/releases/1.7.txt @@ -637,7 +637,7 @@ Forms value. * :attr:`SelectDateWidget.months - ` can be used to + ` can be used to customize the wording of the months displayed in the select widget. * The ``min_num`` and ``validate_min`` parameters were added to @@ -1378,7 +1378,7 @@ Miscellaneous the current class *and* on a parent ``Form``. * The ``required`` argument of - :class:`~django.forms.extras.widgets.SelectDateWidget` has been removed. + :class:`~django.forms.SelectDateWidget` has been removed. This widget now respects the form field's ``is_required`` attribute like other widgets. diff --git a/docs/releases/1.8.txt b/docs/releases/1.8.txt index c89c65de5c2..71a2a430e31 100644 --- a/docs/releases/1.8.txt +++ b/docs/releases/1.8.txt @@ -336,9 +336,10 @@ Forms a form's :attr:`~django.forms.Form.label_suffix` while using shortcuts such as ``{{ form.as_p }}`` in templates. -* :class:`~django.forms.extras.widgets.SelectDateWidget` now accepts an - :attr:`~django.forms.extras.widgets.SelectDateWidget.empty_label` argument, which will - override the top list choice label when :class:`~django.forms.DateField` is not required. +* :class:`~django.forms.SelectDateWidget` now accepts an + :attr:`~django.forms.SelectDateWidget.empty_label` argument, which will + override the top list choice label when :class:`~django.forms.DateField` + is not required. * After an :class:`~django.forms.ImageField` has been cleaned and validated, the ``UploadedFile`` object will have an additional ``image`` attribute containing diff --git a/docs/releases/1.9.txt b/docs/releases/1.9.txt index 9abb05d9210..5f62f081e90 100644 --- a/docs/releases/1.9.txt +++ b/docs/releases/1.9.txt @@ -187,6 +187,10 @@ Miscellaneous will be removed in Django 2.1. The more general ``check_expression_support()`` should be used instead. +``django.forms.extras`` is deprecated. You can find + :class:`~django.forms.SelectDateWidget` in ``django.forms.widgets`` + (or simply ``django.forms``) instead. + .. removed-features-1.9: Features removed in 1.9 diff --git a/tests/forms_tests/tests/test_widgets.py b/tests/forms_tests/tests/test_widgets.py index dd8a9b40307..d46378320ee 100644 --- a/tests/forms_tests/tests/test_widgets.py +++ b/tests/forms_tests/tests/test_widgets.py @@ -12,10 +12,9 @@ from django.forms import ( ChoiceField, ClearableFileInput, DateField, DateInput, DateTimeInput, FileInput, Form, HiddenInput, MultipleChoiceField, MultipleHiddenInput, MultiValueField, MultiWidget, NullBooleanSelect, PasswordInput, - RadioSelect, Select, SelectMultiple, SplitDateTimeField, + RadioSelect, Select, SelectDateWidget, SelectMultiple, SplitDateTimeField, SplitDateTimeWidget, Textarea, TextInput, TimeInput, ValidationError, ) -from django.forms.extras import SelectDateWidget from django.forms.widgets import RadioFieldRenderer from django.utils.safestring import mark_safe, SafeData from django.utils import six, translation diff --git a/tests/i18n/forms.py b/tests/i18n/forms.py index e06ed4b18da..8c290bf6644 100644 --- a/tests/i18n/forms.py +++ b/tests/i18n/forms.py @@ -1,5 +1,4 @@ from django import forms -from django.forms.extras import SelectDateWidget from .models import Company @@ -14,7 +13,7 @@ class I18nForm(forms.Form): class SelectDateForm(forms.Form): - date_field = forms.DateField(widget=SelectDateWidget) + date_field = forms.DateField(widget=forms.SelectDateWidget) class CompanyForm(forms.ModelForm): diff --git a/tests/i18n/tests.py b/tests/i18n/tests.py index f3ae0288c2e..0b5714d6bf6 100644 --- a/tests/i18n/tests.py +++ b/tests/i18n/tests.py @@ -11,6 +11,7 @@ import pickle from threading import local from unittest import skipUnless +from django import forms from django.conf import settings from django.template import Template, Context from django.template.base import TemplateSyntaxError @@ -36,7 +37,7 @@ from django.utils.translation import (activate, deactivate, check_for_language, string_concat, LANGUAGE_SESSION_KEY) -from .forms import I18nForm, SelectDateForm, SelectDateWidget, CompanyForm +from .forms import I18nForm, SelectDateForm, CompanyForm from .models import Company, TestModel @@ -549,7 +550,7 @@ class FormattingTests(TestCase): self.assertEqual(datetime.date(2009, 12, 31), form2.cleaned_data['date_field']) self.assertHTMLEqual( '\n\n', - SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) + forms.SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) ) # We shouldn't change the behavior of the floatformat filter re: @@ -667,14 +668,14 @@ class FormattingTests(TestCase): self.assertEqual(datetime.date(2009, 12, 31), form5.cleaned_data['date_field']) self.assertHTMLEqual( '\n\n', - SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) + forms.SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) ) # Russian locale (with E as month) with translation.override('ru', deactivate=True): self.assertHTMLEqual( '\n\n', - SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) + forms.SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) ) # English locale @@ -739,7 +740,7 @@ class FormattingTests(TestCase): self.assertEqual(datetime.date(2009, 12, 31), form6.cleaned_data['date_field']) self.assertHTMLEqual( '\n\n', - SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) + forms.SelectDateWidget(years=range(2009, 2019)).render('mydate', datetime.date(2009, 12, 31)) ) def test_sub_locales(self):