Added HTML5 url input type

Refs #16630.
This commit is contained in:
Claude Paroz 2013-01-28 14:24:48 +01:00
parent 4f16376274
commit f7394d2c32
8 changed files with 38 additions and 20 deletions

View File

@ -19,7 +19,7 @@ from django.core import validators
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms.util import ErrorList, from_current_timezone, to_current_timezone from django.forms.util import ErrorList, from_current_timezone, to_current_timezone
from django.forms.widgets import ( from django.forms.widgets import (
TextInput, PasswordInput, EmailInput, HiddenInput, TextInput, PasswordInput, EmailInput, URLInput, HiddenInput,
MultipleHiddenInput, ClearableFileInput, CheckboxInput, Select, MultipleHiddenInput, ClearableFileInput, CheckboxInput, Select,
NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput, NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput,
SplitDateTimeWidget, SplitHiddenDateTimeWidget, FILE_INPUT_CONTRADICTION SplitDateTimeWidget, SplitHiddenDateTimeWidget, FILE_INPUT_CONTRADICTION
@ -612,6 +612,7 @@ class ImageField(FileField):
return f return f
class URLField(CharField): class URLField(CharField):
widget = URLInput
default_error_messages = { default_error_messages = {
'invalid': _('Enter a valid URL.'), 'invalid': _('Enter a valid URL.'),
} }

View File

@ -22,7 +22,8 @@ from django.utils.safestring import mark_safe
from django.utils import datetime_safe, formats, six from django.utils import datetime_safe, formats, six
__all__ = ( __all__ = (
'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'EmailInput', 'PasswordInput', 'Media', 'MediaDefiningClass', 'Widget', 'TextInput',
'EmailInput', 'URLInput', 'PasswordInput',
'HiddenInput', 'MultipleHiddenInput', 'ClearableFileInput', 'HiddenInput', 'MultipleHiddenInput', 'ClearableFileInput',
'FileInput', 'DateInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput', 'FileInput', 'DateInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput',
'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect',
@ -255,6 +256,10 @@ class EmailInput(TextInput):
input_type = 'email' input_type = 'email'
class URLInput(TextInput):
input_type = 'url'
class PasswordInput(TextInput): class PasswordInput(TextInput):
input_type = 'password' input_type = 'password'

View File

@ -161,7 +161,7 @@ precedence::
>>> f = CommentForm(initial={'name': 'instance'}, auto_id=False) >>> f = CommentForm(initial={'name': 'instance'}, auto_id=False)
>>> print(f) >>> print(f)
<tr><th>Name:</th><td><input type="text" name="name" value="instance" /></td></tr> <tr><th>Name:</th><td><input type="text" name="name" value="instance" /></td></tr>
<tr><th>Url:</th><td><input type="text" name="url" /></td></tr> <tr><th>Url:</th><td><input type="url" name="url" /></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" /></td></tr> <tr><th>Comment:</th><td><input type="text" name="comment" /></td></tr>
Accessing "clean" data Accessing "clean" data

View File

@ -112,7 +112,7 @@ We've specified ``auto_id=False`` to simplify the output::
>>> f = CommentForm(auto_id=False) >>> f = CommentForm(auto_id=False)
>>> print(f) >>> print(f)
<tr><th>Your name:</th><td><input type="text" name="name" /></td></tr> <tr><th>Your name:</th><td><input type="text" name="name" /></td></tr>
<tr><th>Your Web site:</th><td><input type="text" name="url" /></td></tr> <tr><th>Your Web site:</th><td><input type="url" name="url" /></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" /></td></tr> <tr><th>Comment:</th><td><input type="text" name="comment" /></td></tr>
``initial`` ``initial``
@ -135,7 +135,7 @@ field is initialized to a particular value. For example::
>>> f = CommentForm(auto_id=False) >>> f = CommentForm(auto_id=False)
>>> print(f) >>> print(f)
<tr><th>Name:</th><td><input type="text" name="name" value="Your name" /></td></tr> <tr><th>Name:</th><td><input type="text" name="name" value="Your name" /></td></tr>
<tr><th>Url:</th><td><input type="text" name="url" value="http://" /></td></tr> <tr><th>Url:</th><td><input type="url" name="url" value="http://" /></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" /></td></tr> <tr><th>Comment:</th><td><input type="text" name="comment" /></td></tr>
You may be thinking, why not just pass a dictionary of the initial values as You may be thinking, why not just pass a dictionary of the initial values as
@ -150,7 +150,7 @@ and the HTML output will include any validation errors::
>>> f = CommentForm(default_data, auto_id=False) >>> f = CommentForm(default_data, auto_id=False)
>>> print(f) >>> print(f)
<tr><th>Name:</th><td><input type="text" name="name" value="Your name" /></td></tr> <tr><th>Name:</th><td><input type="text" name="name" value="Your name" /></td></tr>
<tr><th>Url:</th><td><ul class="errorlist"><li>Enter a valid URL.</li></ul><input type="text" name="url" value="http://" /></td></tr> <tr><th>Url:</th><td><ul class="errorlist"><li>Enter a valid URL.</li></ul><input type="url" name="url" value="http://" /></td></tr>
<tr><th>Comment:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="comment" /></td></tr> <tr><th>Comment:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="comment" /></td></tr>
This is why ``initial`` values are only displayed for unbound forms. For bound This is why ``initial`` values are only displayed for unbound forms. For bound
@ -805,7 +805,7 @@ For each field, we describe the default widget used if you don't specify
.. class:: URLField(**kwargs) .. class:: URLField(**kwargs)
* Default widget: :class:`TextInput` * Default widget: :class:`URLInput`
* Empty value: ``''`` (an empty string) * Empty value: ``''`` (an empty string)
* Normalizes to: A Unicode object. * Normalizes to: A Unicode object.
* Validates that the given value is a valid URL. * Validates that the given value is a valid URL.

View File

@ -139,7 +139,7 @@ provided for each widget will be rendered exactly the same::
>>> f = CommentForm(auto_id=False) >>> f = CommentForm(auto_id=False)
>>> f.as_table() >>> f.as_table()
<tr><th>Name:</th><td><input type="text" name="name" /></td></tr> <tr><th>Name:</th><td><input type="text" name="name" /></td></tr>
<tr><th>Url:</th><td><input type="text" name="url"/></td></tr> <tr><th>Url:</th><td><input type="url" name="url"/></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" /></td></tr> <tr><th>Comment:</th><td><input type="text" name="comment" /></td></tr>
On a real Web page, you probably don't want every widget to look the same. You On a real Web page, you probably don't want every widget to look the same. You
@ -160,7 +160,7 @@ Django will then include the extra attributes in the rendered output:
>>> f = CommentForm(auto_id=False) >>> f = CommentForm(auto_id=False)
>>> f.as_table() >>> f.as_table()
<tr><th>Name:</th><td><input type="text" name="name" class="special"/></td></tr> <tr><th>Name:</th><td><input type="text" name="name" class="special"/></td></tr>
<tr><th>Url:</th><td><input type="text" name="url"/></td></tr> <tr><th>Url:</th><td><input type="url" name="url"/></td></tr>
<tr><th>Comment:</th><td><input type="text" name="comment" size="40"/></td></tr> <tr><th>Comment:</th><td><input type="text" name="comment" size="40"/></td></tr>
.. _styling-widget-classes: .. _styling-widget-classes:
@ -403,6 +403,15 @@ These widgets make use of the HTML elements ``input`` and ``textarea``.
Text input: ``<input type="email" ...>`` Text input: ``<input type="email" ...>``
``URLInput``
~~~~~~~~~~~~
.. class:: URLInput
.. versionadded:: 1.6
Text input: ``<input type="url" ...>``
``PasswordInput`` ``PasswordInput``
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~

View File

@ -31,8 +31,9 @@ Minor features
* Added :meth:`~django.db.models.query.QuerySet.earliest` for symmetry with * Added :meth:`~django.db.models.query.QuerySet.earliest` for symmetry with
:meth:`~django.db.models.query.QuerySet.latest`. :meth:`~django.db.models.query.QuerySet.latest`.
* The default widgets for :class:`~django.forms.EmailField` use * The default widgets for :class:`~django.forms.EmailField` and
the new type attribute available in HTML5 (type='email'). :class:`~django.forms.URLField` use the new type attributes available in
HTML5 (type='email', type='url').
Backwards incompatible changes in 1.6 Backwards incompatible changes in 1.6
===================================== =====================================
@ -44,7 +45,7 @@ Backwards incompatible changes in 1.6
* If your CSS/Javascript code used to access HTML input widgets by type, you * 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 should review it as ``type='text'`` widgets might be now output as
``type='email'`` depending on their corresponding field type. ``type='email'`` or ``type='url'`` depending on their corresponding field type.
.. warning:: .. warning::

View File

@ -639,6 +639,7 @@ class FieldsTests(SimpleTestCase):
def test_urlfield_1(self): def test_urlfield_1(self):
f = URLField() f = URLField()
self.assertWidgetRendersTo(f, '<input type="url" name="f" id="id_f" />')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '')
self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None)
self.assertEqual('http://localhost/', f.clean('http://localhost')) self.assertEqual('http://localhost/', f.clean('http://localhost'))
@ -690,6 +691,7 @@ class FieldsTests(SimpleTestCase):
def test_urlfield_5(self): def test_urlfield_5(self):
f = URLField(min_length=15, max_length=20) f = URLField(min_length=15, max_length=20)
self.assertWidgetRendersTo(f, '<input id="id_f" type="url" name="f" maxlength="20" />')
self.assertRaisesMessage(ValidationError, "'Ensure this value has at least 15 characters (it has 13).'", f.clean, 'http://f.com') self.assertRaisesMessage(ValidationError, "'Ensure this value has at least 15 characters (it has 13).'", f.clean, 'http://f.com')
self.assertEqual('http://example.com/', f.clean('http://example.com')) self.assertEqual('http://example.com/', f.clean('http://example.com'))
self.assertRaisesMessage(ValidationError, "'Ensure this value has at most 20 characters (it has 38).'", f.clean, 'http://abcdefghijklmnopqrstuvwxyz.com') self.assertRaisesMessage(ValidationError, "'Ensure this value has at most 20 characters (it has 38).'", f.clean, 'http://abcdefghijklmnopqrstuvwxyz.com')

View File

@ -102,22 +102,22 @@ class GenericAdminViewTest(TestCase):
# Works with no queryset # Works with no queryset
formset = EpisodeMediaFormSet(instance=e) formset = EpisodeMediaFormSet(instance=e)
self.assertEqual(len(formset.forms), 5) self.assertEqual(len(formset.forms), 5)
self.assertHTMLEqual(formset.forms[0].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-0-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-0-url" type="text" name="generic_inline_admin-media-content_type-object_id-0-url" value="http://example.com/podcast.mp3" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-0-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-0-id" /></p>' % self.mp3_media_pk) self.assertHTMLEqual(formset.forms[0].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-0-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-0-url" type="url" name="generic_inline_admin-media-content_type-object_id-0-url" value="http://example.com/podcast.mp3" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-0-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-0-id" /></p>' % self.mp3_media_pk)
self.assertHTMLEqual(formset.forms[1].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-1-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-1-url" type="text" name="generic_inline_admin-media-content_type-object_id-1-url" value="http://example.com/logo.png" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-1-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-1-id" /></p>' % self.png_media_pk) self.assertHTMLEqual(formset.forms[1].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-1-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-1-url" type="url" name="generic_inline_admin-media-content_type-object_id-1-url" value="http://example.com/logo.png" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-1-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-1-id" /></p>' % self.png_media_pk)
self.assertHTMLEqual(formset.forms[2].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-2-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-2-url" type="text" name="generic_inline_admin-media-content_type-object_id-2-url" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-2-id" id="id_generic_inline_admin-media-content_type-object_id-2-id" /></p>') self.assertHTMLEqual(formset.forms[2].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-2-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-2-url" type="url" name="generic_inline_admin-media-content_type-object_id-2-url" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-2-id" id="id_generic_inline_admin-media-content_type-object_id-2-id" /></p>')
# A queryset can be used to alter display ordering # A queryset can be used to alter display ordering
formset = EpisodeMediaFormSet(instance=e, queryset=Media.objects.order_by('url')) formset = EpisodeMediaFormSet(instance=e, queryset=Media.objects.order_by('url'))
self.assertEqual(len(formset.forms), 5) self.assertEqual(len(formset.forms), 5)
self.assertHTMLEqual(formset.forms[0].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-0-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-0-url" type="text" name="generic_inline_admin-media-content_type-object_id-0-url" value="http://example.com/logo.png" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-0-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-0-id" /></p>' % self.png_media_pk) self.assertHTMLEqual(formset.forms[0].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-0-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-0-url" type="url" name="generic_inline_admin-media-content_type-object_id-0-url" value="http://example.com/logo.png" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-0-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-0-id" /></p>' % self.png_media_pk)
self.assertHTMLEqual(formset.forms[1].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-1-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-1-url" type="text" name="generic_inline_admin-media-content_type-object_id-1-url" value="http://example.com/podcast.mp3" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-1-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-1-id" /></p>' % self.mp3_media_pk) self.assertHTMLEqual(formset.forms[1].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-1-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-1-url" type="url" name="generic_inline_admin-media-content_type-object_id-1-url" value="http://example.com/podcast.mp3" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-1-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-1-id" /></p>' % self.mp3_media_pk)
self.assertHTMLEqual(formset.forms[2].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-2-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-2-url" type="text" name="generic_inline_admin-media-content_type-object_id-2-url" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-2-id" id="id_generic_inline_admin-media-content_type-object_id-2-id" /></p>') self.assertHTMLEqual(formset.forms[2].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-2-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-2-url" type="url" name="generic_inline_admin-media-content_type-object_id-2-url" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-2-id" id="id_generic_inline_admin-media-content_type-object_id-2-id" /></p>')
# Works with a queryset that omits items # Works with a queryset that omits items
formset = EpisodeMediaFormSet(instance=e, queryset=Media.objects.filter(url__endswith=".png")) formset = EpisodeMediaFormSet(instance=e, queryset=Media.objects.filter(url__endswith=".png"))
self.assertEqual(len(formset.forms), 4) self.assertEqual(len(formset.forms), 4)
self.assertHTMLEqual(formset.forms[0].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-0-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-0-url" type="text" name="generic_inline_admin-media-content_type-object_id-0-url" value="http://example.com/logo.png" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-0-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-0-id" /></p>' % self.png_media_pk) self.assertHTMLEqual(formset.forms[0].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-0-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-0-url" type="url" name="generic_inline_admin-media-content_type-object_id-0-url" value="http://example.com/logo.png" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-0-id" value="%s" id="id_generic_inline_admin-media-content_type-object_id-0-id" /></p>' % self.png_media_pk)
self.assertHTMLEqual(formset.forms[1].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-1-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-1-url" type="text" name="generic_inline_admin-media-content_type-object_id-1-url" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-1-id" id="id_generic_inline_admin-media-content_type-object_id-1-id" /></p>') self.assertHTMLEqual(formset.forms[1].as_p(), '<p><label for="id_generic_inline_admin-media-content_type-object_id-1-url">Url:</label> <input id="id_generic_inline_admin-media-content_type-object_id-1-url" type="url" name="generic_inline_admin-media-content_type-object_id-1-url" maxlength="200" /><input type="hidden" name="generic_inline_admin-media-content_type-object_id-1-id" id="id_generic_inline_admin-media-content_type-object_id-1-id" /></p>')
def testGenericInlineFormsetFactory(self): def testGenericInlineFormsetFactory(self):
# Regression test for #10522. # Regression test for #10522.