Permalink
Browse files

Fixed #22383 -- Added support for HTML5 required attribute on require…

…d form fields.
  • Loading branch information...
1 parent 4d1c229 commit ec6121693f112ae33b653b4364e812722d2eb567 @jdufresne jdufresne committed with timgraham Mar 28, 2016
@@ -85,6 +85,8 @@ def as_widget(self, widget=None, attrs=None, only_initial=False):
widget.is_localized = True
attrs = attrs or {}
+ if not widget.is_hidden and self.field.required and self.form.use_required_attribute:
+ attrs['required'] = True
if self.field.disabled:
attrs['disabled'] = True
auto_id = self.auto_id
View
@@ -67,10 +67,11 @@ class BaseForm(object):
# class, not to the Form class.
field_order = None
prefix = None
+ use_required_attribute = True
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
- empty_permitted=False, field_order=None):
+ empty_permitted=False, field_order=None, use_required_attribute=None):
self.is_bound = data is not None or files is not None
self.data = data or {}
self.files = files or {}
@@ -93,6 +94,9 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
self._bound_fields_cache = {}
self.order_fields(self.field_order if field_order is None else field_order)
+ if use_required_attribute is not None:
+ self.use_required_attribute = use_required_attribute
+
def order_fields(self, field_order):
"""
Rearranges the fields according to field_order.
View
@@ -161,6 +161,10 @@ def _construct_form(self, i, **kwargs):
'auto_id': self.auto_id,
'prefix': self.add_prefix(i),
'error_class': self.error_class,
+ # Don't render the HTML 'required' attribute as it may cause
+ # incorrect validation for extra, optional, and deleted
+ # forms in the formset.
+ 'use_required_attribute': False,
}
if self.is_bound:
defaults['data'] = self.data
@@ -195,6 +199,7 @@ def empty_form(self):
auto_id=self.auto_id,
prefix=self.add_prefix('__prefix__'),
empty_permitted=True,
+ use_required_attribute=False,
**self.get_form_kwargs(None)
)
self.add_fields(form, None)
View
@@ -278,7 +278,7 @@ def __new__(mcs, name, bases, attrs):
class BaseModelForm(BaseForm):
def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
initial=None, error_class=ErrorList, label_suffix=None,
- empty_permitted=False, instance=None):
+ empty_permitted=False, instance=None, use_required_attribute=None):
opts = self._meta
if opts.model is None:
raise ValueError('ModelForm has no model class specified.')
@@ -296,8 +296,10 @@ def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None,
# It is False by default so overriding self.clean() and failing to call
# super will stop validate_unique from being called.
self._validate_unique = False
- super(BaseModelForm, self).__init__(data, files, auto_id, prefix, object_data,
- error_class, label_suffix, empty_permitted)
+ super(BaseModelForm, self).__init__(
+ data, files, auto_id, prefix, object_data, error_class,
+ label_suffix, empty_permitted, use_required_attribute=use_required_attribute,
+ )
# Apply ``limit_choices_to`` to each field.
for field_name in self.fields:
formfield = self.fields[field_name]
View

Large diffs are not rendered by default.

Oops, something went wrong.
View
@@ -115,9 +115,9 @@ We've specified ``auto_id=False`` to simplify the output::
... comment = forms.CharField()
>>> f = CommentForm(auto_id=False)
>>> print(f)
- <tr><th>Your name:</th><td><input type="text" name="name" /></td></tr>
- <tr><th>Your website:</th><td><input type="url" name="url" /></td></tr>
- <tr><th>Comment:</th><td><input type="text" name="comment" /></td></tr>
+ <tr><th>Your name:</th><td><input type="text" name="name" required /></td></tr>
+ <tr><th>Your website:</th><td><input type="url" name="url" required /></td></tr>
+ <tr><th>Comment:</th><td><input type="text" name="comment" required /></td></tr>
``label_suffix``
----------------
@@ -133,9 +133,9 @@ The ``label_suffix`` argument lets you override the form's
... captcha_answer = forms.IntegerField(label='2 + 2', label_suffix=' =')
>>> f = ContactForm(label_suffix='?')
>>> print(f.as_p())
- <p><label for="id_age">Age?</label> <input id="id_age" name="age" type="number" /></p>
- <p><label for="id_nationality">Nationality?</label> <input id="id_nationality" name="nationality" type="text" /></p>
- <p><label for="id_captcha_answer">2 + 2 =</label> <input id="id_captcha_answer" name="captcha_answer" type="number" /></p>
+ <p><label for="id_age">Age?</label> <input id="id_age" name="age" type="number" required /></p>
+ <p><label for="id_nationality">Nationality?</label> <input id="id_nationality" name="nationality" type="text" required /></p>
+ <p><label for="id_captcha_answer">2 + 2 =</label> <input id="id_captcha_answer" name="captcha_answer" type="number" required /></p>
``initial``
-----------
@@ -157,9 +157,9 @@ field is initialized to a particular value. For example::
... comment = forms.CharField()
>>> f = CommentForm(auto_id=False)
>>> print(f)
- <tr><th>Name:</th><td><input type="text" name="name" value="Your name" /></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>Name:</th><td><input type="text" name="name" value="Your name" required /></td></tr>
+ <tr><th>Url:</th><td><input type="url" name="url" value="http://" required /></td></tr>
+ <tr><th>Comment:</th><td><input type="text" name="comment" required /></td></tr>
You may be thinking, why not just pass a dictionary of the initial values as
data when displaying the form? Well, if you do that, you'll trigger validation,
@@ -172,9 +172,9 @@ and the HTML output will include any validation errors::
>>> default_data = {'name': 'Your name', 'url': 'http://'}
>>> f = CommentForm(default_data, auto_id=False)
>>> print(f)
- <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="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>Name:</th><td><input type="text" name="name" value="Your name" required /></td></tr>
+ <tr><th>Url:</th><td><ul class="errorlist"><li>Enter a valid URL.</li></ul><input type="url" name="url" value="http://" required /></td></tr>
+ <tr><th>Comment:</th><td><ul class="errorlist"><li>This field is required.</li></ul><input type="text" name="comment" required /></td></tr>
This is why ``initial`` values are only displayed for unbound forms. For bound
forms, the HTML output will use the bound data.
@@ -201,7 +201,7 @@ Instead of a constant, you can also pass any callable::
>>> class DateForm(forms.Form):
... day = forms.DateField(initial=datetime.date.today)
>>> print(DateForm())
- <tr><th>Day:</th><td><input type="text" name="day" value="12/23/2008" /><td></tr>
+ <tr><th>Day:</th><td><input type="text" name="day" value="12/23/2008" required /><td></tr>
The callable will be evaluated only when the unbound form is displayed, not when it is defined.
@@ -237,19 +237,19 @@ fields. We've specified ``auto_id=False`` to simplify the output::
... cc_myself = forms.BooleanField(required=False)
>>> f = HelpTextContactForm(auto_id=False)
>>> print(f.as_table())
- <tr><th>Subject:</th><td><input type="text" name="subject" maxlength="100" /><br /><span class="helptext">100 characters max.</span></td></tr>
- <tr><th>Message:</th><td><input type="text" name="message" /></td></tr>
- <tr><th>Sender:</th><td><input type="email" name="sender" /><br />A valid email address, please.</td></tr>
+ <tr><th>Subject:</th><td><input type="text" name="subject" maxlength="100" required /><br /><span class="helptext">100 characters max.</span></td></tr>
+ <tr><th>Message:</th><td><input type="text" name="message" required /></td></tr>
+ <tr><th>Sender:</th><td><input type="email" name="sender" required /><br />A valid email address, please.</td></tr>
<tr><th>Cc myself:</th><td><input type="checkbox" name="cc_myself" /></td></tr>
>>> print(f.as_ul()))
- <li>Subject: <input type="text" name="subject" maxlength="100" /> <span class="helptext">100 characters max.</span></li>
- <li>Message: <input type="text" name="message" /></li>
- <li>Sender: <input type="email" name="sender" /> A valid email address, please.</li>
+ <li>Subject: <input type="text" name="subject" maxlength="100" required /> <span class="helptext">100 characters max.</span></li>
+ <li>Message: <input type="text" name="message" required /></li>
+ <li>Sender: <input type="email" name="sender" required /> A valid email address, please.</li>
<li>Cc myself: <input type="checkbox" name="cc_myself" /></li>
>>> print(f.as_p())
- <p>Subject: <input type="text" name="subject" maxlength="100" /> <span class="helptext">100 characters max.</span></p>
- <p>Message: <input type="text" name="message" /></p>
- <p>Sender: <input type="email" name="sender" /> A valid email address, please.</p>
+ <p>Subject: <input type="text" name="subject" maxlength="100" required /> <span class="helptext">100 characters max.</span></p>
+ <p>Message: <input type="text" name="message" required /></p>
+ <p>Sender: <input type="email" name="sender" required /> A valid email address, please.</p>
<p>Cc myself: <input type="checkbox" name="cc_myself" /></p>
``error_messages``
View
@@ -135,9 +135,9 @@ provided for each widget will be rendered exactly the same::
>>> f = CommentForm(auto_id=False)
>>> f.as_table()
- <tr><th>Name:</th><td><input type="text" name="name" /></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>Name:</th><td><input type="text" name="name" required /></td></tr>
+ <tr><th>Url:</th><td><input type="url" name="url" required /></td></tr>
+ <tr><th>Comment:</th><td><input type="text" name="comment" required /></td></tr>
On a real Web page, you probably don't want every widget to look the same. You
might want a larger input element for the comment, and you might want the
@@ -154,9 +154,9 @@ Django will then include the extra attributes in the rendered output:
>>> f = CommentForm(auto_id=False)
>>> f.as_table()
- <tr><th>Name:</th><td><input type="text" name="name" class="special"/></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>Name:</th><td><input type="text" name="name" class="special" required /></td></tr>
+ <tr><th>Url:</th><td><input type="url" name="url" required /></td></tr>
+ <tr><th>Comment:</th><td><input type="text" name="comment" size="40" required /></td></tr>
You can also set the HTML ``id`` using :attr:`~Widget.attrs`. See
:attr:`BoundField.id_for_label` for an example.
@@ -204,7 +204,7 @@ foundation for custom widgets.
>>> from django import forms
>>> name = forms.TextInput(attrs={'size': 10, 'title': 'Your name',})
>>> name.render('name', 'A name')
- '<input title="Your name" type="text" name="name" value="A name" size="10" />'
+ '<input title="Your name" type="text" name="name" value="A name" size="10" required />'
If you assign a value of ``True`` or ``False`` to an attribute,
it will be rendered as an HTML5 boolean attribute::
@@ -627,16 +627,16 @@ Selector and checkbox widgets
.. code-block:: html
<div class="myradio">
- <label for="id_beatles_0"><input id="id_beatles_0" name="beatles" type="radio" value="john" /> John</label>
+ <label for="id_beatles_0"><input id="id_beatles_0" name="beatles" type="radio" value="john" required /> John</label>
</div>
<div class="myradio">
- <label for="id_beatles_1"><input id="id_beatles_1" name="beatles" type="radio" value="paul" /> Paul</label>
+ <label for="id_beatles_1"><input id="id_beatles_1" name="beatles" type="radio" value="paul" required /> Paul</label>
</div>
<div class="myradio">
- <label for="id_beatles_2"><input id="id_beatles_2" name="beatles" type="radio" value="george" /> George</label>
+ <label for="id_beatles_2"><input id="id_beatles_2" name="beatles" type="radio" value="george" required /> George</label>
</div>
<div class="myradio">
- <label for="id_beatles_3"><input id="id_beatles_3" name="beatles" type="radio" value="ringo" /> Ringo</label>
+ <label for="id_beatles_3"><input id="id_beatles_3" name="beatles" type="radio" value="ringo" required /> Ringo</label>
</div>
That included the ``<label>`` tags. To get more granular, you can use each
@@ -658,22 +658,22 @@ Selector and checkbox widgets
<label for="id_beatles_0">
John
- <span class="radio"><input id="id_beatles_0" name="beatles" type="radio" value="john" /></span>
+ <span class="radio"><input id="id_beatles_0" name="beatles" type="radio" value="john" required /></span>
</label>
<label for="id_beatles_1">
Paul
- <span class="radio"><input id="id_beatles_1" name="beatles" type="radio" value="paul" /></span>
+ <span class="radio"><input id="id_beatles_1" name="beatles" type="radio" value="paul" required /></span>
</label>
<label for="id_beatles_2">
George
- <span class="radio"><input id="id_beatles_2" name="beatles" type="radio" value="george" /></span>
+ <span class="radio"><input id="id_beatles_2" name="beatles" type="radio" value="george" required /></span>
</label>
<label for="id_beatles_3">
Ringo
- <span class="radio"><input id="id_beatles_3" name="beatles" type="radio" value="ringo" /></span>
+ <span class="radio"><input id="id_beatles_3" name="beatles" type="radio" value="ringo" required /></span>
</label>
If you decide not to loop over the radio buttons -- e.g., if your template
View
@@ -259,6 +259,12 @@ Forms
* The ``<input>`` tag rendered by :class:`~django.forms.CharField` now includes
a ``minlength`` attribute if the field has a ``min_length``.
+* Required form fields now have the ``required`` HTML attribute. Set the new
+ :attr:`Form.use_required_attribute <django.forms.Form.use_required_attribute>`
+ attribute to ``False`` to disable it. The ``required`` attribute isn't
+ included on forms of formsets because the browser validation may not be
+ correct when adding and deleting formsets.
+
Generic Views
~~~~~~~~~~~~~
@@ -760,6 +766,11 @@ Miscellaneous
:attr:`ModelAdmin.save_as_continue
<django.contrib.admin.ModelAdmin.save_as_continue>` attribute to ``False``.
+* Required form fields now have the ``required`` HTML attribute. Set the
+ :attr:`Form.use_required_attribute <django.forms.Form.use_required_attribute>`
+ attribute to ``False`` to disable it. You could also add the ``novalidate``
+ attribute to ``<form>`` if you don't want browser validation.
+
.. _deprecated-features-1.10:
Features deprecated in 1.10
@@ -164,6 +164,11 @@ As we can see, ``formset.errors`` is a list whose entries correspond to the
forms in the formset. Validation was performed for each of the two forms, and
the expected error message appears for the second item.
+Just like when using a normal ``Form``, each form in the formset may include
+HTML attributes such as ``maxlength`` for browser validation. However, forms of
+formsets won't include the ``required`` attribute as that validation may be
+incorrect when adding and deleting forms.
+
.. method:: BaseFormSet.total_error_count()
To check how many errors there are in the formset, we can use the
@@ -259,7 +259,7 @@ The whole form, when rendered for the first time, will look like:
.. code-block:: html+django
<label for="your_name">Your name: </label>
- <input id="your_name" type="text" name="your_name" maxlength="100">
+ <input id="your_name" type="text" name="your_name" maxlength="100" required />
Note that it **does not** include the ``<form>`` tags, or a submit button.
We'll have to provide those ourselves in the template.
@@ -512,11 +512,11 @@ Here's the output of ``{{ form.as_p }}`` for our ``ContactForm`` instance:
.. code-block:: html+django
<p><label for="id_subject">Subject:</label>
- <input id="id_subject" type="text" name="subject" maxlength="100" /></p>
+ <input id="id_subject" type="text" name="subject" maxlength="100" required /></p>
<p><label for="id_message">Message:</label>
- <textarea name="message" id="id_message"></textarea></p>
+ <textarea name="message" id="id_message" required></textarea></p>
<p><label for="id_sender">Sender:</label>
- <input type="email" name="sender" id="id_sender" /></p>
+ <input type="email" name="sender" id="id_sender" required /></p>
<p><label for="id_cc_myself">Cc myself:</label>
<input type="checkbox" name="cc_myself" id="id_cc_myself" /></p>
@@ -3291,7 +3291,7 @@ def test_actions_ordering(self):
Refs #15964.
"""
response = self.client.get(reverse('admin:admin_views_externalsubscriber_changelist'))
- self.assertContains(response, '''<label>Action: <select name="action">
+ self.assertContains(response, '''<label>Action: <select name="action" required>
<option value="" selected="selected">---------</option>
<option value="delete_selected">Delete selected external
subscribers</option>
@@ -119,4 +119,4 @@ def test_charfield_strip(self):
def test_charfield_disabled(self):
f = CharField(disabled=True)
- self.assertWidgetRendersTo(f, '<input type="text" name="f" id="id_f" disabled />')
+ self.assertWidgetRendersTo(f, '<input type="text" name="f" id="id_f" disabled required />')
@@ -81,6 +81,6 @@ def test_choicefield_disabled(self):
f = ChoiceField(choices=[('J', 'John'), ('P', 'Paul')], disabled=True)
self.assertWidgetRendersTo(
f,
- '<select id="id_f" name="f" disabled><option value="J">John</option>'
+ '<select id="id_f" name="f" disabled required><option value="J">John</option>'
'<option value="P">Paul</option></select>'
)
Oops, something went wrong.

0 comments on commit ec61216

Please sign in to comment.