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:
parent
d33368b4ab
commit
cad376f844
@ -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
|
||||
|
@ -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__()
|
||||
|
||||
|
@ -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 %}
|
||||
|
10
django/forms/jinja2/django/forms/field.html
Normal file
10
django/forms/jinja2/django/forms/field.html
Normal 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 %}
|
@ -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()")
|
||||
|
@ -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 %}
|
||||
|
10
django/forms/templates/django/forms/field.html
Normal file
10
django/forms/templates/django/forms/field.html
Normal 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 %}
|
@ -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."""
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
======================================
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
--------------
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
{{ field.label_tag }}
|
||||
<p>Custom Field<p>
|
||||
{{ field }}
|
@ -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()
|
||||
|
@ -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), "")
|
||||
|
Loading…
Reference in New Issue
Block a user