diff --git a/dsfr/enums.py b/dsfr/enums.py index b0df178fd..fdd7fa011 100644 --- a/dsfr/enums.py +++ b/dsfr/enums.py @@ -1,6 +1,3 @@ -import functools -from typing import Any - from django.db import models from django.db.models import TextChoices from django.utils.safestring import mark_safe @@ -12,13 +9,53 @@ from types import DynamicClassAttribute as enum_property +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] != "_" + ) + + +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 _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() + @classmethod + def __prepare__(metacls, cls, bases, **kwds): + classdict = super().__prepare__(cls, bases, **kwds) + + class _EnumDict(classdict.__class__): + def __init__(self, old_classdict): + super().__init__() + self._additional_attributes = {} + # Copy absent old_classdict members into self + for member_name, member_value in old_classdict.__dict__.items(): + self.__dict__.setdefault(member_name, member_value) + + # Copy dict content + self.update(old_classdict) + + def __setitem__(self, member, value): + """Allows to handle declaring enum members as dicts""" + # _additional_attributes is also a dict, but we don't want it to be + # processed like an enum member + if not isinstance(value, dict): + return super().__setitem__(member, value) + if "value" not in value: raise ValueError( "enum value for {member} should contain member 'value' " @@ -27,92 +64,82 @@ def __new__(metacls, classname, bases, classdict, **kwds): ) ) - dict.__setitem__(classdict, member, metacls.get_value_from_dict(value)) - value.pop("value") - value.pop("label", None) + if "label" in value: + super().__setitem__( + member, (value.pop("value"), value.pop("label")) + ) + else: + super().__setitem__(member, value.pop("value")) - for k, v in value.items(): - if metacls.is_sunder(k) or metacls.is_dunder(k): + for attr_name, attr_value in value.items(): + if _is_sunder(attr_name) or _is_dunder(attr_name): 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) + ).format(member=member, key=attr_name) ) - 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) + self._additional_attributes.setdefault(attr_name, {}) + self._additional_attributes[attr_name][member] = attr_value - metacls.set_dynamic_attributes(cls, dynamic_attributes) + return _EnumDict(classdict) - return cls + def __new__(metacls, classname, bases, classdict, **kwds): - @staticmethod - def set_dynamic_attributes(cls, dynamic_attributes: dict[str, dict[str, Any]]): - cls.NO_VALUE = object() + cls = super().__new__(metacls, classname, bases, classdict, **kwds) + cls._additional_attributes = list(classdict._additional_attributes.keys()) - for k, v in dynamic_attributes.items(): - variable = "_{}_".format(k) + for attr_name, attr_value in classdict._additional_attributes.items(): + private_name = "__{}".format(attr_name) for instance in cls: - if hasattr(instance, variable): + if hasattr(instance, private_name): raise ValueError( ( - "Can't set {} on {} members; please choose a different name " + "Can't set {} on {}.{}; please choose a different name " "or remove from the member value" - ).format(variable, cls.__name__) + ).format(attr_name, cls.__name__, instance.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] != "_" - ) + if instance.name in attr_value: + setattr(instance, private_name, attr_value[instance.name]) + + def default_dynamic_attribute_value(enum_item, name): + raise NotImplementedError( + ( + "{}.{} does not contain key {}. Please add key or implement " + "a 'dynamic_attribute_value(cls, instance, name)' classmethod in you enum " + "to provide the value" + ).format(cls.__name__, instance.name, cls.__name__) + ) + + def instance_property_getter(name, default_value_getter): + @enum_property + def _instance_property_getter(enum_item): + if hasattr(enum_item, private_name): + return getattr(enum_item, private_name) + else: + return default_value_getter(enum_item, name) + + return _instance_property_getter + + setattr( + cls, + attr_name, + instance_property_getter( + attr_name, + classdict.get( + "dynamic_attribute_value", + default_dynamic_attribute_value, + ), + ), + ) - @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] != "_" - ) + return cls + + @property + def additional_attributes(cls): + """Enum additionnal attributes that were set from dict""" + return cls._additional_attributes class ExtendedChoices(models.Choices, metaclass=_ExtendedChoicesType): @@ -122,12 +149,10 @@ class ExtendedChoices(models.Choices, metaclass=_ExtendedChoicesType): class RichRadioButtonChoices(ExtendedChoices, TextChoices): @enum_property def pictogram(self): - return self._pictogram_ if hasattr(self, "_pictogram_") else "" + 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 + mark_safe(self._html_label) if hasattr(self, "_html_label") else self.label ) diff --git a/dsfr/templates/dsfr/widgets/radio_option.html b/dsfr/templates/dsfr/widgets/radio_option.html new file mode 100644 index 000000000..0b8fa01fc --- /dev/null +++ b/dsfr/templates/dsfr/widgets/radio_option.html @@ -0,0 +1,15 @@ +