mirror of
https://github.com/django/django.git
synced 2025-01-18 14:24:39 +00:00
Fixed #35521 -- Allowed overriding BoundField class on fields, forms and renderers.
Thank you Sarah Boyce, Carlton Gibson, Tim Schilling and Adam Johnson for reviews. Co-authored-by: Christophe Henry <contact@c-henry.fr> Co-authored-by: David Smith <smithdc@gmail.com> Co-authored-by: Natalia <124304+nessita@users.noreply.github.com> Co-authored-by: Matthias Kestenholz <mk@feinheit.ch>
This commit is contained in:
parent
0cabed9efa
commit
6a7ee02f59
@ -95,6 +95,7 @@ class Field:
|
||||
"required": _("This field is required."),
|
||||
}
|
||||
empty_values = list(validators.EMPTY_VALUES)
|
||||
bound_field_class = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -111,6 +112,7 @@ class Field:
|
||||
disabled=False,
|
||||
label_suffix=None,
|
||||
template_name=None,
|
||||
bound_field_class=None,
|
||||
):
|
||||
# required -- Boolean that specifies whether the field is required.
|
||||
# True by default.
|
||||
@ -135,11 +137,13 @@ class Field:
|
||||
# is its widget is shown in the form but not editable.
|
||||
# label_suffix -- Suffix to be added to the label. Overrides
|
||||
# form's label_suffix.
|
||||
# bound_field_class -- BoundField class to use in Field.get_bound_field.
|
||||
self.required, self.label, self.initial = required, label, initial
|
||||
self.show_hidden_initial = show_hidden_initial
|
||||
self.help_text = help_text
|
||||
self.disabled = disabled
|
||||
self.label_suffix = label_suffix
|
||||
self.bound_field_class = bound_field_class or self.bound_field_class
|
||||
widget = widget or self.widget
|
||||
if isinstance(widget, type):
|
||||
widget = widget()
|
||||
@ -251,7 +255,10 @@ class Field:
|
||||
Return a BoundField instance that will be used when accessing the form
|
||||
field in a template.
|
||||
"""
|
||||
return BoundField(form, self, field_name)
|
||||
bound_field_class = (
|
||||
self.bound_field_class or form.bound_field_class or BoundField
|
||||
)
|
||||
return bound_field_class(form, self, field_name)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
result = copy.copy(self)
|
||||
|
@ -68,6 +68,8 @@ class BaseForm(RenderableFormMixin):
|
||||
template_name_ul = "django/forms/ul.html"
|
||||
template_name_label = "django/forms/label.html"
|
||||
|
||||
bound_field_class = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
data=None,
|
||||
@ -81,6 +83,7 @@ class BaseForm(RenderableFormMixin):
|
||||
field_order=None,
|
||||
use_required_attribute=None,
|
||||
renderer=None,
|
||||
bound_field_class=None,
|
||||
):
|
||||
self.is_bound = data is not None or files is not None
|
||||
self.data = MultiValueDict() if data is None else data
|
||||
@ -124,6 +127,12 @@ class BaseForm(RenderableFormMixin):
|
||||
renderer = renderer()
|
||||
self.renderer = renderer
|
||||
|
||||
self.bound_field_class = (
|
||||
bound_field_class
|
||||
or self.bound_field_class
|
||||
or getattr(self.renderer, "bound_field_class", None)
|
||||
)
|
||||
|
||||
def order_fields(self, field_order):
|
||||
"""
|
||||
Rearrange the fields according to field_order.
|
||||
|
@ -21,6 +21,8 @@ class BaseRenderer:
|
||||
formset_template_name = "django/forms/formsets/div.html"
|
||||
field_template_name = "django/forms/field.html"
|
||||
|
||||
bound_field_class = None
|
||||
|
||||
def get_template(self, template_name):
|
||||
raise NotImplementedError("subclasses must implement get_template()")
|
||||
|
||||
|
@ -822,6 +822,9 @@ classes, as needed. The HTML will look something like:
|
||||
>>> f["subject"].legend_tag(attrs={"class": "foo"})
|
||||
<legend for="id_subject" class="foo required">Subject:</legend>
|
||||
|
||||
You may further modify the rendering of form rows by using a
|
||||
:ref:`custom BoundField <custom-boundfield>`.
|
||||
|
||||
.. _ref-forms-api-configuring-label:
|
||||
|
||||
Configuring form elements' HTML ``id`` attributes and ``<label>`` tags
|
||||
@ -1149,6 +1152,12 @@ they're not the only way a form object can be displayed.
|
||||
|
||||
The ``__str__()`` method of this object displays the HTML for this field.
|
||||
|
||||
You can use :attr:`.Form.bound_field_class` and
|
||||
:attr:`.Field.bound_field_class` to specify a different ``BoundField`` class
|
||||
per form or per field, respectively.
|
||||
|
||||
See :ref:`custom-boundfield` for examples of overriding a ``BoundField``.
|
||||
|
||||
To retrieve a single ``BoundField``, use dictionary lookup syntax on your form
|
||||
using the field's name as the key:
|
||||
|
||||
@ -1488,23 +1497,34 @@ Methods of ``BoundField``
|
||||
>>> print(bound_form["subject"].value())
|
||||
hi
|
||||
|
||||
.. _custom-boundfield:
|
||||
|
||||
Customizing ``BoundField``
|
||||
==========================
|
||||
|
||||
If you need to access some additional information about a form field in a
|
||||
template and using a subclass of :class:`~django.forms.Field` isn't
|
||||
sufficient, consider also customizing :class:`~django.forms.BoundField`.
|
||||
.. attribute:: Form.bound_field_class
|
||||
|
||||
A custom form field can override ``get_bound_field()``:
|
||||
.. versionadded:: 5.2
|
||||
|
||||
.. method:: Field.get_bound_field(form, field_name)
|
||||
Define a custom :class:`~django.forms.BoundField` class to use when rendering
|
||||
the form. This takes precedence over the project-level
|
||||
:attr:`.BaseRenderer.bound_field_class` (along with a custom
|
||||
:setting:`FORM_RENDERER`), but can be overridden by the field-level
|
||||
:attr:`.Field.bound_field_class`.
|
||||
|
||||
Takes an instance of :class:`~django.forms.Form` and the name of the field.
|
||||
The return value will be used when accessing the field in a template. Most
|
||||
likely it will be an instance of a subclass of
|
||||
:class:`~django.forms.BoundField`.
|
||||
If not defined as a class variable, ``bound_field_class`` can be set via the
|
||||
``bound_field_class`` argument in the :class:`Form` or :class:`Field`
|
||||
constructor.
|
||||
|
||||
If you have a ``GPSCoordinatesField``, for example, and want to be able to
|
||||
For compatibility reasons, a custom form field can still override
|
||||
:meth:`.Field.get_bound_field()` to use a custom class, though any of the
|
||||
previous options are preferred.
|
||||
|
||||
You may want to use a custom :class:`.BoundField` if you need to access some
|
||||
additional information about a form field in a template and using a subclass of
|
||||
:class:`~django.forms.Field` isn't sufficient.
|
||||
|
||||
For example, if you have a ``GPSCoordinatesField``, and want to be able to
|
||||
access additional information about the coordinates in a template, this could
|
||||
be implemented as follows::
|
||||
|
||||
@ -1523,12 +1543,74 @@ be implemented as follows::
|
||||
|
||||
|
||||
class GPSCoordinatesField(Field):
|
||||
def get_bound_field(self, form, field_name):
|
||||
return GPSCoordinatesBoundField(form, self, field_name)
|
||||
bound_field_class = GPSCoordinatesBoundField
|
||||
|
||||
Now you can access the country in a template with
|
||||
``{{ form.coordinates.country }}``.
|
||||
|
||||
You may also want to customize the default form field template rendering. For
|
||||
example, you can override :meth:`.BoundField.label_tag` to add a custom class::
|
||||
|
||||
class StyledLabelBoundField(BoundField):
|
||||
def label_tag(self, contents=None, attrs=None, label_suffix=None, tag=None):
|
||||
attrs = attrs or {}
|
||||
attrs["class"] = "wide"
|
||||
return super().label_tag(contents, attrs, label_suffix, tag)
|
||||
|
||||
|
||||
class UserForm(forms.Form):
|
||||
bound_field_class = StyledLabelBoundField
|
||||
name = CharField()
|
||||
|
||||
This would update the default form rendering:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> f = UserForm()
|
||||
>>> print(f["name"].label_tag)
|
||||
<label for="id_name" class="wide">Name:</label>
|
||||
|
||||
To add a CSS class to the wrapping HTML element of all fields, a ``BoundField``
|
||||
can be overridden to return a different collection of CSS classes::
|
||||
|
||||
class WrappedBoundField(BoundField):
|
||||
def css_classes(self, extra_classes=None):
|
||||
parent_css_classes = super().css_classes(extra_classes)
|
||||
return f"field-class {parent_css_classes}".strip()
|
||||
|
||||
|
||||
class UserForm(forms.Form):
|
||||
bound_field_class = WrappedBoundField
|
||||
name = CharField()
|
||||
|
||||
This would update the form rendering as follows:
|
||||
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> f = UserForm()
|
||||
>>> print(f)
|
||||
<div class="field-class"><label for="id_name">Name:</label><input type="text" name="name" required id="id_name"></div>
|
||||
|
||||
Alternatively, to override the ``BoundField`` class at the project level,
|
||||
:attr:`.BaseRenderer.bound_field_class` can be defined on a custom
|
||||
:setting:`FORM_RENDERER`:
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``mysite/renderers.py``
|
||||
|
||||
from django.forms.renderers import DjangoTemplates
|
||||
|
||||
from .forms import CustomBoundField
|
||||
|
||||
|
||||
class CustomRenderer(DjangoTemplates):
|
||||
bound_field_class = CustomBoundField
|
||||
|
||||
.. code-block:: python
|
||||
:caption: ``settings.py``
|
||||
|
||||
FORM_RENDERER = "mysite.renderers.CustomRenderer"
|
||||
|
||||
.. _binding-uploaded-files:
|
||||
|
||||
Binding uploaded files to a form
|
||||
|
@ -397,6 +397,16 @@ 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`.
|
||||
|
||||
``bound_field_class``
|
||||
---------------------
|
||||
|
||||
.. attribute:: Field.bound_field_class
|
||||
|
||||
.. versionadded:: 5.2
|
||||
|
||||
The ``bound_field_class`` attribute allows a per-field override of
|
||||
:attr:`.Form.bound_field_class`.
|
||||
|
||||
Checking if the field data has changed
|
||||
======================================
|
||||
|
||||
@ -1635,4 +1645,14 @@ only requirements are that it implement a ``clean()`` method and that its
|
||||
``label``, ``initial``, ``widget``, ``help_text``).
|
||||
|
||||
You can also customize how a field will be accessed by overriding
|
||||
:meth:`~django.forms.Field.get_bound_field()`.
|
||||
:attr:`~django.forms.Field.bound_field_class` or override
|
||||
:meth:`.Field.get_bound_field()` if you need more flexibility when creating
|
||||
the ``BoundField``:
|
||||
|
||||
.. method:: Field.get_bound_field(form, field_name)
|
||||
|
||||
Takes an instance of :class:`~django.forms.Form` and the name of the field.
|
||||
The returned :class:`.BoundField` instance will be used when accessing the
|
||||
field in a template.
|
||||
|
||||
See :ref:`custom-boundfield` for examples of overriding a ``BoundField``.
|
||||
|
@ -65,6 +65,18 @@ should return a rendered templates (as a string) or raise
|
||||
|
||||
Defaults to ``"django/forms/field.html"``
|
||||
|
||||
.. attribute:: bound_field_class
|
||||
|
||||
.. versionadded:: 5.2
|
||||
|
||||
The default class used to represent form fields across the project.
|
||||
|
||||
Defaults to :class:`.BoundField` class.
|
||||
|
||||
This can be customized further using :attr:`.Form.bound_field_class`
|
||||
for per-form overrides, or :attr:`.Field.bound_field_class` for
|
||||
per-field overrides.
|
||||
|
||||
.. method:: get_template(template_name)
|
||||
|
||||
Subclasses must implement this method with the appropriate template
|
||||
|
@ -70,6 +70,62 @@ to be a ``CompositePrimaryKey``::
|
||||
|
||||
See :doc:`/topics/composite-primary-key` for more details.
|
||||
|
||||
Simplified override of :class:`~django.forms.BoundField`
|
||||
--------------------------------------------------------
|
||||
|
||||
Prior to version 5.2, overriding :meth:`.Field.get_bound_field()` was the only
|
||||
option to use a custom :class:`~django.forms.BoundField`. Django now supports
|
||||
specifying the following attributes to customize form rendering:
|
||||
|
||||
* :attr:`.BaseRenderer.bound_field_class` at the project level,
|
||||
* :attr:`.Form.bound_field_class` at the form level, and
|
||||
* :attr:`.Field.bound_field_class` at the field level.
|
||||
|
||||
For example, to customize the ``BoundField`` of a ``Form`` class::
|
||||
|
||||
from django.forms import Form
|
||||
|
||||
|
||||
class CustomBoundField(forms.BoundField):
|
||||
|
||||
custom_class = "custom"
|
||||
|
||||
def css_classes(self, extra_classes=None):
|
||||
result = super().css_classes(extra_classes)
|
||||
if self.custom_class not in result:
|
||||
result += f" {self.custom_class}"
|
||||
return result.strip()
|
||||
|
||||
|
||||
class CustomForm(forms.Form):
|
||||
bound_field_class = CustomBoundField
|
||||
|
||||
name = forms.CharField(
|
||||
label="Your Name",
|
||||
max_length=100,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={"class": "name-input-class"}),
|
||||
)
|
||||
email = forms.EmailField(label="Your Email")
|
||||
|
||||
|
||||
When rendering a ``CustomForm`` instance, the following HTML is included:
|
||||
|
||||
.. code:: html
|
||||
|
||||
<div class="custom">
|
||||
<label for="id_name">Your Name:</label>
|
||||
<input type="text" name="name" class="name-input-class" maxlength="100" id="id_name">
|
||||
</div>
|
||||
|
||||
<div class="custom">
|
||||
<label for="id_email">Your Email:</label>
|
||||
<input type="email" name="email" maxlength="320" required="" id="id_email">
|
||||
</div>
|
||||
|
||||
|
||||
See :ref:`custom-boundfield` for more details about this feature.
|
||||
|
||||
Minor features
|
||||
--------------
|
||||
|
||||
|
@ -8,6 +8,7 @@ from django.core.files.uploadedfile import SimpleUploadedFile
|
||||
from django.core.validators import MaxValueValidator, RegexValidator
|
||||
from django.forms import (
|
||||
BooleanField,
|
||||
BoundField,
|
||||
CharField,
|
||||
CheckboxSelectMultiple,
|
||||
ChoiceField,
|
||||
@ -4971,6 +4972,22 @@ class RendererTests(SimpleTestCase):
|
||||
context = form.get_context()
|
||||
self.assertEqual(context["errors"].renderer, custom)
|
||||
|
||||
def test_boundfield_fallback(self):
|
||||
class RendererWithoutBoundFieldClassAttribute:
|
||||
form_template_name = "django/forms/div.html"
|
||||
formset_template_name = "django/forms/formsets/div.html"
|
||||
field_template_name = "django/forms/field.html"
|
||||
|
||||
def render(self, template_name, context, request=None):
|
||||
return "Nice"
|
||||
|
||||
class UserForm(Form):
|
||||
name = CharField()
|
||||
|
||||
form = UserForm(renderer=RendererWithoutBoundFieldClassAttribute())
|
||||
self.assertIsInstance(form["name"], BoundField)
|
||||
self.assertEqual(form["name"].as_field_group(), "Nice")
|
||||
|
||||
|
||||
class TemplateTests(SimpleTestCase):
|
||||
def test_iterate_radios(self):
|
||||
@ -5473,3 +5490,146 @@ class OverrideTests(SimpleTestCase):
|
||||
'<label for="id_name" class="required">Name:</label>'
|
||||
'<legend class="required">Language:</legend>',
|
||||
)
|
||||
|
||||
|
||||
class BoundFieldWithoutColon(BoundField):
|
||||
def label_tag(self, contents=None, attrs=None, label_suffix=None, tag=None):
|
||||
return super().label_tag(
|
||||
contents=contents, attrs=attrs, label_suffix="", tag=None
|
||||
)
|
||||
|
||||
|
||||
class BoundFieldWithTwoColons(BoundField):
|
||||
def label_tag(self, contents=None, attrs=None, label_suffix=None, tag=None):
|
||||
return super().label_tag(
|
||||
contents=contents, attrs=attrs, label_suffix="::", tag=None
|
||||
)
|
||||
|
||||
|
||||
class BoundFieldWithCustomClass(BoundField):
|
||||
def label_tag(self, contents=None, attrs=None, label_suffix=None, tag=None):
|
||||
attrs = attrs or {}
|
||||
attrs["class"] = "custom-class"
|
||||
return super().label_tag(contents, attrs, label_suffix, tag)
|
||||
|
||||
|
||||
class BoundFieldWithWrappingClass(BoundField):
|
||||
def css_classes(self, extra_classes=None):
|
||||
parent_classes = super().css_classes(extra_classes)
|
||||
return f"field-class {parent_classes}"
|
||||
|
||||
|
||||
class BoundFieldOverrideRenderer(DjangoTemplates):
|
||||
bound_field_class = BoundFieldWithoutColon
|
||||
|
||||
|
||||
@override_settings(
|
||||
FORM_RENDERER="forms_tests.tests.test_forms.BoundFieldOverrideRenderer"
|
||||
)
|
||||
class CustomBoundFieldTest(SimpleTestCase):
|
||||
def test_renderer_custom_bound_field(self):
|
||||
t = Template("{{ form }}")
|
||||
html = t.render(Context({"form": Person()}))
|
||||
expected = """
|
||||
<div><label for="id_first_name">First name</label>
|
||||
<input type="text" name="first_name" required
|
||||
id="id_first_name"></div>
|
||||
<div><label for="id_last_name">Last name</label>
|
||||
<input type="text" name="last_name" required
|
||||
id="id_last_name"></div><div>
|
||||
<label for="id_birthday">Birthday</label>
|
||||
<input type="text" name="birthday" required
|
||||
id="id_birthday"></div>"""
|
||||
self.assertHTMLEqual(html, expected)
|
||||
|
||||
def test_form_custom_boundfield(self):
|
||||
class CustomBoundFieldPerson(Person):
|
||||
bound_field_class = BoundFieldWithTwoColons
|
||||
|
||||
with self.subTest("form's BoundField takes over renderer's BoundField"):
|
||||
t = Template("{{ form }}")
|
||||
html = t.render(Context({"form": CustomBoundFieldPerson()}))
|
||||
expected = """
|
||||
<div><label for="id_first_name">First name::</label>
|
||||
<input type="text" name="first_name" required
|
||||
id="id_first_name"></div>
|
||||
<div><label for="id_last_name">Last name::</label>
|
||||
<input type="text" name="last_name" required
|
||||
id="id_last_name"></div><div>
|
||||
<label for="id_birthday">Birthday::</label>
|
||||
<input type="text" name="birthday" required
|
||||
id="id_birthday"></div>"""
|
||||
self.assertHTMLEqual(html, expected)
|
||||
|
||||
with self.subTest("Constructor argument takes over class property"):
|
||||
t = Template("{{ form }}")
|
||||
html = t.render(
|
||||
Context(
|
||||
{
|
||||
"form": CustomBoundFieldPerson(
|
||||
bound_field_class=BoundFieldWithCustomClass
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
expected = """
|
||||
<div><label class="custom-class" for="id_first_name">First name:</label>
|
||||
<input type="text" name="first_name" required
|
||||
id="id_first_name"></div>
|
||||
<div><label class="custom-class" for="id_last_name">Last name:</label>
|
||||
<input type="text" name="last_name" required
|
||||
id="id_last_name"></div><div>
|
||||
<label class="custom-class" for="id_birthday">Birthday:</label>
|
||||
<input type="text" name="birthday" required
|
||||
id="id_birthday"></div>"""
|
||||
self.assertHTMLEqual(html, expected)
|
||||
|
||||
with self.subTest("Overriding css_classes works as expected"):
|
||||
t = Template("{{ form }}")
|
||||
html = t.render(
|
||||
Context(
|
||||
{
|
||||
"form": CustomBoundFieldPerson(
|
||||
bound_field_class=BoundFieldWithWrappingClass
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
expected = """
|
||||
<div class="field-class"><label for="id_first_name">First name:</label>
|
||||
<input type="text" name="first_name" required
|
||||
id="id_first_name"></div>
|
||||
<div class="field-class"><label for="id_last_name">Last name:</label>
|
||||
<input type="text" name="last_name" required
|
||||
id="id_last_name"></div><div class="field-class">
|
||||
<label for="id_birthday">Birthday:</label>
|
||||
<input type="text" name="birthday" required
|
||||
id="id_birthday"></div>"""
|
||||
self.assertHTMLEqual(html, expected)
|
||||
|
||||
def test_field_custom_bound_field(self):
|
||||
class BoundFieldWithTwoColonsCharField(CharField):
|
||||
bound_field_class = BoundFieldWithTwoColons
|
||||
|
||||
class CustomFieldBoundFieldPerson(Person):
|
||||
bound_field_class = BoundField
|
||||
|
||||
first_name = BoundFieldWithTwoColonsCharField()
|
||||
last_name = BoundFieldWithTwoColonsCharField(
|
||||
bound_field_class=BoundFieldWithCustomClass
|
||||
)
|
||||
|
||||
html = Template("{{ form }}").render(
|
||||
Context({"form": CustomFieldBoundFieldPerson()})
|
||||
)
|
||||
expected = """
|
||||
<div><label for="id_first_name">First name::</label>
|
||||
<input type="text" name="first_name" required
|
||||
id="id_first_name"></div>
|
||||
<div><label class="custom-class" for="id_last_name">Last name:</label>
|
||||
<input type="text" name="last_name" required
|
||||
id="id_last_name"></div><div>
|
||||
<label for="id_birthday">Birthday:</label>
|
||||
<input type="text" name="birthday" required
|
||||
id="id_birthday"></div>"""
|
||||
self.assertHTMLEqual(html, expected)
|
||||
|
Loading…
x
Reference in New Issue
Block a user