Prechádzať zdrojové kódy

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

Co-authored-by: David Smith <smithdc@gmail.com>
Carlton Gibson 2 rokov pred
rodič
commit
476d4d5087

+ 4 - 1
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 = []

+ 5 - 1
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}
 

+ 3 - 0
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()")
 

+ 12 - 6
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``
 -----------------------

+ 27 - 2
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
 <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

+ 14 - 0
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 ``<legend>`` tags via the new ``tag`` argument of
   :meth:`~django.forms.BoundField.label_tag`.

+ 16 - 5
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
 

+ 36 - 6
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:
         </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
 ==============
 

+ 17 - 2
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 = """
+        <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"

+ 21 - 0
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