Skip to content

Commit e219f45

Browse files
Integrate more into Form rendering API
Override BoundField to allow fields to be rendered using '{{ field.as_field_group }}' instead of relying on template tags. This eliminates the need for '{% dsfr_form_field %}'. Also implement Toggle form field
1 parent aca373a commit e219f45

16 files changed

Lines changed: 213 additions & 69 deletions

dsfr/fields.py

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
from django.core.exceptions import ValidationError
2-
from django.forms.fields import MultiValueField, IntegerField
2+
from django.forms.fields import MultiValueField, IntegerField, BooleanField
33

4-
from .widgets import NumberCursor
4+
from .widgets import NumberCursor, Toggle
55

6-
__all__ = ["IntegerRangeField"]
6+
__all__ = ["IntegerRangeField", "ToggleField"]
77

88

99
class IntegerRangeField(MultiValueField):
@@ -49,3 +49,14 @@ def compress(self, data_list: list[int]) -> range:
4949
"Le second nombre doit être supérieur au premier pour déterminer un intervalle"
5050
)
5151
return range(data_list[0], data_list[1] + 1)
52+
53+
54+
class ToggleField(BooleanField):
55+
widget = Toggle
56+
template_name = "dsfr/toggle.html"
57+
58+
def __init__(self, **kwargs):
59+
kwargs["label_suffix"] = ""
60+
super().__init__(**kwargs)
61+
if self.disabled:
62+
self.widget.attrs.setdefault("disabled", "true")

dsfr/forms.py

Lines changed: 73 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import warnings
22
from pathlib import Path
33

4+
import django
45
from django import forms
56
from django.forms.renderers import DjangoTemplates, get_default_renderer
67
from django.utils.functional import cached_property
78

8-
from .utils import dsfr_input_class_attr
9+
from dsfr.utils import dsfr_input_class_attr
910

1011

1112
class DsfrDjangoTemplates(DjangoTemplates):
@@ -24,17 +25,53 @@ def engine(self): # type: ignore
2425
)
2526

2627

28+
class DsfrBoundField(forms.BoundField):
29+
if django.VERSION < (5, 0):
30+
"""
31+
Compatibility with Django<5.0
32+
https://docs.djangoproject.com/en/5.0/ref/forms/api/#django.forms.BoundField.as_field_group
33+
"""
34+
35+
def as_field_group(self):
36+
return self.render()
37+
38+
@property
39+
def template_name(self):
40+
template_name = self.field.template_name or getattr(
41+
self.field.__class__, "template_name", None
42+
)
43+
if template_name:
44+
return template_name
45+
46+
match self.widget_type:
47+
case "checkboxinput":
48+
return "dsfr/form_field_snippets/checkbox_snippet.html"
49+
case "checkboxselectmultiple" | "inlinecheckboxselectmultiple":
50+
return "dsfr/form_field_snippets/checkboxselectmultiple_snippet.html"
51+
case "radioselect" | "inlineradioselect":
52+
return "dsfr/form_field_snippets/radioselect_snippet.html"
53+
case "richradioselect":
54+
return "dsfr/form_field_snippets/richradioselect_snippet.html"
55+
case "numbercursor":
56+
return "dsfr/form_field_snippets/numbercursor_snippet.html"
57+
case "segmentedcontrol":
58+
return "dsfr/form_field_snippets/segmented_control_snippet.html"
59+
case _:
60+
return "dsfr/form_field_snippets/input_snippet.html"
61+
62+
def label_tag(self, contents=None, attrs=None, label_suffix=None, tag=None):
63+
if hasattr(self.field.widget, "dsfr_label_attrs"):
64+
attrs = {**self.field.widget.dsfr_label_attrs, **(attrs or {})}
65+
return super().label_tag(contents, attrs, label_suffix, tag)
66+
67+
2768
class DsfrBaseForm(forms.Form):
2869
"""
2970
A base form that adds the necessary classes on relevant fields
3071
"""
3172

3273
template_name = "dsfr/form_snippet.html" # type: ignore
33-
34-
def __init__(self, *args, **kwargs):
35-
super().__init__(*args, **kwargs)
36-
for visible in self.visible_fields():
37-
dsfr_input_class_attr(visible)
74+
bound_field_class = DsfrBoundField
3875

3976
@property
4077
def default_renderer(self): # type: ignore
@@ -46,6 +83,36 @@ def default_renderer(self): # type: ignore
4683
else get_default_renderer()
4784
)
4885

86+
def __init__(self, *args, **kwargs):
87+
super().__init__(*args, **kwargs)
88+
for visible in self.visible_fields():
89+
dsfr_input_class_attr(visible)
90+
91+
if django.VERSION < (5, 2):
92+
"""
93+
Compatibility with Django<5.2
94+
https://docs.djangoproject.com/en/5.2/releases/5.2/#simplified-override-of-boundfield
95+
"""
96+
97+
def __getitem__(self, name):
98+
try:
99+
field = self.fields[name]
100+
except KeyError:
101+
raise KeyError(
102+
"Key '%s' not found in '%s'. Choices are: %s."
103+
% (
104+
name,
105+
self.__class__.__name__,
106+
", ".join(sorted(self.fields)),
107+
)
108+
)
109+
if name not in self._bound_fields_cache:
110+
bound_field_class = getattr(
111+
self, "bound_field_class", self.bound_field_class
112+
)
113+
self._bound_fields_cache[name] = bound_field_class(self, field, name)
114+
return self._bound_fields_cache[name]
115+
49116
def get_context(self) -> dict:
50117
from django.conf import settings
51118

dsfr/templates/dsfr/form_base.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
{# Everything that needs to be in the for loop #}
3737
{% block inside_form %}
3838
{% endblock inside_form %}
39-
{% include "dsfr/form_field_snippets/field_snippet.html" %}
39+
{{ field.as_field_group }}
4040
{% endfor %}
4141
{# If you need to add formsets after the form #}
4242
{% block extra_formset_after %}

dsfr/templates/dsfr/form_field_snippets/checkbox_snippet.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
{{ field|dsfr_input_class_attr|attr:"type:checkbox" }}
44
<label for="{{ field.id_for_label }}" class="fr-label">
55
{# djlint:off #}
6-
{{ field.label }}{% if not DSFR_MARK_OPTIONAL_FIELDS and field.field.required %}<span class="fr-required-marker" aria-hidden="true"> *</span>{% elif DSFR_MARK_OPTIONAL_FIELDS and not field.field.required %} {% translate '(Optional)' %}{% endif %}
6+
{{ field.label }}{% dsfr_mark_optionnal_fields field %}
77
{% if field.help_text %}
88
<span class="fr-hint-text" id="{{ field.auto_id }}_helptext">{{ field.help_text|safe }}</span>
99
{% endif %}

dsfr/templates/dsfr/form_field_snippets/checkboxselectmultiple_snippet.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<legend class="fr-fieldset__legend fr-fieldset__legend--regular"
66
id="{{ field.auto_id }}-legend">
77
{# djlint:off #}
8-
{{ field.label }}{% if not DSFR_MARK_OPTIONAL_FIELDS and field.field.required %}<span class="fr-required-marker" aria-hidden="true"> *</span>{% elif DSFR_MARK_OPTIONAL_FIELDS and not field.field.required %} {% translate '(Optional)' %}{% endif %}
8+
{{ field.label }}{% dsfr_mark_optionnal_fields field %}
99
{% if field.help_text %}
1010
<span class="fr-hint-text" id="{{ field.auto_id }}_helptext">{{ field.help_text|safe }}</span>
1111
{% endif %}

dsfr/templates/dsfr/form_field_snippets/field_snippet.html

Lines changed: 0 additions & 18 deletions
This file was deleted.

dsfr/templates/dsfr/form_field_snippets/input_snippet.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div class="{{ field.field.widget.group_class|default:'fr-input-group' }}{% if field.errors %} {{ field.field.widget.group_class|default:'fr-input-group' }}--error{% endif %}{% if field.field.disabled %} fr-input-group--disabled{% endif %}">
44
<label for="{{ field.id_for_label }}" class="fr-label">
55
{# djlint:off #}
6-
{{ field.label }}{% if not DSFR_MARK_OPTIONAL_FIELDS and field.field.required %}<span class="fr-required-marker" aria-hidden="true"> *</span>{% elif DSFR_MARK_OPTIONAL_FIELDS and not field.field.required %} {% translate '(Optional)' %}{% endif %}
6+
{{ field.label }}{% dsfr_mark_optionnal_fields field %}
77
{% if field.help_text %}
88
<span class="fr-hint-text" id="{{ field.id_for_label }}_helptext">{{ field.help_text|safe }}</span>
99
{% endif %}

dsfr/templates/dsfr/form_field_snippets/numbercursor_snippet.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<div class="fr-range-group {% if field.errors %}fr-range-group--error{% endif %} {% if field.field.disabled %}fr-range-group--disabled{% endif %}">
44
<label for="{{ field.id_for_label }}" class="fr-label">
55
{# djlint:off #}
6-
{{ field.label }}{% if not DSFR_MARK_OPTIONAL_FIELDS and field.field.required %}<span class="fr-required-marker" aria-hidden="true"> *</span>{% elif DSFR_MARK_OPTIONAL_FIELDS and not field.field.required %} {% translate '(Optional)' %}{% endif %}
6+
{{ field.label }}{% dsfr_mark_optionnal_fields field %}
77
{% if field.help_text %}
88
<span class="fr-hint-text" id="{{ field.id_for_label }}_helptext">
99
{{ field.help_text|safe }}, valeur

dsfr/templates/dsfr/form_field_snippets/radioselect_snippet.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
<legend class="fr-fieldset__legend fr-fieldset__legend--regular"
66
id="{{ field.auto_id }}-legend">
77
{# djlint:off #}
8-
{{ field.label }}{% if not DSFR_MARK_OPTIONAL_FIELDS and field.field.required %}<span class="fr-required-marker" aria-hidden="true"> *</span>{% elif DSFR_MARK_OPTIONAL_FIELDS and not field.field.required %} {% translate '(Optional)' %}{% endif %}
8+
{{ field.label }}{% dsfr_mark_optionnal_fields field %}
99
{% if field.help_text %}
1010
<span class="fr-hint-text" id="{{ field.auto_id }}_helptext">{{ field.help_text|safe }}</span>
1111
{% endif %}

dsfr/templates/dsfr/form_snippet.html

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,9 @@
55
</section>
66
{% endif %}
77
{% for field in form %}
8-
{% dsfr_form_field field %}
8+
{% if field.is_hidden %}
9+
{{ field.as_hidden }}
10+
{% else %}
11+
{{ field.as_field_group }}
12+
{% endif %}
913
{% endfor %}

0 commit comments

Comments
 (0)