Skip to content

Commit

Permalink
Implement rich radio buttons DSFR component
Browse files Browse the repository at this point in the history
  • Loading branch information
christophehenry committed Aug 28, 2024
1 parent 121843e commit f4d87da
Show file tree
Hide file tree
Showing 4 changed files with 409 additions and 1 deletion.
133 changes: 133 additions & 0 deletions dsfr/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import functools
from typing import Any

from django.db import models
from django.db.models import TextChoices
from django.utils.safestring import mark_safe
from django.utils.version import PY311

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


class _ExtendedChoicesType(models.enums.ChoicesType):
def __new__(metacls, classname, bases, classdict, **kwds):
dynamic_attributes = {}
for member in classdict._member_names:
value = classdict[member]
if isinstance(value, dict):
value = value.copy()
if "value" not in value:
raise ValueError(
"enum value for {member} should contain member 'value' "
"when using a dict as value; got {member} = {value}".format(
member=member, value=repr(value)
)
)

dict.__setitem__(classdict, member, metacls.get_value_from_dict(value))
value.pop("value")
value.pop("label", None)

for k, v in value.items():
if metacls.is_sunder(k) or metacls.is_dunder(k):
raise ValueError(
(
"enum value for {member} contains key {key}. "
"Names surrounded with single or double underscores are "
"not authorized as dict values"
).format(member=member, key=k)
)
dynamic_attributes.setdefault(k, {})
dynamic_attributes[k][member] = v

classdict._last_values = [
metacls.get_value_from_dict(item) for item in classdict._last_values
]

cls = super().__new__(metacls, classname, bases, classdict, **kwds)

metacls.set_dynamic_attributes(cls, dynamic_attributes)

return cls

@staticmethod
def set_dynamic_attributes(cls, dynamic_attributes: dict[str, dict[str, Any]]):
cls.NO_VALUE = object()

for k, v in dynamic_attributes.items():
variable = "_{}_".format(k)
for instance in cls:
if hasattr(instance, variable):
raise ValueError(
(
"Can't set {} on {} members; please choose a different name "
"or remove from the member value"
).format(variable, cls.__name__)
)
setattr(instance, variable, v.get(instance.name, cls.NO_VALUE))

def _getter(name, self):
result = getattr(self, name, cls.NO_VALUE)
if result is cls.NO_VALUE:
raise AttributeError(
"{} not present in {}.{}".format(
variable, cls.__name__, self.name
)
)
return result

setattr(cls, k, enum_property(functools.partial(_getter, variable)))

@staticmethod
def get_value_from_dict(value):
if not isinstance(value, dict):
return value
elif "label" in value:
return value["value"], value["label"]
else:
return value["value"]

@staticmethod
def is_dunder(name):
"""
Returns True if a __dunder__ name, False otherwise.
"""
return (
len(name) > 4
and name[:2] == name[-2:] == "__"
and name[2] != "_"
and name[-3] != "_"
)

@staticmethod
def is_sunder(name):
"""
Returns True if a _sunder_ name, False otherwise.
"""
return (
len(name) > 2
and name[0] == name[-1] == "_"
and name[1:2] != "_"
and name[-2:-1] != "_"
)


class ExtendedChoices(models.Choices, metaclass=_ExtendedChoicesType):
...


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

@enum_property
def html_label(self):
return (
mark_safe(self._html_label_)
if hasattr(self, "_html_label_")
else self.label
)
133 changes: 132 additions & 1 deletion dsfr/models.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,144 @@
import functools
import os

from typing import Any

from django.core.exceptions import ValidationError
from django.db import models
from django.db.models import TextChoices
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.utils.version import PY311

from dsfr.constants import DJANGO_DSFR_LANGUAGES, NOTICE_TYPE_CHOICES


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


class _ExtendedChoicesType(models.enums.ChoicesType):
def __new__(metacls, classname, bases, classdict, **kwds):
dynamic_attributes = {}
for member in classdict._member_names:
value = classdict[member]
if isinstance(value, dict):
value = value.copy()
if "value" not in value:
raise ValueError(
"enum value for {member} should contain member 'value' "
"when using a dict as value; got {member} = {value}".format(
member=member, value=repr(value)
)
)

dict.__setitem__(classdict, member, metacls.get_value_from_dict(value))
value.pop("value")
value.pop("label", None)

for k, v in value.items():
if metacls.is_sunder(k) or metacls.is_dunder(k):
raise ValueError(
(
"enum value for {member} contains key {key}. "
"Names surrounded with single or double underscores are "
"not authorized as dict values"
).format(member=member, key=k)
)
dynamic_attributes.setdefault(k, {})
dynamic_attributes[k][member] = v

classdict._last_values = [
metacls.get_value_from_dict(item) for item in classdict._last_values
]

cls = super().__new__(metacls, classname, bases, classdict, **kwds)

metacls.set_dynamic_attributes(cls, dynamic_attributes)

return cls

@staticmethod
def set_dynamic_attributes(cls, dynamic_attributes: dict[str, dict[str, Any]]):
cls.NO_VALUE = object()

for k, v in dynamic_attributes.items():
variable = "_{}_".format(k)
for instance in cls:
if hasattr(instance, variable):
raise ValueError(
(
"Can't set {} on {} members; please choose a different name "
"or remove from the member value"
).format(variable, cls.__name__)
)
setattr(instance, variable, v.get(instance.name, cls.NO_VALUE))

def _getter(name, self):
result = getattr(self, name, cls.NO_VALUE)
if result is cls.NO_VALUE:
raise AttributeError(
"{} not present in {}.{}".format(
variable, cls.__name__, self.name
)
)
return result

setattr(cls, k, enum_property(functools.partial(_getter, variable)))

@staticmethod
def get_value_from_dict(value):
if not isinstance(value, dict):
return value
elif "label" in value:
return value["value"], value["label"]
else:
return value["value"]

@staticmethod
def is_dunder(name):
"""
Returns True if a __dunder__ name, False otherwise.
"""
return (
len(name) > 4
and name[:2] == name[-2:] == "__"
and name[2] != "_"
and name[-3] != "_"
)

@staticmethod
def is_sunder(name):
"""
Returns True if a _sunder_ name, False otherwise.
"""
return (
len(name) > 2
and name[0] == name[-1] == "_"
and name[1:2] != "_"
and name[-2:-1] != "_"
)


class ExtendedChoices(models.Choices, metaclass=_ExtendedChoicesType):
...


class RichRadioButton(ExtendedChoices, TextChoices):
@enum_property
def pictogram(self):
return self._pictogram_ if hasattr(self, "_pictogram_") else ""

@enum_property
def html_label(self):
return (
mark_safe(self._html_label_)
if hasattr(self, "_html_label_")
else self.label
)


def validate_image_extension(value):
ext = os.path.splitext(value.name)[1] # [0] returns path+filename
valid_extensions = [".jpg", ".jpeg", ".png", ".svg"]
Expand Down
Loading

0 comments on commit f4d87da

Please sign in to comment.