Refs #32339 -- Allowed renderer to specify default form and formset templates.

Co-authored-by: David Smith <smithdc@gmail.com>
This commit is contained in:
Carlton Gibson 2022-04-26 16:01:59 +02:00
parent 832096478c
commit 476d4d5087
10 changed files with 155 additions and 23 deletions

View File

@ -66,7 +66,6 @@ class BaseForm(RenderableFormMixin):
prefix = None
use_required_attribute = True
template_name = "django/forms/default.html"
template_name_p = "django/forms/p.html"
template_name_table = "django/forms/table.html"
template_name_ul = "django/forms/ul.html"
@ -316,6 +315,10 @@ class BaseForm(RenderableFormMixin):
output.append(str_hidden)
return mark_safe("\n".join(output))
@property
def template_name(self):
return self.renderer.form_template_name
def get_context(self):
fields = []
hidden_fields = []

View File

@ -62,7 +62,7 @@ class BaseFormSet(RenderableFormMixin):
"%(field_names)s. You may need to file a bug report if the issue persists."
),
}
template_name = "django/forms/formsets/default.html"
template_name_p = "django/forms/formsets/p.html"
template_name_table = "django/forms/formsets/table.html"
template_name_ul = "django/forms/formsets/ul.html"
@ -517,6 +517,10 @@ class BaseFormSet(RenderableFormMixin):
else:
return self.empty_form.media
@property
def template_name(self):
return self.renderer.formset_template_name
def get_context(self):
return {"formset": self}

View File

@ -15,6 +15,9 @@ def get_default_renderer():
class BaseRenderer:
form_template_name = "django/forms/default.html"
formset_template_name = "django/forms/formsets/default.html"
def get_template(self, template_name):
raise NotImplementedError("subclasses must implement get_template()")

View File

@ -527,12 +527,18 @@ a form object, and each rendering method returns a string.
.. attribute:: Form.template_name
The name of a template that is going to be rendered if the form is cast into a
string, e.g. via ``print(form)`` or in a template via ``{{ form }}``. By
default this template is ``'django/forms/default.html'``, which is a proxy for
``'django/forms/table.html'``. The template can be changed per form by
overriding the ``template_name`` attribute or more generally by overriding the
default template, see also :ref:`overriding-built-in-form-templates`.
The name of the template rendered if the form is cast into a string, e.g. via
``print(form)`` or in a template via ``{{ form }}``.
By default, a property returning the value of the renderer's
:attr:`~django.forms.renderers.BaseRenderer.form_template_name`. You may set it
as a string template name in order to override that for a particular form
class.
.. versionchanged:: 4.1
In older versions ``template_name`` defaulted to the string value
``'django/forms/default.html'``.
``template_name_label``
-----------------------

View File

@ -26,9 +26,16 @@ A custom renderer can be specified by updating the :setting:`FORM_RENDERER`
setting. It defaults to
``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``.
You can also provide a custom renderer by setting the
By specifying a custom form renderer and overriding
:attr:`~.BaseRenderer.form_template_name` you can adjust the default form
markup across your project from a single place.
You can also provide a custom renderer per-form or per-widget by setting the
:attr:`.Form.default_renderer` attribute or by using the ``renderer`` argument
of :meth:`.Widget.render`.
of :meth:`.Form.render`, or :meth:`.Widget.render`.
Matching points apply to formset rendering. See :ref:`formset-rendering` for
discussion.
Use one of the :ref:`built-in template form renderers
<built-in-template-form-renderers>` or implement your own. Custom renderers
@ -40,6 +47,24 @@ should return a rendered templates (as a string) or raise
The base class for the built-in form renderers.
.. attribute:: form_template_name
.. versionadded:: 4.1
The default name of the template to use to render a form.
Defaults to ``"django/forms/default.html"``, which is a proxy for
``"django/forms/table.html"``.
.. attribute:: formset_template_name
.. versionadded:: 4.1
The default name of the template to use to render a formset.
Defaults to ``"django/forms/formsets/default.html"``, which is a proxy
for ``"django/forms/formsets/table.html"``.
.. method:: get_template(template_name)
Subclasses must implement this method with the appropriate template

View File

@ -244,6 +244,20 @@ File Uploads
Forms
~~~~~
* The default template used to render forms when cast to a string, e.g. in
templates as ``{{ form }}``, is now configurable at the project-level by
setting :attr:`~django.forms.renderers.BaseRenderer.form_template_name` on
the class provided for :setting:`FORM_RENDERER`.
:attr:`.Form.template_name` is now a property deferring to the renderer, but
may be overridden with a string value to specify the template name per-form
class.
Similarly, the default template used to render formsets can be specified via
the matching
:attr:`~django.forms.renderers.BaseRenderer.formset_template_name` renderer
attribute.
* The new :meth:`~django.forms.BoundField.legend_tag` allows rendering field
labels in ``<legend>`` tags via the new ``tag`` argument of
:meth:`~django.forms.BoundField.label_tag`.

View File

@ -783,11 +783,22 @@ Formsets have the following attributes and methods associated with rendering:
.. versionadded:: 4.0
The name of the template used when calling ``__str__`` or :meth:`.render`.
This template renders the formset's management form and then each form in
the formset as per the template defined by the form's
:attr:`~django.forms.Form.template_name`. This is a proxy of ``as_table``
by default.
The name of the template rendered if the formset is cast into a string,
e.g. via ``print(formset)`` or in a template via ``{{ formset }}``.
By default, a property returning the value of the renderer's
:attr:`~django.forms.renderers.BaseRenderer.formset_template_name`. You may
set it as a string template name in order to override that for a particular
formset class.
This template will be used to render the formset's management form, and
then each form in the formset as per the template defined by the form's
:attr:`~django.forms.Form.template_name`.
.. versionchanged:: 4.1
In older versions ``template_name`` defaulted to the string value
``'django/forms/formset/default.html'``.
.. attribute:: BaseFormSet.template_name_p

View File

@ -759,10 +759,14 @@ Reusable form templates
If your site uses the same rendering logic for forms in multiple places, you
can reduce duplication by saving the form's loop in a standalone template and
overriding the forms :attr:`~django.forms.Form.template_name` attribute to
render the form using the custom template. The below example will result in
``{{ form }}`` being rendered as the output of the ``form_snippet.html``
template.
setting a custom :setting:`FORM_RENDERER` to use that
:attr:`~django.forms.renderers.BaseRenderer.form_template_name` site-wide. You
can also customize per-form by overriding the form's
:attr:`~django.forms.Form.template_name` attribute to render the form using the
custom template.
The below example will result in ``{{ form }}`` being rendered as the output of
the ``form_snippet.html`` template.
In your templates:
@ -779,16 +783,42 @@ In your templates:
</div>
{% endfor %}
In your form::
Then you can configure the :setting:`FORM_RENDERER` setting:
.. code-block:: python
:caption: settings.py
from django.forms.renderers import TemplatesSetting
class CustomFormRenderer(TemplatesSetting):
form_template_name = "form_snippet.html"
FORM_RENDERER = "project.settings.CustomFormRenderer"
… or for a single form::
class MyForm(forms.Form):
template_name = 'form_snippet.html'
template_name = "form_snippet.html"
...
… or for a single render of a form instance, passing in the template name to
the :meth:`.Form.render()`. Here's an example of this being used in a view::
def index(request):
form = MyForm()
rendered_form = form.render("form_snippet.html")
context = {'form': rendered_form}
return render(request, 'index.html', context)
.. versionchanged:: 4.0
Template rendering of forms was added.
.. versionchanged:: 4.1
The ability to set the default ``form_template_name`` on the form renderer
was added.
Further topics
==============

View File

@ -4397,7 +4397,7 @@ class Jinja2FormsTestCase(FormsTestCase):
class CustomRenderer(DjangoTemplates):
pass
form_template_name = "forms_tests/form_snippet.html"
class RendererTests(SimpleTestCase):
@ -4813,7 +4813,22 @@ class TemplateTests(SimpleTestCase):
class OverrideTests(SimpleTestCase):
def test_use_custom_template(self):
@override_settings(FORM_RENDERER="forms_tests.tests.test_forms.CustomRenderer")
def test_custom_renderer_template_name(self):
class Person(Form):
first_name = CharField()
get_default_renderer.cache_clear()
t = Template("{{ form }}")
html = t.render(Context({"form": Person()}))
expected = """
<div class="fieldWrapper"><label for="id_first_name">First name:</label>
<input type="text" name="first_name" required id="id_first_name"></div>
"""
self.assertHTMLEqual(html, expected)
get_default_renderer.cache_clear()
def test_per_form_template_name(self):
class Person(Form):
first_name = CharField()
template_name = "forms_tests/form_snippet.html"

View File

@ -23,6 +23,7 @@ from django.forms.formsets import (
all_valid,
formset_factory,
)
from django.forms.renderers import TemplatesSetting
from django.forms.utils import ErrorList
from django.forms.widgets import HiddenInput
from django.test import SimpleTestCase
@ -1435,6 +1436,26 @@ class FormsFormsetTestCase(SimpleTestCase):
self.assertIs(formset._should_delete_form(formset.forms[1]), False)
self.assertIs(formset._should_delete_form(formset.forms[2]), False)
def test_template_name_uses_renderer_value(self):
class CustomRenderer(TemplatesSetting):
formset_template_name = "a/custom/formset/template.html"
ChoiceFormSet = formset_factory(Choice, renderer=CustomRenderer)
self.assertEqual(
ChoiceFormSet().template_name, "a/custom/formset/template.html"
)
def test_template_name_can_be_overridden(self):
class CustomFormSet(BaseFormSet):
template_name = "a/custom/formset/template.html"
ChoiceFormSet = formset_factory(Choice, formset=CustomFormSet)
self.assertEqual(
ChoiceFormSet().template_name, "a/custom/formset/template.html"
)
def test_custom_renderer(self):
"""
A custom renderer passed to a formset_factory() is passed to all forms