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 prefix = None
use_required_attribute = True use_required_attribute = True
template_name = "django/forms/default.html"
template_name_p = "django/forms/p.html" template_name_p = "django/forms/p.html"
template_name_table = "django/forms/table.html" template_name_table = "django/forms/table.html"
template_name_ul = "django/forms/ul.html" template_name_ul = "django/forms/ul.html"
@ -316,6 +315,10 @@ class BaseForm(RenderableFormMixin):
output.append(str_hidden) output.append(str_hidden)
return mark_safe("\n".join(output)) return mark_safe("\n".join(output))
@property
def template_name(self):
return self.renderer.form_template_name
def get_context(self): def get_context(self):
fields = [] fields = []
hidden_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." "%(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_p = "django/forms/formsets/p.html"
template_name_table = "django/forms/formsets/table.html" template_name_table = "django/forms/formsets/table.html"
template_name_ul = "django/forms/formsets/ul.html" template_name_ul = "django/forms/formsets/ul.html"
@ -517,6 +517,10 @@ class BaseFormSet(RenderableFormMixin):
else: else:
return self.empty_form.media return self.empty_form.media
@property
def template_name(self):
return self.renderer.formset_template_name
def get_context(self): def get_context(self):
return {"formset": self} return {"formset": self}

View File

@ -15,6 +15,9 @@ def get_default_renderer():
class BaseRenderer: class BaseRenderer:
form_template_name = "django/forms/default.html"
formset_template_name = "django/forms/formsets/default.html"
def get_template(self, template_name): def get_template(self, template_name):
raise NotImplementedError("subclasses must implement get_template()") 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 .. attribute:: Form.template_name
The name of a template that is going to be rendered if the form is cast into a The name of the template rendered if the form is cast into a string, e.g. via
string, e.g. via ``print(form)`` or in a template via ``{{ form }}``. By ``print(form)`` or in a template via ``{{ form }}``.
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 By default, a property returning the value of the renderer's
overriding the ``template_name`` attribute or more generally by overriding the :attr:`~django.forms.renderers.BaseRenderer.form_template_name`. You may set it
default template, see also :ref:`overriding-built-in-form-templates`. 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`` ``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 setting. It defaults to
``'``:class:`django.forms.renderers.DjangoTemplates`\ ``'``. ``'``: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 :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 Use one of the :ref:`built-in template form renderers
<built-in-template-form-renderers>` or implement your own. Custom 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. 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) .. method:: get_template(template_name)
Subclasses must implement this method with the appropriate template Subclasses must implement this method with the appropriate template

View File

@ -244,6 +244,20 @@ File Uploads
Forms 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 * 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`.

View File

@ -783,11 +783,22 @@ Formsets have the following attributes and methods associated with rendering:
.. versionadded:: 4.0 .. versionadded:: 4.0
The name of the template used when calling ``__str__`` or :meth:`.render`. The name of the template rendered if the formset is cast into a string,
This template renders the formset's management form and then each form in e.g. via ``print(formset)`` or in a template via ``{{ formset }}``.
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, a property returning the value of the renderer's
by default. :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 .. 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 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 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 setting a custom :setting:`FORM_RENDERER` to use that
render the form using the custom template. The below example will result in :attr:`~django.forms.renderers.BaseRenderer.form_template_name` site-wide. You
``{{ form }}`` being rendered as the output of the ``form_snippet.html`` can also customize per-form by overriding the form's
template. :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: In your templates:
@ -779,16 +783,42 @@ In your templates:
</div> </div>
{% endfor %} {% 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): 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 .. versionchanged:: 4.0
Template rendering of forms was added. 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 Further topics
============== ==============

View File

@ -4397,7 +4397,7 @@ class Jinja2FormsTestCase(FormsTestCase):
class CustomRenderer(DjangoTemplates): class CustomRenderer(DjangoTemplates):
pass form_template_name = "forms_tests/form_snippet.html"
class RendererTests(SimpleTestCase): class RendererTests(SimpleTestCase):
@ -4813,7 +4813,22 @@ class TemplateTests(SimpleTestCase):
class OverrideTests(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): class Person(Form):
first_name = CharField() first_name = CharField()
template_name = "forms_tests/form_snippet.html" template_name = "forms_tests/form_snippet.html"

View File

@ -23,6 +23,7 @@ from django.forms.formsets import (
all_valid, all_valid,
formset_factory, formset_factory,
) )
from django.forms.renderers import TemplatesSetting
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
@ -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[1]), False)
self.assertIs(formset._should_delete_form(formset.forms[2]), 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): def test_custom_renderer(self):
""" """
A custom renderer passed to a formset_factory() is passed to all forms A custom renderer passed to a formset_factory() is passed to all forms