From 476d4d508717977101bba1a7f765653e48e88e76 Mon Sep 17 00:00:00 2001 From: Carlton Gibson Date: Tue, 26 Apr 2022 16:01:59 +0200 Subject: [PATCH] Refs #32339 -- Allowed renderer to specify default form and formset templates. Co-authored-by: David Smith --- django/forms/forms.py | 5 ++- django/forms/formsets.py | 6 +++- django/forms/renderers.py | 3 ++ docs/ref/forms/api.txt | 18 ++++++---- docs/ref/forms/renderers.txt | 29 ++++++++++++++-- docs/releases/4.1.txt | 14 ++++++++ docs/topics/forms/formsets.txt | 21 +++++++++--- docs/topics/forms/index.txt | 42 ++++++++++++++++++++---- tests/forms_tests/tests/test_forms.py | 19 +++++++++-- tests/forms_tests/tests/test_formsets.py | 21 ++++++++++++ 10 files changed, 155 insertions(+), 23 deletions(-) diff --git a/django/forms/forms.py b/django/forms/forms.py index 952b974130..2500dccc9b 100644 --- a/django/forms/forms.py +++ b/django/forms/forms.py @@ -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 = [] diff --git a/django/forms/formsets.py b/django/forms/formsets.py index e5807e8688..d51b13548e 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -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} diff --git a/django/forms/renderers.py b/django/forms/renderers.py index 88cf504653..0e406c9c7e 100644 --- a/django/forms/renderers.py +++ b/django/forms/renderers.py @@ -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()") diff --git a/docs/ref/forms/api.txt b/docs/ref/forms/api.txt index 4d3cf8997d..a6b4d11f4a 100644 --- a/docs/ref/forms/api.txt +++ b/docs/ref/forms/api.txt @@ -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`` ----------------------- diff --git a/docs/ref/forms/renderers.txt b/docs/ref/forms/renderers.txt index 809b151516..8c0263e051 100644 --- a/docs/ref/forms/renderers.txt +++ b/docs/ref/forms/renderers.txt @@ -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 ` 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 diff --git a/docs/releases/4.1.txt b/docs/releases/4.1.txt index b564a325a9..4cad556dc3 100644 --- a/docs/releases/4.1.txt +++ b/docs/releases/4.1.txt @@ -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 ```` tags via the new ``tag`` argument of :meth:`~django.forms.BoundField.label_tag`. diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index a549a047cd..0206959f1d 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -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 diff --git a/docs/topics/forms/index.txt b/docs/topics/forms/index.txt index 2d0d61aa02..0e98e50692 100644 --- a/docs/topics/forms/index.txt +++ b/docs/topics/forms/index.txt @@ -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: {% 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 ============== diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py index c5376b115f..ad8f7b9a31 100644 --- a/tests/forms_tests/tests/test_forms.py +++ b/tests/forms_tests/tests/test_forms.py @@ -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 = """ +
+
+ """ + 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" diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index 87084102a5..afa7a8a33d 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -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