1
0
mirror of https://github.com/django/django.git synced 2024-12-22 09:05:43 +00:00

Fixed #34077 -- Added form field rendering.

This commit is contained in:
David Smith 2022-11-02 20:13:16 +00:00 committed by Mariusz Felisiak
parent d33368b4ab
commit cad376f844
16 changed files with 324 additions and 33 deletions

View File

@ -1,7 +1,7 @@
import re
from django.core.exceptions import ValidationError
from django.forms.utils import pretty_name
from django.forms.utils import RenderableFieldMixin, pretty_name
from django.forms.widgets import MultiWidget, Textarea, TextInput
from django.utils.functional import cached_property
from django.utils.html import format_html, html_safe
@ -10,8 +10,7 @@ from django.utils.translation import gettext_lazy as _
__all__ = ("BoundField",)
@html_safe
class BoundField:
class BoundField(RenderableFieldMixin):
"A Field plus data"
def __init__(self, form, field, name):
@ -26,12 +25,7 @@ class BoundField:
else:
self.label = self.field.label
self.help_text = field.help_text or ""
def __str__(self):
"""Render this field as an HTML widget."""
if self.field.show_hidden_initial:
return self.as_widget() + self.as_hidden(only_initial=True)
return self.as_widget()
self.renderer = form.renderer
@cached_property
def subwidgets(self):
@ -81,6 +75,13 @@ class BoundField:
self.name, self.form.error_class(renderer=self.form.renderer)
)
@property
def template_name(self):
return self.field.template_name or self.form.renderer.field_template_name
def get_context(self):
return {"field": self}
def as_widget(self, widget=None, attrs=None, only_initial=False):
"""
Render the field by rendering the passed widget, adding any HTML

View File

@ -107,6 +107,7 @@ class Field:
localize=False,
disabled=False,
label_suffix=None,
template_name=None,
):
# required -- Boolean that specifies whether the field is required.
# True by default.
@ -164,6 +165,7 @@ class Field:
self.error_messages = messages
self.validators = [*self.default_validators, *validators]
self.template_name = template_name
super().__init__()

View File

@ -4,16 +4,7 @@
{% endif %}
{% for field, errors in fields %}
<div{% set classes = field.css_classes() %}{% if classes %} class="{{ classes }}"{% endif %}>
{% if field.use_fieldset %}
<fieldset>
{% if field.label %}{{ field.legend_tag() }}{% endif %}
{% else %}
{% if field.label %}{{ field.label_tag() }}{% endif %}
{% endif %}
{% if field.help_text %}<div class="helptext">{{ field.help_text|safe }}</div>{% endif %}
{{ errors }}
{{ field }}
{% if field.use_fieldset %}</fieldset>{% endif %}
{{ field.as_field_group() }}
{% if loop.last %}
{% for field in hidden_fields %}{{ field }}{% endfor %}
{% endif %}

View File

@ -0,0 +1,10 @@
{% if field.use_fieldset %}
<fieldset>
{% if field.label %}{{ field.legend_tag() }}{% endif %}
{% else %}
{% if field.label %}{{ field.label_tag() }}{% endif %}
{% endif %}
{% if field.help_text %}<div class="helptext">{{ field.help_text|safe }}</div>{% endif %}
{{ field.errors }}
{{ field }}
{% if field.use_fieldset %}</fieldset>{% endif %}

View File

@ -19,6 +19,7 @@ def get_default_renderer():
class BaseRenderer:
form_template_name = "django/forms/div.html"
formset_template_name = "django/forms/formsets/div.html"
field_template_name = "django/forms/field.html"
def get_template(self, template_name):
raise NotImplementedError("subclasses must implement get_template()")

View File

@ -4,16 +4,7 @@
{% endif %}
{% for field, errors in fields %}
<div{% with classes=field.css_classes %}{% if classes %} class="{{ classes }}"{% endif %}{% endwith %}>
{% if field.use_fieldset %}
<fieldset>
{% if field.label %}{{ field.legend_tag }}{% endif %}
{% else %}
{% if field.label %}{{ field.label_tag }}{% endif %}
{% endif %}
{% if field.help_text %}<div class="helptext">{{ field.help_text|safe }}</div>{% endif %}
{{ errors }}
{{ field }}
{% if field.use_fieldset %}</fieldset>{% endif %}
{{ field.as_field_group }}
{% if forloop.last %}
{% for field in hidden_fields %}{{ field }}{% endfor %}
{% endif %}

View File

@ -0,0 +1,10 @@
{% if field.use_fieldset %}
<fieldset>
{% if field.label %}{{ field.legend_tag }}{% endif %}
{% else %}
{% if field.label %}{{ field.label_tag }}{% endif %}
{% endif %}
{% if field.help_text %}<div class="helptext">{{ field.help_text|safe }}</div>{% endif %}
{{ field.errors }}
{{ field }}
{% if field.use_fieldset %}</fieldset>{% endif %}

View File

@ -58,6 +58,29 @@ class RenderableMixin:
__html__ = render
class RenderableFieldMixin(RenderableMixin):
def as_field_group(self):
return self.render()
def as_hidden(self):
raise NotImplementedError(
"Subclasses of RenderableFieldMixin must provide an as_hidden() method."
)
def as_widget(self):
raise NotImplementedError(
"Subclasses of RenderableFieldMixin must provide an as_widget() method."
)
def __str__(self):
"""Render this field as an HTML widget."""
if self.field.show_hidden_initial:
return self.as_widget() + self.as_hidden(only_initial=True)
return self.as_widget()
__html__ = __str__
class RenderableFormMixin(RenderableMixin):
def as_p(self):
"""Render as <p> elements."""

View File

@ -1257,6 +1257,16 @@ Attributes of ``BoundField``
>>> print(f["message"].name)
message
.. attribute:: BoundField.template_name
.. versionadded:: 5.0
The name of the template rendered with :meth:`.BoundField.as_field_group`.
A property returning the value of the
:attr:`~django.forms.Field.template_name` if set otherwise
:attr:`~django.forms.renderers.BaseRenderer.field_template_name`.
.. attribute:: BoundField.use_fieldset
Returns the value of this BoundField widget's ``use_fieldset`` attribute.
@ -1281,6 +1291,15 @@ Attributes of ``BoundField``
Methods of ``BoundField``
-------------------------
.. method:: BoundField.as_field_group()
.. versionadded:: 5.0
Renders the field using :meth:`.BoundField.render` with default values
which renders the ``BoundField``, including its label, help text and errors
using the template's :attr:`~django.forms.Field.template_name` if set
otherwise :attr:`~django.forms.renderers.BaseRenderer.field_template_name`
.. method:: BoundField.as_hidden(attrs=None, **kwargs)
Returns a string of HTML for representing this as an ``<input type="hidden">``.
@ -1321,6 +1340,13 @@ Methods of ``BoundField``
>>> f["message"].css_classes("foo bar")
'foo bar required'
.. method:: BoundField.get_context()
.. versionadded:: 5.0
Return the template context for rendering the field. The available context
is ``field`` being the instance of the bound field.
.. method:: BoundField.label_tag(contents=None, attrs=None, label_suffix=None, tag=None)
Renders a label tag for the form field using the template specified by
@ -1368,6 +1394,20 @@ Methods of ``BoundField``
checkbox widgets where ``<legend>`` may be more appropriate than a
``<label>``.
.. method:: BoundField.render(template_name=None, context=None, renderer=None)
.. versionadded:: 5.0
The render method is called by ``as_field_group``. All arguments are
optional and default to:
* ``template_name``: :attr:`.BoundField.template_name`
* ``context``: Value returned by :meth:`.BoundField.get_context`
* ``renderer``: Value returned by :attr:`.Form.default_renderer`
By passing ``template_name`` you can customize the template used for just a
single call.
.. method:: BoundField.value()
Use this method to render the raw value of this field as it would be rendered

View File

@ -337,6 +337,19 @@ using the ``disabled`` HTML attribute so that it won't be editable by users.
Even if a user tampers with the field's value submitted to the server, it will
be ignored in favor of the value from the form's initial data.
``template_name``
-----------------
.. attribute:: Field.template_name
.. versionadded:: 5.0
The ``template_name`` argument allows a custom template to be used when the
field is rendered with :meth:`~django.forms.BoundField.as_field_group`. By
default this value is set to ``"django/forms/field.html"``. Can be changed per
field by overriding this attribute or more generally by overriding the default
template, see also :ref:`overriding-built-in-field-templates`.
Checking if the field data has changed
======================================

View File

@ -59,6 +59,14 @@ should return a rendered templates (as a string) or raise
Defaults to ``"django/forms/formsets/div.html"`` template.
.. attribute:: field_template_name
.. versionadded:: 5.0
The default name of the template used to render a ``BoundField``.
Defaults to ``"django/forms/field.html"``
.. method:: get_template(template_name)
Subclasses must implement this method with the appropriate template
@ -162,6 +170,16 @@ forms receive a dictionary with the following values:
* ``hidden_fields``: All hidden bound fields.
* ``errors``: All non field related or hidden field related form errors.
Context available in field templates
====================================
.. versionadded:: 5.0
Field templates receive a context from :meth:`.BoundField.get_context`. By
default, fields receive a dictionary with the following values:
* ``field``: The :class:`~django.forms.BoundField`.
Context available in widget templates
=====================================
@ -201,6 +219,19 @@ To override form templates, you must use the :class:`TemplatesSetting`
renderer. Then overriding widget templates works :doc:`the same as
</howto/overriding-templates>` overriding any other template in your project.
.. _overriding-built-in-field-templates:
Overriding built-in field templates
===================================
.. versionadded:: 5.0
:attr:`.Field.template_name`
To override field templates, you must use the :class:`TemplatesSetting`
renderer. Then overriding field templates works :doc:`the same as
</howto/overriding-templates>` overriding any other template in your project.
.. _overriding-built-in-widget-templates:
Overriding built-in widget templates

View File

@ -45,6 +45,69 @@ toggled on via the UI. This behavior can be changed via the new
:attr:`.ModelAdmin.show_facets` attribute. For more information see
:ref:`facet-filters`.
Simplified templates for form field rendering
---------------------------------------------
Django 5.0 introduces the concept of a field group, and field group templates.
This simplifies rendering of the related elements of a Django form field such
as its label, widget, help text, and errors.
For example, the template below:
.. code-block:: html+django
<form>
...
<div>
{{ form.name.label }}
{% if form.name.help_text %}
<div class="helptext">{{ form.name.help_text|safe }}</div>
{% endif %}
{{ form.name.errors }}
{{ form.name }}
<div class="row">
<div class="col">
{{ form.email.label }}
{% if form.email.help_text %}
<div class="helptext">{{ form.email.help_text|safe }}</div>
{% endif %}
{{ form.email.errors }}
{{ form.email }}
</div>
<div class="col">
{{ form.password.label }}
{% if form.password.help_text %}
<div class="helptext">{{ form.password.help_text|safe }}</div>
{% endif %}
{{ form.password.errors }}
{{ form.password }}
</div>
</div>
</div>
...
</form>
Can now be simplified to:
.. code-block:: html+django
<form>
...
<div>
{{ form.name.as_field_group }}
<div class="row">
<div class="col">{{ form.email.as_field_group }}</div>
<div class="col">{{ form.password.as_field_group }}</div>
</div>
</div>
...
</form>
:meth:`~django.forms.BoundField.as_field_group` renders fields with the
``"django/forms/field.html"`` template by default and can be customized on a
per-project, per-field, or per-request basis. See
:ref:`reusable-field-group-templates`.
Minor features
--------------

View File

@ -559,13 +559,73 @@ the :meth:`.Form.render`. Here's an example of this being used in a view::
See :ref:`ref-forms-api-outputting-html` for more details.
.. _reusable-field-group-templates:
Reusable field group templates
------------------------------
.. versionadded:: 5.0
Each field is available as an attribute of the form, using
``{{form.name_of_field }}`` in a template. A field has a
:meth:`~django.forms.BoundField.as_field_group` method which renders the
related elements of the field as a group, its label, widget, errors, and help
text.
This allows generic templates to be written that arrange fields elements in the
required layout. For example:
.. code-block:: html+django
{{ form.non_field_errors }}
<div class="fieldWrapper">
{{ form.subject.as_field_group }}
</div>
<div class="fieldWrapper">
{{ form.message.as_field_group }}
</div>
<div class="fieldWrapper">
{{ form.sender.as_field_group }}
</div>
<div class="fieldWrapper">
{{ form.cc_myself.as_field_group }}
</div>
By default Django uses the ``"django/forms/field.html"`` template which is
designed for use with the default ``"django/forms/div.html"`` form style.
The default template can be customized by by setting
:attr:`~django.forms.renderers.BaseRenderer.field_template_name` in your
project-level :setting:`FORM_RENDERER`::
from django.forms.renderers import TemplatesSetting
class CustomFormRenderer(TemplatesSetting):
field_template_name = "field_snippet.html"
… or on a single field::
class MyForm(forms.Form):
subject = forms.CharField(template_name="my_custom_template.html")
...
… or on a per-request basis by calling
:meth:`.BoundField.render` and supplying a template name::
def index(request):
form = ContactForm()
subject = form["subject"]
context = {"subject": subject.render("my_custom_template.html")}
return render(request, "index.html", context)
Rendering fields manually
-------------------------
We don't have to let Django unpack the form's fields; we can do it manually if
we like (allowing us to reorder the fields, for example). Each field is
available as an attribute of the form using ``{{ form.name_of_field }}``, and
in a Django template, will be rendered appropriately. For example:
More fine grained control over field rendering is also possible. Likely this
will be in a custom field template, to allow the template to be written once
and reused for each field. However, it can also be directly accessed from the
field attribute on the form. For example:
.. code-block:: html+django

View File

@ -0,0 +1,3 @@
{{ field.label_tag }}
<p>Custom Field<p>
{{ field }}

View File

@ -4602,6 +4602,7 @@ class Jinja2FormsTestCase(FormsTestCase):
class CustomRenderer(DjangoTemplates):
form_template_name = "forms_tests/form_snippet.html"
field_template_name = "forms_tests/custom_field.html"
class RendererTests(SimpleTestCase):
@ -5009,6 +5010,28 @@ class TemplateTests(SimpleTestCase):
"('username', 'adrian')]",
)
def test_custom_field_template(self):
class MyForm(Form):
first_name = CharField(template_name="forms_tests/custom_field.html")
f = MyForm()
self.assertHTMLEqual(
f.render(),
'<div><label for="id_first_name">First name:</label><p>Custom Field<p>'
'<input type="text" name="first_name" required id="id_first_name"></div>',
)
def test_custom_field_render_template(self):
class MyForm(Form):
first_name = CharField()
f = MyForm()
self.assertHTMLEqual(
f["first_name"].render(template_name="forms_tests/custom_field.html"),
'<label for="id_first_name">First name:</label><p>Custom Field<p>'
'<input type="text" name="first_name" required id="id_first_name">',
)
class OverrideTests(SimpleTestCase):
@override_settings(FORM_RENDERER="forms_tests.tests.test_forms.CustomRenderer")
@ -5026,6 +5049,22 @@ class OverrideTests(SimpleTestCase):
self.assertHTMLEqual(html, expected)
get_default_renderer.cache_clear()
@override_settings(FORM_RENDERER="forms_tests.tests.test_forms.CustomRenderer")
def test_custom_renderer_field_template_name(self):
class Person(Form):
first_name = CharField()
get_default_renderer.cache_clear()
t = Template("{{ form.first_name.as_field_group }}")
html = t.render(Context({"form": Person()}))
expected = """
<label for="id_first_name">First name:</label>
<p>Custom Field<p>
<input type="text" name="first_name" required id="id_first_name">
"""
self.assertHTMLEqual(html, expected)
get_default_renderer.cache_clear()
def test_per_form_template_name(self):
class Person(Form):
first_name = CharField()

View File

@ -5,6 +5,7 @@ from django.core.exceptions import ValidationError
from django.forms.utils import (
ErrorDict,
ErrorList,
RenderableFieldMixin,
RenderableMixin,
flatatt,
pretty_name,
@ -258,6 +259,18 @@ class FormsUtilsTestCase(SimpleTestCase):
with self.assertRaisesMessage(NotImplementedError, msg):
mixin.get_context()
def test_field_mixin_as_hidden_must_be_implemented(self):
mixin = RenderableFieldMixin()
msg = "Subclasses of RenderableFieldMixin must provide an as_hidden() method."
with self.assertRaisesMessage(NotImplementedError, msg):
mixin.as_hidden()
def test_field_mixin_as_widget_must_be_implemented(self):
mixin = RenderableFieldMixin()
msg = "Subclasses of RenderableFieldMixin must provide an as_widget() method."
with self.assertRaisesMessage(NotImplementedError, msg):
mixin.as_widget()
def test_pretty_name(self):
self.assertEqual(pretty_name("john_doe"), "John doe")
self.assertEqual(pretty_name(None), "")