diff --git a/django/forms/fields.py b/django/forms/fields.py index 8a3dbfeb49..9fbbce107c 100644 --- a/django/forms/fields.py +++ b/django/forms/fields.py @@ -19,7 +19,7 @@ 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, EmailInput, HiddenInput, + TextInput, PasswordInput, EmailInput, URLInput, HiddenInput, MultipleHiddenInput, ClearableFileInput, CheckboxInput, Select, NullBooleanSelect, SelectMultiple, DateInput, DateTimeInput, TimeInput, SplitDateTimeWidget, SplitHiddenDateTimeWidget, FILE_INPUT_CONTRADICTION @@ -612,6 +612,7 @@ class ImageField(FileField): return f class URLField(CharField): + widget = URLInput default_error_messages = { 'invalid': _('Enter a valid URL.'), } diff --git a/django/forms/widgets.py b/django/forms/widgets.py index f201e914dd..e906ed5bc6 100644 --- a/django/forms/widgets.py +++ b/django/forms/widgets.py @@ -22,7 +22,8 @@ from django.utils.safestring import mark_safe from django.utils import datetime_safe, formats, six __all__ = ( - 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', 'EmailInput', 'PasswordInput', + 'Media', 'MediaDefiningClass', 'Widget', 'TextInput', + 'EmailInput', 'URLInput', 'PasswordInput', 'HiddenInput', 'MultipleHiddenInput', 'ClearableFileInput', 'FileInput', 'DateInput', 'DateTimeInput', 'TimeInput', 'Textarea', 'CheckboxInput', 'Select', 'NullBooleanSelect', 'SelectMultiple', 'RadioSelect', @@ -255,6 +256,10 @@ class EmailInput(TextInput): input_type = 'email' +class URLInput(TextInput): + input_type = 'url' + + class PasswordInput(TextInput): input_type = 'password' diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 44e4684c1d..4c5c275806 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -161,7 +161,7 @@ precedence:: >>> f = CommentForm(initial={'name': 'instance'}, auto_id=False) >>> print(f) Name: - Url: + Url: Comment: Accessing "clean" data diff --git a/docs/ref/forms/fields.txt b/docs/ref/forms/fields.txt index 8bfe77cc20..2e4e779f0c 100644 --- a/docs/ref/forms/fields.txt +++ b/docs/ref/forms/fields.txt @@ -112,7 +112,7 @@ We've specified ``auto_id=False`` to simplify the output:: >>> f = CommentForm(auto_id=False) >>> print(f) Your name: - Your Web site: + Your Web site: Comment: ``initial`` @@ -135,7 +135,7 @@ field is initialized to a particular value. For example:: >>> f = CommentForm(auto_id=False) >>> print(f) Name: - Url: + Url: Comment: 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) >>> print(f) Name: - Url: + Url: Comment: 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) - * Default widget: :class:`TextInput` + * Default widget: :class:`URLInput` * Empty value: ``''`` (an empty string) * Normalizes to: A Unicode object. * Validates that the given value is a valid URL. diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt index 9105a41b25..cb5224fd3c 100644 --- a/docs/ref/forms/widgets.txt +++ b/docs/ref/forms/widgets.txt @@ -139,7 +139,7 @@ provided for each widget will be rendered exactly the same:: >>> f = CommentForm(auto_id=False) >>> f.as_table() Name: - Url: + Url: Comment: 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.as_table() Name: - Url: + Url: Comment: .. _styling-widget-classes: @@ -403,6 +403,15 @@ These widgets make use of the HTML elements ``input`` and ``textarea``. Text input: ```` +``URLInput`` +~~~~~~~~~~~~ + +.. class:: URLInput + + .. versionadded:: 1.6 + + Text input: ```` + ``PasswordInput`` ~~~~~~~~~~~~~~~~~ diff --git a/docs/releases/1.6.txt b/docs/releases/1.6.txt index 03341e586b..e0c07c40fe 100644 --- a/docs/releases/1.6.txt +++ b/docs/releases/1.6.txt @@ -31,8 +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'). +* The default widgets for :class:`~django.forms.EmailField` and + :class:`~django.forms.URLField` use the new type attributes available in + HTML5 (type='email', type='url'). 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 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:: diff --git a/tests/regressiontests/forms/tests/fields.py b/tests/regressiontests/forms/tests/fields.py index aa03377d44..fc7fc70da4 100644 --- a/tests/regressiontests/forms/tests/fields.py +++ b/tests/regressiontests/forms/tests/fields.py @@ -639,6 +639,7 @@ class FieldsTests(SimpleTestCase): def test_urlfield_1(self): f = URLField() + self.assertWidgetRendersTo(f, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, '') self.assertRaisesMessage(ValidationError, "'This field is required.'", f.clean, None) self.assertEqual('http://localhost/', f.clean('http://localhost')) @@ -690,6 +691,7 @@ class FieldsTests(SimpleTestCase): def test_urlfield_5(self): f = URLField(min_length=15, max_length=20) + self.assertWidgetRendersTo(f, '') 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.assertRaisesMessage(ValidationError, "'Ensure this value has at most 20 characters (it has 38).'", f.clean, 'http://abcdefghijklmnopqrstuvwxyz.com') diff --git a/tests/regressiontests/generic_inline_admin/tests.py b/tests/regressiontests/generic_inline_admin/tests.py index fea30b4946..f03641d292 100644 --- a/tests/regressiontests/generic_inline_admin/tests.py +++ b/tests/regressiontests/generic_inline_admin/tests.py @@ -102,22 +102,22 @@ class GenericAdminViewTest(TestCase): # Works with no queryset formset = EpisodeMediaFormSet(instance=e) self.assertEqual(len(formset.forms), 5) - self.assertHTMLEqual(formset.forms[0].as_p(), '

' % self.mp3_media_pk) - self.assertHTMLEqual(formset.forms[1].as_p(), '

' % self.png_media_pk) - self.assertHTMLEqual(formset.forms[2].as_p(), '

') + self.assertHTMLEqual(formset.forms[0].as_p(), '

' % self.mp3_media_pk) + self.assertHTMLEqual(formset.forms[1].as_p(), '

' % self.png_media_pk) + self.assertHTMLEqual(formset.forms[2].as_p(), '

') # A queryset can be used to alter display ordering formset = EpisodeMediaFormSet(instance=e, queryset=Media.objects.order_by('url')) self.assertEqual(len(formset.forms), 5) - self.assertHTMLEqual(formset.forms[0].as_p(), '

' % self.png_media_pk) - self.assertHTMLEqual(formset.forms[1].as_p(), '

' % self.mp3_media_pk) - self.assertHTMLEqual(formset.forms[2].as_p(), '

') + self.assertHTMLEqual(formset.forms[0].as_p(), '

' % self.png_media_pk) + self.assertHTMLEqual(formset.forms[1].as_p(), '

' % self.mp3_media_pk) + self.assertHTMLEqual(formset.forms[2].as_p(), '

') # Works with a queryset that omits items formset = EpisodeMediaFormSet(instance=e, queryset=Media.objects.filter(url__endswith=".png")) self.assertEqual(len(formset.forms), 4) - self.assertHTMLEqual(formset.forms[0].as_p(), '

' % self.png_media_pk) - self.assertHTMLEqual(formset.forms[1].as_p(), '

') + self.assertHTMLEqual(formset.forms[0].as_p(), '

' % self.png_media_pk) + self.assertHTMLEqual(formset.forms[1].as_p(), '

') def testGenericInlineFormsetFactory(self): # Regression test for #10522.