0
0
mirror of https://github.com/django/django.git synced 2024-11-28 21:43:13 +01:00

Refs #32339 -- Deprecated default.html form template.

Co-authored-by: Carlton Gibson <carlton.gibson@noumenal.es>
This commit is contained in:
David Smith 2022-05-05 14:26:09 +02:00 committed by Carlton Gibson
parent 6af8673255
commit d126eba363
12 changed files with 337 additions and 176 deletions

View File

@ -15,6 +15,9 @@ def get_default_renderer():
class BaseRenderer: class BaseRenderer:
# RemovedInDjango50Warning: When the deprecation ends, replace with
# form_template_name = "django/forms/div.html"
# formset_template_name = "django/forms/formsets/div.html"
form_template_name = "django/forms/default.html" form_template_name = "django/forms/default.html"
formset_template_name = "django/forms/formsets/default.html" formset_template_name = "django/forms/formsets/default.html"
@ -64,6 +67,31 @@ class Jinja2(EngineMixin, BaseRenderer):
return Jinja2 return Jinja2
class DjangoDivFormRenderer(DjangoTemplates):
"""
Load Django templates from django/forms/templates and from apps'
'templates' directory and use the 'div.html' template to render forms and
formsets.
"""
# RemovedInDjango50Warning Deprecate this class in 5.0 and remove in 6.0.
form_template_name = "django/forms/div.html"
formset_template_name = "django/forms/formsets/div.html"
class Jinja2DivFormRenderer(Jinja2):
"""
Load Jinja2 templates from the built-in widget templates in
django/forms/jinja2 and from apps' 'jinja2' directory.
"""
# RemovedInDjango50Warning Deprecate this class in 5.0 and remove in 6.0.
form_template_name = "django/forms/div.html"
formset_template_name = "django/forms/formsets/div.html"
class TemplatesSetting(BaseRenderer): class TemplatesSetting(BaseRenderer):
""" """
Load templates using template.loader.get_template() which is configured Load templates using template.loader.get_template() which is configured

View File

@ -1,13 +1,16 @@
import json import json
import warnings
from collections import UserList from collections import UserList
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.forms.renderers import get_default_renderer from django.forms.renderers import get_default_renderer
from django.utils import timezone from django.utils import timezone
from django.utils.deprecation import RemovedInDjango50Warning
from django.utils.html import escape, format_html_join from django.utils.html import escape, format_html_join
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.utils.version import get_docs_version
def pretty_name(name): def pretty_name(name):
@ -42,6 +45,16 @@ def flatatt(attrs):
) )
DEFAULT_TEMPLATE_DEPRECATION_MSG = (
'The "default.html" templates for forms and formsets will be removed. These were '
'proxies to the equivalent "table.html" templates, but the new "div.html" '
"templates will be the default from Django 5.0. Transitional renderers are "
"provided to allow you to opt-in to the new output style now. See "
"https://docs.djangoproject.com/en/%s/releases/4.1/ for more details"
% get_docs_version()
)
class RenderableMixin: class RenderableMixin:
def get_context(self): def get_context(self):
raise NotImplementedError( raise NotImplementedError(
@ -49,12 +62,17 @@ class RenderableMixin:
) )
def render(self, template_name=None, context=None, renderer=None): def render(self, template_name=None, context=None, renderer=None):
return mark_safe( renderer = renderer or self.renderer
(renderer or self.renderer).render( template = template_name or self.template_name
template_name or self.template_name, context = context or self.get_context()
context or self.get_context(), if (
template == "django/forms/default.html"
or template == "django/forms/formsets/default.html"
):
warnings.warn(
DEFAULT_TEMPLATE_DEPRECATION_MSG, RemovedInDjango50Warning, stacklevel=2
) )
) return mark_safe(renderer.render(template, context))
__str__ = render __str__ = render
__html__ = render __html__ = render

View File

@ -105,6 +105,9 @@ details on these changes.
* The ``django.contrib.auth.hashers.CryptPasswordHasher`` will be removed. * The ``django.contrib.auth.hashers.CryptPasswordHasher`` will be removed.
* The ``"django/forms/default.html"`` and
``"django/forms/formsets/default.html"`` templates will be removed.
* The ability to pass ``nulls_first=False`` or ``nulls_last=False`` to * The ability to pass ``nulls_first=False`` or ``nulls_last=False`` to
``Expression.asc()`` and ``Expression.desc()`` methods, and the ``OrderBy`` ``Expression.asc()`` and ``Expression.desc()`` methods, and the ``OrderBy``
expression will be removed. expression will be removed.

View File

@ -56,6 +56,12 @@ should return a rendered templates (as a string) or raise
Defaults to ``"django/forms/default.html"``, which is a proxy for Defaults to ``"django/forms/default.html"``, which is a proxy for
``"django/forms/table.html"``. ``"django/forms/table.html"``.
.. deprecated:: 4.1
The ``"django/forms/default.html"`` template is deprecated and will be
removed in Django 5.0. The default will become
``"django/forms/default.html"`` at that time.
.. attribute:: formset_template_name .. attribute:: formset_template_name
.. versionadded:: 4.1 .. versionadded:: 4.1
@ -65,6 +71,12 @@ should return a rendered templates (as a string) or raise
Defaults to ``"django/forms/formsets/default.html"``, which is a proxy Defaults to ``"django/forms/formsets/default.html"``, which is a proxy
for ``"django/forms/formsets/table.html"``. for ``"django/forms/formsets/table.html"``.
.. deprecated:: 4.1
The ``"django/forms/formset/default.html"`` template is deprecated and
will be removed in Django 5.0. The default will become
``"django/forms/formset/div.html"`` template.
.. method:: get_template(template_name) .. method:: get_template(template_name)
Subclasses must implement this method with the appropriate template Subclasses must implement this method with the appropriate template
@ -97,6 +109,26 @@ If you want to render templates with customizations from your
:setting:`TEMPLATES` setting, such as context processors for example, use the :setting:`TEMPLATES` setting, such as context processors for example, use the
:class:`TemplatesSetting` renderer. :class:`TemplatesSetting` renderer.
.. class:: DjangoDivFormRenderer
.. versionadded:: 4.1
Subclass of :class:`DjangoTemplates` that specifies
:attr:`~BaseRenderer.form_template_name` and
:attr:`~BaseRenderer.formset_template_name` as ``"django/forms/div.html"`` and
``"django/forms/formset/div.html"`` respectively.
This is a transitional renderer for opt-in to the new ``<div>`` based
templates, which are the default from Django 5.0.
Apply this via the :setting:`FORM_RENDERER` setting::
FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer"
Once the ``<div>`` templates are the default, this transitional renderer will
be deprecated, for removal in Django 6.0. The ``FORM_RENDERER`` declaration can
be removed at that time.
``Jinja2`` ``Jinja2``
---------- ----------
@ -113,6 +145,17 @@ templates for widgets that don't have any, you can't use this renderer. For
example, :mod:`django.contrib.admin` doesn't include Jinja2 templates for its example, :mod:`django.contrib.admin` doesn't include Jinja2 templates for its
widgets due to their usage of Django template tags. widgets due to their usage of Django template tags.
.. class:: Jinja2DivFormRenderer
.. versionadded:: 4.1
A transitional renderer as per :class:`DjangoDivFormRenderer` above, but
subclassing :class:`Jinja2` for use with the Jinja2 backend.
Apply this via the :setting:`FORM_RENDERER` setting::
FORM_RENDERER = "django.forms.renderers.Jinja2DivFormRenderer"
``TemplatesSetting`` ``TemplatesSetting``
-------------------- --------------------

View File

@ -74,6 +74,24 @@ Validation of Constraints
in the :attr:`Meta.constraints <django.db.models.Options.constraints>` option in the :attr:`Meta.constraints <django.db.models.Options.constraints>` option
are now checked during :ref:`model validation <validating-objects>`. are now checked during :ref:`model validation <validating-objects>`.
Form rendering accessibility
----------------------------
In order to aid users with screen readers, and other assistive technology, new
``<div>`` based form templates are available from this release. These provide
more accessible navigation than the older templates, and are able to correctly
group related controls, such as radio-lists, into fieldsets.
The new templates are recommended, and will become the default form rendering
style when outputting a form, like ``{{ form }}`` in a template, from Django
5.0.
In order to ease adopting the new output style, the default form and formset
templates are now configurable at the project level via the
:setting:`FORM_RENDERER` setting.
See :ref:`the Forms section (below)<forms-4.1>` for full details.
.. _csrf-cookie-masked-usage: .. _csrf-cookie-masked-usage:
``CSRF_COOKIE_MASKED`` setting ``CSRF_COOKIE_MASKED`` setting
@ -253,6 +271,8 @@ File Uploads
* ... * ...
.. _forms-4.1:
Forms Forms
~~~~~ ~~~~~
@ -279,6 +299,34 @@ Forms
as the template implements ``<fieldset>`` and ``<legend>`` to group related as the template implements ``<fieldset>`` and ``<legend>`` to group related
inputs and is easier for screen reader users to navigate. inputs and is easier for screen reader users to navigate.
The div-based output will become the default rendering style from Django 5.0.
* In order to smooth adoption of the new ``<div>`` output style, two
transitional form renderer classes are available:
:class:`django.forms.renderers.DjangoDivFormRenderer` and
:class:`django.forms.renderers.Jinja2DivFormRenderer`, for the Django and
Jinja2 template backends respectively.
You can apply one of these via the :setting:`FORM_RENDERER` setting. For
example::
FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer"
Once the ``<div>`` output style is the default, from Django 5.0, these
transitional renderers will be deprecated, for removal in Django 6.0. The
``FORM_RENDERER`` declaration can be removed at that time.
* If the new ``<div>`` output style is not appropriate for your project, you should
define a renderer subclass specifying
:attr:`~django.forms.renderers.BaseRenderer.form_template_name` and
:attr:`~django.forms.renderers.BaseRenderer.formset_template_name` for your
required style, and set :setting:`FORM_RENDERER` accordingly.
For example, for the ``<p>`` output style used by :meth:`~.Form.as_p`, you
would define a form renderer setting ``form_template_name`` to
``"django/forms/p.html"`` and ``formset_template_name`` to
``"django/forms/formsets/p.html"``.
* The new :meth:`~django.forms.BoundField.legend_tag` allows rendering field * The new :meth:`~django.forms.BoundField.legend_tag` allows rendering field
labels in ``<legend>`` tags via the new ``tag`` argument of labels in ``<legend>`` tags via the new ``tag`` argument of
:meth:`~django.forms.BoundField.label_tag`. :meth:`~django.forms.BoundField.label_tag`.
@ -718,6 +766,10 @@ Miscellaneous
``Expression.asc()`` and ``Expression.desc()`` methods, and the ``OrderBy`` ``Expression.asc()`` and ``Expression.desc()`` methods, and the ``OrderBy``
expression is deprecated. Use ``None`` instead. expression is deprecated. Use ``None`` instead.
* The ``"django/forms/default.html"`` and
``"django/forms/formsets/default.html"`` templates which are a proxy to the
table-based templates are deprecated. Use the specific template instead.
Features removed in 4.1 Features removed in 4.1
======================= =======================

View File

@ -11,6 +11,8 @@ except ImportError:
def jinja2_tests(test_func): def jinja2_tests(test_func):
test_func = skipIf(jinja2 is None, "this test requires jinja2")(test_func) test_func = skipIf(jinja2 is None, "this test requires jinja2")(test_func)
return override_settings( return override_settings(
FORM_RENDERER="django.forms.renderers.Jinja2", # RemovedInDjango50Warning: When the deprecation ends, revert to
# FORM_RENDERER="django.forms.renderers.Jinja2",
FORM_RENDERER="django.forms.renderers.Jinja2DivFormRenderer",
TEMPLATES={"BACKEND": "django.template.backends.jinja2.Jinja2"}, TEMPLATES={"BACKEND": "django.template.backends.jinja2.Jinja2"},
)(test_func) )(test_func)

View File

@ -42,8 +42,9 @@ from django.forms.utils import ErrorList
from django.http import QueryDict from django.http import QueryDict
from django.template import Context, Template from django.template import Context, Template
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.test.utils import override_settings from django.test.utils import isolate_lru_cache, override_settings
from django.utils.datastructures import MultiValueDict from django.utils.datastructures import MultiValueDict
from django.utils.deprecation import RemovedInDjango50Warning
from django.utils.safestring import mark_safe from django.utils.safestring import mark_safe
from . import jinja2_tests from . import jinja2_tests
@ -149,17 +150,12 @@ class FormsTestCase(SimpleTestCase):
) )
self.assertHTMLEqual( self.assertHTMLEqual(
str(p), str(p),
""" '<div><label for="id_first_name">First name:</label><input type="text" '
<tr><th><label for="id_first_name">First name:</label></th><td> 'name="first_name" value="John" required id="id_first_name"></div><div>'
<input type="text" name="first_name" value="John" id="id_first_name" '<label for="id_last_name">Last name:</label><input type="text" '
required></td></tr> 'name="last_name" value="Lennon" required id="id_last_name"></div><div>'
<tr><th><label for="id_last_name">Last name:</label></th><td> '<label for="id_birthday">Birthday:</label><input type="text" '
<input type="text" name="last_name" value="Lennon" id="id_last_name" 'name="birthday" value="1940-10-9" required id="id_birthday"></div>',
required></td></tr>
<tr><th><label for="id_birthday">Birthday:</label></th><td>
<input type="text" name="birthday" value="1940-10-9" id="id_birthday"
required></td></tr>
""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_div(), p.as_div(),
@ -182,15 +178,15 @@ class FormsTestCase(SimpleTestCase):
self.assertEqual(p.cleaned_data, {}) self.assertEqual(p.cleaned_data, {})
self.assertHTMLEqual( self.assertHTMLEqual(
str(p), str(p),
"""<tr><th><label for="id_first_name">First name:</label></th><td> '<div><label for="id_first_name">First name:</label>'
<ul class="errorlist"><li>This field is required.</li></ul> '<ul class="errorlist"><li>This field is required.</li></ul>'
<input type="text" name="first_name" id="id_first_name" required></td></tr> '<input type="text" name="first_name" required id="id_first_name"></div>'
<tr><th><label for="id_last_name">Last name:</label></th> '<div><label for="id_last_name">Last name:</label>'
<td><ul class="errorlist"><li>This field is required.</li></ul> '<ul class="errorlist"><li>This field is required.</li></ul>'
<input type="text" name="last_name" id="id_last_name" required></td></tr> '<input type="text" name="last_name" required id="id_last_name"></div><div>'
<tr><th><label for="id_birthday">Birthday:</label></th><td> '<label for="id_birthday">Birthday:</label>'
<ul class="errorlist"><li>This field is required.</li></ul> '<ul class="errorlist"><li>This field is required.</li></ul>'
<input type="text" name="birthday" id="id_birthday" required></td></tr>""", '<input type="text" name="birthday" required id="id_birthday"></div>',
) )
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_table(), p.as_table(),
@ -261,12 +257,12 @@ class FormsTestCase(SimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
str(p), str(p),
"""<tr><th><label for="id_first_name">First name:</label></th><td> '<div><label for="id_first_name">First name:</label><input type="text" '
<input type="text" name="first_name" id="id_first_name" required></td></tr> 'name="first_name" id="id_first_name" required></div><div><label '
<tr><th><label for="id_last_name">Last name:</label></th><td> 'for="id_last_name">Last name:</label><input type="text" name="last_name" '
<input type="text" name="last_name" id="id_last_name" required></td></tr> 'id="id_last_name" required></div><div><label for="id_birthday">'
<tr><th><label for="id_birthday">Birthday:</label></th><td> 'Birthday:</label><input type="text" name="birthday" id="id_birthday" '
<input type="text" name="birthday" id="id_birthday" required></td></tr>""", "required></div>",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
p.as_table(), p.as_table(),
@ -4932,9 +4928,7 @@ class TemplateTests(SimpleTestCase):
t = Template( t = Template(
'<form method="post">' '<form method="post">'
"<table>"
"{{ form }}" "{{ form }}"
"</table>"
'<input type="submit" required>' '<input type="submit" required>'
"</form>" "</form>"
) )
@ -4944,14 +4938,12 @@ class TemplateTests(SimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
my_function("GET", {}), my_function("GET", {}),
'<form method="post">' '<form method="post">'
"<table>" "<div>Username:"
"<tr><th>Username:</th><td>" '<input type="text" name="username" maxlength="10" required></div>'
'<input type="text" name="username" maxlength="10" required></td></tr>' "<div>Password1:"
"<tr><th>Password1:</th><td>" '<input type="password" name="password1" required></div>'
'<input type="password" name="password1" required></td></tr>' "<div>Password2:"
"<tr><th>Password2:</th><td>" '<input type="password" name="password2" required></div>'
'<input type="password" name="password2" required></td></tr>'
"</table>"
'<input type="submit" required>' '<input type="submit" required>'
"</form>", "</form>",
) )
@ -4966,18 +4958,16 @@ class TemplateTests(SimpleTestCase):
}, },
), ),
'<form method="post">' '<form method="post">'
"<table>" '<ul class="errorlist nonfield">'
'<tr><td colspan="2"><ul class="errorlist nonfield">' "<li>Please make sure your passwords match.</li></ul>"
"<li>Please make sure your passwords match.</li></ul></td></tr>" '<div>Username:<ul class="errorlist">'
'<tr><th>Username:</th><td><ul class="errorlist">'
"<li>Ensure this value has at most 10 characters (it has 23).</li></ul>" "<li>Ensure this value has at most 10 characters (it has 23).</li></ul>"
'<input type="text" name="username" ' '<input type="text" name="username" '
'value="this-is-a-long-username" maxlength="10" required></td></tr>' 'value="this-is-a-long-username" maxlength="10" required></div>'
"<tr><th>Password1:</th><td>" "<div>Password1:"
'<input type="password" name="password1" required></td></tr>' '<input type="password" name="password1" required></div>'
"<tr><th>Password2:</th><td>" "<div>Password2:"
'<input type="password" name="password2" required></td></tr>' '<input type="password" name="password2" required></div>'
"</table>"
'<input type="submit" required>' '<input type="submit" required>'
"</form>", "</form>",
) )
@ -5054,7 +5044,7 @@ class OverrideTests(SimpleTestCase):
f = FirstNameForm() f = FirstNameForm()
try: try:
self.assertInHTML("<th>1</th>", f.render()) f.render()
except RecursionError: except RecursionError:
self.fail("Cyclic reference in BoundField.render().") self.fail("Cyclic reference in BoundField.render().")
@ -5069,3 +5059,16 @@ class OverrideTests(SimpleTestCase):
'<label for="id_name" class="required">Name:</label>' '<label for="id_name" class="required">Name:</label>'
'<legend class="required">Language:</legend>', '<legend class="required">Language:</legend>',
) )
class DeprecationTests(SimpleTestCase):
def test_warning(self):
from django.forms.utils import DEFAULT_TEMPLATE_DEPRECATION_MSG
with isolate_lru_cache(get_default_renderer), self.settings(
FORM_RENDERER="django.forms.renderers.DjangoTemplates"
), self.assertRaisesMessage(
RemovedInDjango50Warning, DEFAULT_TEMPLATE_DEPRECATION_MSG
):
form = Person()
str(form)

View File

@ -23,10 +23,12 @@ from django.forms.formsets import (
all_valid, all_valid,
formset_factory, formset_factory,
) )
from django.forms.renderers import TemplatesSetting from django.forms.renderers import TemplatesSetting, get_default_renderer
from django.forms.utils import ErrorList from django.forms.utils import ErrorList
from django.forms.widgets import HiddenInput from django.forms.widgets import HiddenInput
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.test.utils import isolate_lru_cache
from django.utils.deprecation import RemovedInDjango50Warning
from . import jinja2_tests from . import jinja2_tests
@ -125,8 +127,8 @@ class FormsFormsetTestCase(SimpleTestCase):
<input type="hidden" name="choices-INITIAL_FORMS" value="0"> <input type="hidden" name="choices-INITIAL_FORMS" value="0">
<input type="hidden" name="choices-MIN_NUM_FORMS" value="0"> <input type="hidden" name="choices-MIN_NUM_FORMS" value="0">
<input type="hidden" name="choices-MAX_NUM_FORMS" value="1000"> <input type="hidden" name="choices-MAX_NUM_FORMS" value="1000">
<tr><th>Choice:</th><td><input type="text" name="choices-0-choice"></td></tr> <div>Choice:<input type="text" name="choices-0-choice"></div>
<tr><th>Votes:</th><td><input type="number" name="choices-0-votes"></td></tr>""", <div>Votes:<input type="number" name="choices-0-votes"></div>""",
) )
# FormSet are treated similarly to Forms. FormSet has an is_valid() # FormSet are treated similarly to Forms. FormSet has an is_valid()
# method, and a cleaned_data or errors attribute depending on whether # method, and a cleaned_data or errors attribute depending on whether
@ -976,12 +978,12 @@ class FormsFormsetTestCase(SimpleTestCase):
formset = LimitedFavoriteDrinkFormSet() formset = LimitedFavoriteDrinkFormSet()
self.assertHTMLEqual( self.assertHTMLEqual(
"\n".join(str(form) for form in formset.forms), "\n".join(str(form) for form in formset.forms),
"""<tr><th><label for="id_form-0-name">Name:</label></th> """<div><label for="id_form-0-name">Name:</label>
<td><input type="text" name="form-0-name" id="id_form-0-name"></td></tr> <input type="text" name="form-0-name" id="id_form-0-name"></div>
<tr><th><label for="id_form-1-name">Name:</label></th> <div><label for="id_form-1-name">Name:</label>
<td><input type="text" name="form-1-name" id="id_form-1-name"></td></tr> <input type="text" name="form-1-name" id="id_form-1-name"></div>
<tr><th><label for="id_form-2-name">Name:</label></th> <div><label for="id_form-2-name">Name:</label>
<td><input type="text" name="form-2-name" id="id_form-2-name"></td></tr>""", <input type="text" name="form-2-name" id="id_form-2-name"></div>""",
) )
# If max_num is 0 then no form is rendered at all. # If max_num is 0 then no form is rendered at all.
LimitedFavoriteDrinkFormSet = formset_factory( LimitedFavoriteDrinkFormSet = formset_factory(
@ -997,10 +999,10 @@ class FormsFormsetTestCase(SimpleTestCase):
formset = LimitedFavoriteDrinkFormSet() formset = LimitedFavoriteDrinkFormSet()
self.assertHTMLEqual( self.assertHTMLEqual(
"\n".join(str(form) for form in formset.forms), "\n".join(str(form) for form in formset.forms),
"""<tr><th><label for="id_form-0-name">Name:</label></th><td> """<div><label for="id_form-0-name">Name:</label>
<input type="text" name="form-0-name" id="id_form-0-name"></td></tr> <input type="text" name="form-0-name" id="id_form-0-name"></div>
<tr><th><label for="id_form-1-name">Name:</label></th> <div><label for="id_form-1-name">Name:</label>
<td><input type="text" name="form-1-name" id="id_form-1-name"></td></tr>""", <input type="text" name="form-1-name" id="id_form-1-name"></div>""",
) )
def test_limiting_extra_lest_than_max_num(self): def test_limiting_extra_lest_than_max_num(self):
@ -1011,8 +1013,8 @@ class FormsFormsetTestCase(SimpleTestCase):
formset = LimitedFavoriteDrinkFormSet() formset = LimitedFavoriteDrinkFormSet()
self.assertHTMLEqual( self.assertHTMLEqual(
"\n".join(str(form) for form in formset.forms), "\n".join(str(form) for form in formset.forms),
"""<tr><th><label for="id_form-0-name">Name:</label></th> """<div><label for="id_form-0-name">Name:</label>
<td><input type="text" name="form-0-name" id="id_form-0-name"></td></tr>""", <input type="text" name="form-0-name" id="id_form-0-name"></div>""",
) )
def test_max_num_with_initial_data(self): def test_max_num_with_initial_data(self):
@ -1024,11 +1026,11 @@ class FormsFormsetTestCase(SimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
"\n".join(str(form) for form in formset.forms), "\n".join(str(form) for form in formset.forms),
""" """
<tr><th><label for="id_form-0-name">Name:</label></th> <div><label for="id_form-0-name">Name:</label>
<td><input type="text" name="form-0-name" value="Fernet and Coke" <input type="text" name="form-0-name" value="Fernet and Coke"
id="id_form-0-name"></td></tr> id="id_form-0-name"></div>
<tr><th><label for="id_form-1-name">Name:</label></th> <div><label for="id_form-1-name">Name:</label>
<td><input type="text" name="form-1-name" id="id_form-1-name"></td></tr> <input type="text" name="form-1-name" id="id_form-1-name"></div>
""", """,
) )
@ -1056,12 +1058,12 @@ class FormsFormsetTestCase(SimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
"\n".join(str(form) for form in formset.forms), "\n".join(str(form) for form in formset.forms),
""" """
<tr><th><label for="id_form-0-name">Name:</label></th> <div><label for="id_form-0-name">Name:</label>
<td><input id="id_form-0-name" name="form-0-name" type="text" <input id="id_form-0-name" name="form-0-name" type="text"
value="Fernet and Coke"></td></tr> value="Fernet and Coke"></div>
<tr><th><label for="id_form-1-name">Name:</label></th> <div><label for="id_form-1-name">Name:</label>
<td><input id="id_form-1-name" name="form-1-name" type="text" <input id="id_form-1-name" name="form-1-name" type="text"
value="Bloody Mary"></td></tr> value="Bloody Mary"></div>
""", """,
) )
@ -1082,18 +1084,15 @@ class FormsFormsetTestCase(SimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
"\n".join(str(form) for form in formset.forms), "\n".join(str(form) for form in formset.forms),
""" """
<tr><th><label for="id_form-0-name">Name:</label></th> <div><label for="id_form-0-name">Name:</label>
<td>
<input id="id_form-0-name" name="form-0-name" type="text" value="Gin Tonic"> <input id="id_form-0-name" name="form-0-name" type="text" value="Gin Tonic">
</td></tr> </div>
<tr><th><label for="id_form-1-name">Name:</label></th> <div><label for="id_form-1-name">Name:</label>
<td>
<input id="id_form-1-name" name="form-1-name" type="text" <input id="id_form-1-name" name="form-1-name" type="text"
value="Bloody Mary"></td></tr> value="Bloody Mary"></div>
<tr><th><label for="id_form-2-name">Name:</label></th> <div><label for="id_form-2-name">Name:</label>
<td>
<input id="id_form-2-name" name="form-2-name" type="text" <input id="id_form-2-name" name="form-2-name" type="text"
value="Jack and Coke"></td></tr> value="Jack and Coke"></div>
""", """,
) )
@ -1173,12 +1172,11 @@ class FormsFormsetTestCase(SimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
"\n".join(str(form) for form in formset.forms), "\n".join(str(form) for form in formset.forms),
""" """
<tr><th><label for="id_form-0-name">Name:</label></th> <div><label for="id_form-0-name">Name:</label>
<td>
<input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name"> <input type="text" name="form-0-name" value="Gin Tonic" id="id_form-0-name">
</td></tr> </div>
<tr><th><label for="id_form-1-name">Name:</label></th> <div><label for="id_form-1-name">Name:</label>
<td><input type="text" name="form-1-name" id="id_form-1-name"></td></tr>""", <input type="text" name="form-1-name" id="id_form-1-name"></div>""",
) )
def test_management_form_field_names(self): def test_management_form_field_names(self):
@ -1701,16 +1699,16 @@ class TestIsBoundBehavior(SimpleTestCase):
# Can still render the formset. # Can still render the formset.
self.assertHTMLEqual( self.assertHTMLEqual(
str(formset), str(formset),
'<tr><td colspan="2">'
'<ul class="errorlist nonfield">' '<ul class="errorlist nonfield">'
"<li>(Hidden field TOTAL_FORMS) This field is required.</li>" "<li>(Hidden field TOTAL_FORMS) This field is required.</li>"
"<li>(Hidden field INITIAL_FORMS) This field is required.</li>" "<li>(Hidden field INITIAL_FORMS) This field is required.</li>"
"</ul>" "</ul>"
"<div>"
'<input type="hidden" name="form-TOTAL_FORMS" id="id_form-TOTAL_FORMS">' '<input type="hidden" name="form-TOTAL_FORMS" id="id_form-TOTAL_FORMS">'
'<input type="hidden" name="form-INITIAL_FORMS" id="id_form-INITIAL_FORMS">' '<input type="hidden" name="form-INITIAL_FORMS" id="id_form-INITIAL_FORMS">'
'<input type="hidden" name="form-MIN_NUM_FORMS" id="id_form-MIN_NUM_FORMS">' '<input type="hidden" name="form-MIN_NUM_FORMS" id="id_form-MIN_NUM_FORMS">'
'<input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS">' '<input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS">'
"</td></tr>\n", "</div>\n",
) )
def test_management_form_invalid_data(self): def test_management_form_invalid_data(self):
@ -1732,18 +1730,18 @@ class TestIsBoundBehavior(SimpleTestCase):
# Can still render the formset. # Can still render the formset.
self.assertHTMLEqual( self.assertHTMLEqual(
str(formset), str(formset),
'<tr><td colspan="2">'
'<ul class="errorlist nonfield">' '<ul class="errorlist nonfield">'
"<li>(Hidden field TOTAL_FORMS) Enter a whole number.</li>" "<li>(Hidden field TOTAL_FORMS) Enter a whole number.</li>"
"<li>(Hidden field INITIAL_FORMS) Enter a whole number.</li>" "<li>(Hidden field INITIAL_FORMS) Enter a whole number.</li>"
"</ul>" "</ul>"
"<div>"
'<input type="hidden" name="form-TOTAL_FORMS" value="two" ' '<input type="hidden" name="form-TOTAL_FORMS" value="two" '
'id="id_form-TOTAL_FORMS">' 'id="id_form-TOTAL_FORMS">'
'<input type="hidden" name="form-INITIAL_FORMS" value="one" ' '<input type="hidden" name="form-INITIAL_FORMS" value="one" '
'id="id_form-INITIAL_FORMS">' 'id="id_form-INITIAL_FORMS">'
'<input type="hidden" name="form-MIN_NUM_FORMS" id="id_form-MIN_NUM_FORMS">' '<input type="hidden" name="form-MIN_NUM_FORMS" id="id_form-MIN_NUM_FORMS">'
'<input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS">' '<input type="hidden" name="form-MAX_NUM_FORMS" id="id_form-MAX_NUM_FORMS">'
"</td></tr>\n", "</div>\n",
) )
def test_customize_management_form_error(self): def test_customize_management_form_error(self):
@ -1889,3 +1887,17 @@ class AllValidTests(SimpleTestCase):
] ]
self.assertEqual(formset1._errors, expected_errors) self.assertEqual(formset1._errors, expected_errors)
self.assertEqual(formset2._errors, expected_errors) self.assertEqual(formset2._errors, expected_errors)
class DeprecationTests(SimpleTestCase):
def test_warning(self):
from django.forms.utils import DEFAULT_TEMPLATE_DEPRECATION_MSG
with isolate_lru_cache(get_default_renderer), self.settings(
FORM_RENDERER="django.forms.renderers.DjangoTemplates"
), self.assertRaisesMessage(
RemovedInDjango50Warning, DEFAULT_TEMPLATE_DEPRECATION_MSG
):
ChoiceFormSet = formset_factory(Choice)
formset = ChoiceFormSet()
str(formset)

View File

@ -687,12 +687,12 @@ class ModelFormBaseTest(TestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
str(SubclassMeta()), str(SubclassMeta()),
"""<tr><th><label for="id_name">Name:</label></th> '<div><label for="id_name">Name:</label>'
<td><input id="id_name" type="text" name="name" maxlength="20" required></td></tr> '<input type="text" name="name" maxlength="20" required id="id_name">'
<tr><th><label for="id_slug">Slug:</label></th> '</div><div><label for="id_slug">Slug:</label><input type="text" '
<td><input id="id_slug" type="text" name="slug" maxlength="20" required></td></tr> 'name="slug" maxlength="20" required id="id_slug"></div><div>'
<tr><th><label for="id_checkbox">Checkbox:</label></th> '<label for="id_checkbox">Checkbox:</label>'
<td><input type="checkbox" name="checkbox" id="id_checkbox" required></td></tr>""", '<input type="checkbox" name="checkbox" required id="id_checkbox"></div>',
) )
def test_orderfields_form(self): def test_orderfields_form(self):
@ -704,10 +704,10 @@ class ModelFormBaseTest(TestCase):
self.assertEqual(list(OrderFields.base_fields), ["url", "name"]) self.assertEqual(list(OrderFields.base_fields), ["url", "name"])
self.assertHTMLEqual( self.assertHTMLEqual(
str(OrderFields()), str(OrderFields()),
"""<tr><th><label for="id_url">The URL:</label></th> '<div><label for="id_url">The URL:</label>'
<td><input id="id_url" type="text" name="url" maxlength="40" required></td></tr> '<input type="text" name="url" maxlength="40" required id="id_url">'
<tr><th><label for="id_name">Name:</label></th> '</div><div><label for="id_name">Name:</label><input type="text" '
<td><input id="id_name" type="text" name="name" maxlength="20" required></td></tr>""", 'name="name" maxlength="20" required id="id_name"></div>',
) )
def test_orderfields2_form(self): def test_orderfields2_form(self):
@ -1460,12 +1460,11 @@ class ModelFormBasicTests(TestCase):
f = BaseCategoryForm() f = BaseCategoryForm()
self.assertHTMLEqual( self.assertHTMLEqual(
str(f), str(f),
"""<tr><th><label for="id_name">Name:</label></th> '<div><label for="id_name">Name:</label><input type="text" name="name" '
<td><input id="id_name" type="text" name="name" maxlength="20" required></td></tr> 'maxlength="20" required id="id_name"></div><div><label for="id_slug">Slug:'
<tr><th><label for="id_slug">Slug:</label></th> '</label><input type="text" name="slug" maxlength="20" required '
<td><input id="id_slug" type="text" name="slug" maxlength="20" required></td></tr> 'id="id_slug"></div><div><label for="id_url">The URL:</label>'
<tr><th><label for="id_url">The URL:</label></th> '<input type="text" name="url" maxlength="40" required id="id_url"></div>',
<td><input id="id_url" type="text" name="url" maxlength="40" required></td></tr>""",
) )
self.assertHTMLEqual( self.assertHTMLEqual(
str(f.as_ul()), str(f.as_ul()),
@ -1538,12 +1537,9 @@ class ModelFormBasicTests(TestCase):
f = RoykoForm(auto_id=False, instance=self.w_royko) f = RoykoForm(auto_id=False, instance=self.w_royko)
self.assertHTMLEqual( self.assertHTMLEqual(
str(f), str(f),
""" '<div>Name:<div class="helptext">Use both first and last names.</div>'
<tr><th>Name:</th><td> '<input type="text" name="name" value="Mike Royko" maxlength="50" '
<input type="text" name="name" value="Mike Royko" maxlength="50" required> "required></div>",
<br>
<span class="helptext">Use both first and last names.</span></td></tr>
""",
) )
art = Article.objects.create( art = Article.objects.create(
@ -1703,30 +1699,39 @@ class ModelFormBasicTests(TestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
str(f), str(f),
""" """
<tr><th>Headline:</th><td> <div>Headline:
<input type="text" name="headline" maxlength="50" required></td></tr> <input type="text" name="headline" maxlength="50" required>
<tr><th>Slug:</th><td> </div>
<input type="text" name="slug" maxlength="50" required></td></tr> <div>Slug:
<tr><th>Pub date:</th><td> <input type="text" name="slug" maxlength="50" required>
<input type="text" name="pub_date" required></td></tr> </div>
<tr><th>Writer:</th><td><select name="writer" required> <div>Pub date:
<option value="" selected>---------</option> <input type="text" name="pub_date" required>
<option value="%s">Bob Woodward</option> </div>
<option value="%s">Mike Royko</option> <div>Writer:
</select></td></tr> <select name="writer" required>
<tr><th>Article:</th><td> <option value="" selected>---------</option>
<textarea rows="10" cols="40" name="article" required></textarea></td></tr> <option value="%s">Bob Woodward</option>
<tr><th>Categories:</th><td><select multiple name="categories"> <option value="%s">Mike Royko</option>
<option value="%s">Entertainment</option> </select>
<option value="%s">It&#x27;s a test</option> </div>
<option value="%s">Third test</option> <div>Article:
</select></td></tr> <textarea name="article" cols="40" rows="10" required></textarea>
<tr><th>Status:</th><td><select name="status"> </div>
<option value="" selected>---------</option> <div>Categories:
<option value="1">Draft</option> <select name="categories" multiple>
<option value="2">Pending</option> <option value="%s">Entertainment</option>
<option value="3">Live</option> <option value="%s">It&#x27;s a test</option>
</select></td></tr> <option value="%s">Third test</option>
</select>
</div>
<div>Status:
<select name="status">
<option value="" selected>---------</option>
<option value="1">Draft</option><option value="2">Pending</option>
<option value="3">Live</option>
</select>
</div>
""" """
% (self.w_woodward.pk, self.w_royko.pk, self.c1.pk, self.c2.pk, self.c3.pk), % (self.w_woodward.pk, self.w_royko.pk, self.c1.pk, self.c2.pk, self.c3.pk),
) )
@ -1791,12 +1796,8 @@ class ModelFormBasicTests(TestCase):
f = PartialArticleForm(auto_id=False) f = PartialArticleForm(auto_id=False)
self.assertHTMLEqual( self.assertHTMLEqual(
str(f), str(f),
""" '<div>Headline:<input type="text" name="headline" maxlength="50" required>'
<tr><th>Headline:</th><td> '</div><div>Pub date:<input type="text" name="pub_date" required></div>',
<input type="text" name="headline" maxlength="50" required></td></tr>
<tr><th>Pub date:</th><td>
<input type="text" name="pub_date" required></td></tr>
""",
) )
class PartialArticleFormWithSlug(forms.ModelForm): class PartialArticleFormWithSlug(forms.ModelForm):
@ -2990,10 +2991,10 @@ class OtherModelFormTests(TestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
str(CategoryForm()), str(CategoryForm()),
"""<tr><th><label for="id_description">Description:</label></th> '<div><label for="id_description">Description:</label><input type="text" '
<td><input type="text" name="description" id="id_description" required></td></tr> 'name="description" required id="id_description"></div><div>'
<tr><th><label for="id_url">The URL:</label></th> '<label for="id_url">The URL:</label><input type="text" name="url" '
<td><input id="id_url" type="text" name="url" maxlength="40" required></td></tr>""", 'maxlength="40" required id="id_url"></div>',
) )
# to_field_name should also work on ModelMultipleChoiceField ################## # to_field_name should also work on ModelMultipleChoiceField ##################
@ -3014,8 +3015,8 @@ class OtherModelFormTests(TestCase):
self.assertEqual(list(CustomFieldForExclusionForm.base_fields), ["name"]) self.assertEqual(list(CustomFieldForExclusionForm.base_fields), ["name"])
self.assertHTMLEqual( self.assertHTMLEqual(
str(CustomFieldForExclusionForm()), str(CustomFieldForExclusionForm()),
"""<tr><th><label for="id_name">Name:</label></th> '<div><label for="id_name">Name:</label><input type="text" '
<td><input id="id_name" type="text" name="name" maxlength="10" required></td></tr>""", 'name="name" maxlength="10" required id="id_name"></div>',
) )
def test_iterable_model_m2m(self): def test_iterable_model_m2m(self):

View File

@ -1196,14 +1196,12 @@ class TestSplitFormField(PostgreSQLSimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
str(SplitForm()), str(SplitForm()),
""" """
<tr> <div>
<th><label for="id_array_0">Array:</label></th> <label for="id_array_0">Array:</label>
<td> <input id="id_array_0" name="array_0" type="text" required>
<input id="id_array_0" name="array_0" type="text" required> <input id="id_array_1" name="array_1" type="text" required>
<input id="id_array_1" name="array_1" type="text" required> <input id="id_array_2" name="array_2" type="text" required>
<input id="id_array_2" name="array_2" type="text" required> </div>
</td>
</tr>
""", """,
) )

View File

@ -687,17 +687,15 @@ class TestFormField(PostgreSQLSimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
str(form), str(form),
""" """
<tr> <div>
<th> <fieldset>
<label>Field:</label> <legend>Field:</legend>
</th>
<td>
<input id="id_field_0_0" name="field_0_0" type="text"> <input id="id_field_0_0" name="field_0_0" type="text">
<input id="id_field_0_1" name="field_0_1" type="text"> <input id="id_field_0_1" name="field_0_1" type="text">
<input id="id_field_1_0" name="field_1_0" type="text"> <input id="id_field_1_0" name="field_1_0" type="text">
<input id="id_field_1_1" name="field_1_1" type="text"> <input id="id_field_1_1" name="field_1_1" type="text">
</td> </fieldset>
</tr> </div>
""", """,
) )
form = SplitForm( form = SplitForm(
@ -788,13 +786,13 @@ class TestFormField(PostgreSQLSimpleTestCase):
self.assertHTMLEqual( self.assertHTMLEqual(
str(RangeForm()), str(RangeForm()),
""" """
<tr> <div>
<th><label>Ints:</label></th> <fieldset>
<td> <legend>Ints:</legend>
<input id="id_ints_0" name="ints_0" type="number"> <input id="id_ints_0" name="ints_0" type="number">
<input id="id_ints_1" name="ints_1" type="number"> <input id="id_ints_1" name="ints_1" type="number">
</td> </fieldset>
</tr> </div>
""", """,
) )

View File

@ -243,6 +243,9 @@ def setup_collect_tests(start_at, start_after, test_labels=None):
"fields.W342", # ForeignKey(unique=True) -> OneToOneField "fields.W342", # ForeignKey(unique=True) -> OneToOneField
] ]
# RemovedInDjango50Warning
settings.FORM_RENDERER = "django.forms.renderers.DjangoDivFormRenderer"
# Load all the ALWAYS_INSTALLED_APPS. # Load all the ALWAYS_INSTALLED_APPS.
django.setup() django.setup()