From 4f16376274a4e52074722c615fccef5fac5f009a Mon Sep 17 00:00:00 2001 From: Claude Paroz Date: Mon, 28 Jan 2013 14:12:56 +0100 Subject: [PATCH] Added HTML5 email input type Refs #16630. --- django/forms/fields.py | 7 ++- django/forms/widgets.py | 6 ++- docs/ref/forms/api.txt | 53 +++++++++++---------- docs/ref/forms/fields.txt | 8 ++-- docs/ref/forms/widgets.txt | 11 ++++- docs/releases/1.6.txt | 7 +++ docs/topics/forms/index.txt | 2 +- tests/regressiontests/forms/tests/extra.py | 2 +- tests/regressiontests/forms/tests/fields.py | 7 +++ tests/regressiontests/forms/tests/forms.py | 10 ++-- 10 files changed, 72 insertions(+), 41 deletions(-) diff --git a/django/forms/fields.py b/django/forms/fields.py index d16b501baa..8a3dbfeb49 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -18,10 +18,12 @@ from io import BytesIO from django.core import validators from django.core.exceptions import ValidationError from django.forms.util import ErrorList, from_current_timezone, to_current_timezone -from django.forms.widgets import (TextInput, PasswordInput, HiddenInput, +from django.forms.widgets import ( + TextInput, PasswordInput, EmailInput, HiddenInput, MultipleHiddenInput, ClearableFileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput, - SplitDateTimeWidget, SplitHiddenDateTimeWidget, FILE_INPUT_CONTRADICTION) + SplitDateTimeWidget, SplitHiddenDateTimeWidget, FILE_INPUT_CONTRADICTION +) from django.utils import formats from django.utils.encoding import smart_text, force_str, force_text from django.utils.ipv6 import clean_ipv6_address @@ -487,6 +489,7 @@ class RegexField(CharField): regex = property(_get_regex, _set_regex) class EmailField(CharField): + widget = EmailInput default_error_messages = { 'invalid': _('Enter a valid email address.'), } diff --git a/django/forms/widgets.py b/django/forms/widgets.py index 303844d44b..f201e914dd 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -22,7 +22,7 @@ from django.utils.safestring import mark_safe from django.utils import datetime_safe, formats, six __all__ = ( - 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'PasswordInput', + 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'EmailInput', 'PasswordInput', 'HiddenInput', 'MultipleHiddenInput', 'ClearableFileInput', 'FileInput', 'DateInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput', 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', @@ -251,6 +251,10 @@ class TextInput(Input): super(TextInput, self).__init__(attrs) +class EmailInput(TextInput): + input_type = 'email' + + class PasswordInput(TextInput): input_type = 'password' diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index d1f877ff65..44e4684c1d 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -270,7 +270,7 @@ simply ``print`` it:: >>> print(f) - + If the form is bound to data, the HTML output will include that data @@ -287,7 +287,7 @@ include ``checked="checked"`` if appropriate:: >>> print(f) - + This default output is a two-column HTML table, with a ```` for each field. @@ -297,8 +297,9 @@ Notice the following: ```` tags, nor does it include the ``
`` and ``
`` tags or an ```` tag. It's your job to do that. -* Each field type has a default HTML representation. ``CharField`` and - ``EmailField`` are represented by an ````. +* Each field type has a default HTML representation. ``CharField`` is + represented by an ```` and ``EmailField`` by an + ````. ``BooleanField`` is represented by an ````. Note these are merely sensible defaults; you can specify which HTML to use for a given field by using widgets, which we'll explain shortly. @@ -335,7 +336,7 @@ a form object, and each rendering method returns a Unicode object. >>> print(f.as_p())

-

+

``as_ul()`` @@ -350,11 +351,11 @@ a form object, and each rendering method returns a Unicode object. >>> f = ContactForm() >>> f.as_ul() - u'
  • \n
  • \n
  • \n
  • ' + u'
  • \n
  • \n
  • \n
  • ' >>> print(f.as_ul())
  • -
  • +
  • ``as_table()`` @@ -368,11 +369,11 @@ a form object, and each rendering method returns a Unicode object. >>> f = ContactForm() >>> f.as_table() - u'\n\n\n' + u'\n\n\n' >>> print(f.as_table()) - + Styling required or erroneous form rows @@ -431,17 +432,17 @@ tags nor ``id`` attributes:: >>> print(f.as_table()) Subject: Message: - Sender: + Sender: Cc myself: >>> print(f.as_ul())
  • Subject:
  • Message:
  • -
  • Sender:
  • +
  • Sender:
  • Cc myself:
  • >>> print(f.as_p())

    Subject:

    Message:

    -

    Sender:

    +

    Sender:

    Cc myself:

    If ``auto_id`` is set to ``True``, then the form output *will* include @@ -452,17 +453,17 @@ field:: >>> print(f.as_table()) - + >>> print(f.as_ul())
  • -
  • +
  • >>> print(f.as_p())

    -

    +

    If ``auto_id`` is set to a string containing the format character ``'%s'``, @@ -475,17 +476,17 @@ attributes based on the format string. For example, for a format string >>> print(f.as_table()) - + >>> print(f.as_ul())
  • -
  • +
  • >>> print(f.as_p())

    -

    +

    If ``auto_id`` is set to any other true value -- such as a string that doesn't @@ -501,13 +502,13 @@ entirely, using the ``label_suffix`` parameter:: >>> print(f.as_ul())
  • -
  • +
  • >>> f = ContactForm(auto_id='id_for_%s', label_suffix=' ->') >>> print(f.as_ul())
  • -
  • +
  • Note that the label suffix is added only if the last character of the @@ -539,19 +540,19 @@ method you're using:: >>> print(f.as_table()) Subject: Message: - Sender: + Sender: Cc myself: >>> print(f.as_ul())
  • Subject:
  • Message:
  • -
  • Sender:
  • +
  • Sender:
  • Cc myself:
  • >>> print(f.as_p())

    Subject:

    Message:

    -

    Sender:

    +

    Sender:

    Cc myself:

    Customizing the error list format @@ -574,7 +575,7 @@ pass that in at construction time::

    Subject:

    Message:

    Enter a valid email address.
    -

    Sender:

    +

    Sender:

    Cc myself:

    More granular output @@ -604,7 +605,7 @@ To retrieve all ``BoundField`` objects, iterate the form:: >>> for boundfield in form: print(boundfield) - + The field-specific output honors the form object's ``auto_id`` setting:: @@ -756,7 +757,7 @@ fields are ordered first:: >>> print(f.as_ul())
  • Subject:
  • Message:
  • -
  • Sender:
  • +
  • Sender:
  • Cc myself:
  • Priority:
  • diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 28b7e49d2d..8bfe77cc20 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -212,17 +212,17 @@ fields. We've specified ``auto_id=False`` to simplify the output:: >>> print(f.as_table()) Subject:
    100 characters max. Message: - Sender:
    A valid email address, please. + Sender:
    A valid email address, please. Cc myself: >>> print(f.as_ul()))
  • Subject: 100 characters max.
  • Message:
  • -
  • Sender: A valid email address, please.
  • +
  • Sender: A valid email address, please.
  • Cc myself:
  • >>> print(f.as_p())

    Subject: 100 characters max.

    Message:

    -

    Sender: A valid email address, please.

    +

    Sender: A valid email address, please.

    Cc myself:

    ``error_messages`` @@ -489,7 +489,7 @@ For each field, we describe the default widget used if you don't specify .. class:: EmailField(**kwargs) - * Default widget: :class:`TextInput` + * Default widget: :class:`EmailInput` * Empty value: ``''`` (an empty string) * Normalizes to: A Unicode object. * Validates that the given value is a valid email address, using a diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index bc1270094b..9105a41b25 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -392,7 +392,16 @@ These widgets make use of the HTML elements ``input`` and ``textarea``. .. class:: TextInput - Text input: ```` + Text input: ```` + +``EmailInput`` +~~~~~~~~~~~~~~ + +.. class:: EmailInput + + .. versionadded:: 1.6 + + Text input: ```` ``PasswordInput`` ~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 29ecad3e9f..03341e586b 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -31,6 +31,9 @@ Minor features * Added :meth:`~django.db.models.query.QuerySet.earliest` for symmetry with :meth:`~django.db.models.query.QuerySet.latest`. +* The default widgets for :class:`~django.forms.EmailField` use + the new type attribute available in HTML5 (type='email'). + Backwards incompatible changes in 1.6 ===================================== @@ -39,6 +42,10 @@ Backwards incompatible changes in 1.6 :meth:`~django.db.models.query.QuerySet.none` has been called: ``isinstance(qs.none(), EmptyQuerySet)`` +* If your CSS/Javascript code used to access HTML input widgets by type, you + should review it as ``type='text'`` widgets might be now output as + ``type='email'`` depending on their corresponding field type. + .. warning:: In addition to the changes outlined in this section, be sure to review the diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index a3c17e1555..78f51f3fcc 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -223,7 +223,7 @@ wrapped in a paragraph. Here's the output for our example template::

    -

    +

    diff --git a/tests/regressiontests/forms/tests/extra.py b/tests/regressiontests/forms/tests/extra.py index 762b774a93..b5f36dc981 100644 --- a/tests/regressiontests/forms/tests/extra.py +++ b/tests/regressiontests/forms/tests/extra.py @@ -631,7 +631,7 @@ class FormsExtraTestCase(TestCase, AssertFormErrorsMixin): f = CommentForm(data, auto_id=False, error_class=DivErrorList) self.assertHTMLEqual(f.as_p(), """

    Name:

    Enter a valid email address.
    -

    Email:

    +

    Email:

    This field is required.

    Comment:

    """) diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index c533370b68..aa03377d44 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -53,6 +53,11 @@ def fix_os_paths(x): class FieldsTests(SimpleTestCase): + def assertWidgetRendersTo(self, field, to): + class _Form(Form): + f = field + self.assertHTMLEqual(str(_Form()['f']), to) + def test_field_sets_widget_is_required(self): self.assertTrue(Field(required=True).widget.is_required) self.assertFalse(Field(required=False).widget.is_required) @@ -545,6 +550,7 @@ class FieldsTests(SimpleTestCase): def test_emailfield_1(self): f = EmailField() + self.assertWidgetRendersTo(f, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) self.assertEqual('person@example.com', f.clean('person@example.com')) @@ -569,6 +575,7 @@ class FieldsTests(SimpleTestCase): def test_emailfield_min_max_length(self): f = EmailField(min_length=10, max_length=15) + self.assertWidgetRendersTo(f, '') self.assertRaisesMessage(ValidationError, "'Ensure this value has at least 10 characters (it has 9).'", f.clean, 'a@foo.com') self.assertEqual('alf@foo.com', f.clean('alf@foo.com')) self.assertRaisesMessage(ValidationError, "'Ensure this value has at most 15 characters (it has 20).'", f.clean, 'alf123456788@foo.com') diff --git a/tests/regressiontests/forms/tests/forms.py b/tests/regressiontests/forms/tests/forms.py index ade06845f8..f2fa78e229 100644 --- a/tests/regressiontests/forms/tests/forms.py +++ b/tests/regressiontests/forms/tests/forms.py @@ -245,11 +245,11 @@ class FormsTestCase(TestCase): get_spam = BooleanField() f = SignupForm(auto_id=False) - self.assertHTMLEqual(str(f['email']), '') + self.assertHTMLEqual(str(f['email']), '') self.assertHTMLEqual(str(f['get_spam']), '') f = SignupForm({'email': 'test@example.com', 'get_spam': True}, auto_id=False) - self.assertHTMLEqual(str(f['email']), '') + self.assertHTMLEqual(str(f['email']), '') self.assertHTMLEqual(str(f['get_spam']), '') # 'True' or 'true' should be rendered without a value attribute @@ -1739,7 +1739,7 @@ class FormsTestCase(TestCase): -
  • +
  • """) self.assertHTMLEqual(p.as_p(), """ @@ -1749,7 +1749,7 @@ class FormsTestCase(TestCase):

    -

    +

    """) @@ -1759,7 +1759,7 @@ class FormsTestCase(TestCase): - + """) def test_label_split_datetime_not_displayed(self):