Skip to content

Commit

Permalink
Finish rich radio button implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
christophehenry committed Sep 18, 2024
1 parent a7557f7 commit 6587d89
Show file tree
Hide file tree
Showing 7 changed files with 150 additions and 55 deletions.
51 changes: 33 additions & 18 deletions dsfr/enums.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from django.db import models
from django.db.models import TextChoices
from django.utils.safestring import mark_safe
from django.utils.version import PY311

Expand All @@ -13,9 +12,7 @@


def _is_dunder(name):
"""
Returns True if a __dunder__ name, False otherwise.
"""
"""Returns True if a __dunder__ name, False otherwise."""
return (
len(name) > 4
and name[:2] == name[-2:] == "__"
Expand All @@ -25,9 +22,7 @@ def _is_dunder(name):


def _is_sunder(name):
"""
Returns True if a _sunder_ name, False otherwise.
"""
"""Returns True if a _sunder_ name, False otherwise."""
return (
len(name) > 2
and name[0] == name[-1] == "_"
Expand Down Expand Up @@ -93,21 +88,32 @@ def __new__(metacls, classname, bases, classdict, **kwds):
cls._additional_attributes = list(classdict._additional_attributes.keys())

for attr_name, attr_value in classdict._additional_attributes.items():
private_name = "__{}".format(attr_name)
for instance in cls:
if hasattr(instance, private_name):
if hasattr(instance, cls.private_variable_name(attr_name)):
raise ValueError(
(
"Can't set {} on {}.{}; please choose a different name "
"or remove from the member value"
).format(attr_name, cls.__name__, instance.name)
"Can't set '{}' on {}.{}; instance already has a private "
"'{}' attribute; please choose a different name or remove "
"from the member value"
).format(
attr_name,
cls.__name__,
instance.name,
cls.private_variable_name(attr_name),
)
)

if instance.name in attr_value:
setattr(instance, private_name, attr_value[instance.name])
setattr(
instance,
cls.private_variable_name(attr_name),
attr_value[instance.name],
)

def instance_property_getter(name):
@enum_property
def _instance_property_getter(enum_item):
private_name = enum_item.private_variable_name(name)
if hasattr(enum_item, private_name):
return getattr(enum_item, private_name)
elif hasattr(enum_item, "dynamic_attribute_value"):
Expand All @@ -123,7 +129,8 @@ def _instance_property_getter(enum_item):

return _instance_property_getter

setattr(cls, attr_name, instance_property_getter(attr_name))
if not hasattr(instance, attr_name):
setattr(cls, attr_name, instance_property_getter(attr_name))

return cls

Expand All @@ -134,16 +141,24 @@ def additional_attributes(cls):


class ExtendedChoices(models.Choices, metaclass=_ExtendedChoicesType):
...
@staticmethod
def private_variable_name(name):
return f"__{name}"


class RichRadioButtonChoices(ExtendedChoices, TextChoices):
class RichRadioButtonChoices(ExtendedChoices):
@enum_property
def pictogram(self):
return self._pictogram if hasattr(self, "_pictogram") else ""
return getattr(self, self.private_variable_name("pictogram"), "")

@enum_property
def pictogram_alt(self):
return getattr(self, self.private_variable_name("pictogram_alt"), None)

@enum_property
def html_label(self):
return (
mark_safe(self._html_label) if hasattr(self, "_html_label") else self.label
mark_safe(getattr(self, self.private_variable_name("html_label")))
if hasattr(self, self.private_variable_name("html_label"))
else self.label
)
27 changes: 20 additions & 7 deletions dsfr/templates/dsfr/widgets/rich_radio.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,22 @@
{% with id=widget.attrs.id %}<fieldset{% if id %} id="{{ id }}"{% endif %} class="fr-fieldset{% if widget.attrs.class %} {{ widget.attrs.class }}{% endif %}">
{% for group, options, index in widget.optgroups %}
{% if group %}<div><legend class="fr-fieldset__legend{% if widget.attrs.legend_classes %} {{ widget.attrs.legend_classes }}{% endif %}">{{ group }}</legend>{% endif %}
{% with id=widget.attrs.id %}
<fieldset
{% if id %}id="{{ id }}"{% endif %}
class="fr-fieldset{% if widget.attrs.class %} {{ widget.attrs.class }}{% endif %}"
{% if id %}aria-labelledby="{{ id }}-legend"{% endif %}
>
{% for group, options, index in widget.optgroups %}
{% if group %}
<legend
id="{{ group }}-legend"
class="fr-fieldset__legend{% if widget.attrs.legend_classes %} {{ widget.attrs.legend_classes }}{% endif %}"
>
{{ group }}
</legend>
<fieldset aria-labelledby="{{ group }}-legend">
{% endif %}
{% for option in options %}
<div>{% include option.template_name with widget=option %}</div>
{% include option.template_name with widget=option %}
{% endfor %}
{% if group %}</div>{% endif %}
{% endfor %}
</fieldset>{% endwith %}
{% if group %}</fieldset>{% endif %}
{% endfor %}
</fieldset>{% endwith %}
29 changes: 14 additions & 15 deletions dsfr/templates/dsfr/widgets/rich_radio_option.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
<div class="fr-fieldset__element">
<div class="fr-radio-group fr-radio-rich">
<input value="1" type="radio" id="radio-rich-1" name="radio-rich">
<label class="fr-label" for="radio-rich-1">
Libellé bouton radio
</label>
<div class="fr-radio-rich__pictogram">
<svg aria-hidden="true" class="fr-artwork" viewBox="0 0 80 80" width="80px" height="80px">
<use class="fr-artwork-decorative" href="../../../dist/artwork/pictograms/buildings/city-hall.svg#artwork-decorative"></use>
<use class="fr-artwork-minor" href="../../../dist/artwork/pictograms/buildings/city-hall.svg#artwork-minor"></use>
<use class="fr-artwork-major" href="../../../dist/artwork/pictograms/buildings/city-hall.svg#artwork-major"></use>
</svg>
</div>
</div>
</div>
<div class="fr-fieldset__element{% if widget.inline %} fr-fieldset__element--inline{% endif %}">
<div class="fr-radio-group fr-radio-rich">
<input value="{{ widget.value }}" type="{{ widget.type }}" id="{{ widget.attrs.id }}" name="{{ widget.name }}">
<label class="fr-label" for="{{ widget.attrs.id }}">{{ widget.html_label }}</label>
{% if widget.pictogram %}
<div class="fr-radio-rich__pictogram">
<img
src="{{ widget.pictogram }}"
{% if widget.pictogram_alt is not None %} alt="{{ widget.pictogram_alt }}"{% endif %}
/>
</div>
{% endif %}
</div>
</div>
60 changes: 56 additions & 4 deletions dsfr/test/test_enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,16 @@

from django.db.models import IntegerChoices
from django.test import SimpleTestCase
from django.utils.safestring import mark_safe
from django.utils.version import PY311

from dsfr.enums import ExtendedChoices

if PY311:
from enum import property as enum_property
else:
from types import DynamicClassAttribute as enum_property


class ExtendedChoicesTestCase(SimpleTestCase):
def test_create_dict_enum(self):
Expand All @@ -29,20 +36,26 @@ def test_additional_attributes(self):
class TestExtendedChoices(ExtendedChoices, IntegerChoices):
TEST_1 = {
"value": auto(),
"additionnal_attribute": {"lorem": "ipsum 1"},
"additionnal_attribute_1": {"lorem": "ipsum 1"},
"additionnal_attribute_2": mark_safe("<strong>Item 1</strong>"),
}
TEST_2 = {
"value": auto(),
"additionnal_attribute": {"lorem": "ipsum 2"},
"additionnal_attribute_1": {"lorem": "ipsum 2"},
"additionnal_attribute_2": mark_safe("<strong>Item 2</strong>"),
}

self.assertEqual(
{"additionnal_attribute"},
{"additionnal_attribute_1", "additionnal_attribute_2"},
set(TestExtendedChoices.additional_attributes),
)
self.assertEqual(
[{"lorem": "ipsum 1"}, {"lorem": "ipsum 2"}],
[it.additionnal_attribute for it in TestExtendedChoices],
[it.additionnal_attribute_1 for it in TestExtendedChoices],
)
self.assertEqual(
{"<strong>Item 1</strong>", "<strong>Item 2</strong>"},
{it.additionnal_attribute_2 for it in TestExtendedChoices},
)

def test_nonmember_attributes(self):
Expand Down Expand Up @@ -113,3 +126,42 @@ class TestExtendedChoices(ExtendedChoices, IntegerChoices):
"method in you enum to provide a value",
str(e.exception),
)

def test_uses_overriden_getter(self):
class TestExtendedChoices(ExtendedChoices, IntegerChoices):
TEST_1 = {
"value": auto(),
"additionnal_attribute": {"lorem": "ipsum 1"},
}
TEST_2 = auto()

@enum_property
def additionnal_attribute(self):
return "overriden attribute"

self.assertEqual(
["overriden attribute", "overriden attribute"],
[it.additionnal_attribute for it in TestExtendedChoices],
)

def test_uses_additional_attribute_private_name(self):
class TestExtendedChoices(ExtendedChoices, IntegerChoices):
TEST_1 = {
"value": auto(),
"additionnal_attribute_1": {"lorem": "ipsum 1"},
"additionnal_attribute_2": mark_safe("<strong>Item 1</strong>"),
}
TEST_2 = {
"value": auto(),
"additionnal_attribute_1": {"lorem": "ipsum 2"},
"additionnal_attribute_2": mark_safe("<strong>Item 2</strong>"),
}

@staticmethod
def private_variable_name(name):
return f"m_{name}"

self.assertEqual(
{"<strong>Item 1</strong>", "<strong>Item 2</strong>"},
{it.m_additionnal_attribute_2 for it in TestExtendedChoices},
)
2 changes: 1 addition & 1 deletion dsfr/utils.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import functools

from django.conf.urls.static import static
from django.forms import BoundField, widgets
from django.core.paginator import Page
from django.templatetags.static import static
from django.utils.functional import keep_lazy_text
from django.utils.text import slugify
import random
Expand Down
23 changes: 14 additions & 9 deletions dsfr/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
from dsfr.enums import RichRadioButtonChoices


__all__ = ["RichRadioSelect", "RichCheckboxSelect"]
__all__ = ["RichRadioSelect"]


class _RichChoiceWidget(ChoiceWidget):
def __init__(self, rich_choices: Type[RichRadioButtonChoices], attrs=None):
inline = False

def __init__(
self, rich_choices: Type[RichRadioButtonChoices], inline=None, attrs=None
):
super().__init__(attrs)
self.rich_choices = rich_choices
if inline is not None:
self.inline = inline

@property
def choices(self):
Expand All @@ -33,9 +39,12 @@ def __deepcopy__(self, memo):
def create_option(
self, name, value, label, selected, index, subindex=None, attrs=None
):
opt = super().create_option(
name, value, label, selected, index, subindex, attrs
)
opt = {
**super().create_option(
name, value, label, selected, index, subindex, attrs
),
"inline": self.inline,
}

opt.update(
{
Expand All @@ -50,7 +59,3 @@ def create_option(
class RichRadioSelect(_RichChoiceWidget, RadioSelect):
template_name = "dsfr/widgets/rich_radio.html"
option_template_name = "dsfr/widgets/rich_radio_option.html"


class RichCheckboxSelect(_RichChoiceWidget, CheckboxSelectMultiple):
...
13 changes: 12 additions & 1 deletion example_app/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
inlineformset_factory,
) # /!\ In order to use formsets
from django.templatetags.static import static
from django.db.models import IntegerChoices

from django.utils.functional import keep_lazy_text

from dsfr.constants import COLOR_CHOICES, COLOR_CHOICES_ILLUSTRATION
Expand All @@ -23,12 +25,13 @@
from example_app.models import Author, Book
from example_app.utils import populate_genre_choices


class Lol(TextChoices):
WEEE = auto(), "Weee"
OOOH = auto(), "Oooh"


class ExampleRichChoices(RichRadioButtonChoices):
class ExampleRichChoices(RichRadioButtonChoices, IntegerChoices):
ITEM_1 = {
"value": auto(),
"label": "Item 1",
Expand Down Expand Up @@ -136,6 +139,14 @@ class ExampleForm(DsfrBaseForm):
widget=RichRadioSelect(rich_choices=ExampleRichChoices),
)

inline_rich_radio = forms.ChoiceField(
label="Cases à cocher",
required=False,
choices=ExampleRichChoices.choices,
help_text="Exemple de boutons radios riches en ligne",
widget=RichRadioSelect(rich_choices=ExampleRichChoices, inline=True),
)

# text blocks
sample_comment = forms.CharField(widget=forms.Textarea, required=False)

Expand Down

0 comments on commit 6587d89

Please sign in to comment.