diff --git a/dsfr/models.py b/dsfr/models.py index 7f2af0173..4399a6f4c 100644 --- a/dsfr/models.py +++ b/dsfr/models.py @@ -1,144 +1,13 @@ -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"] diff --git a/dsfr/test/test_enums.py b/dsfr/test/test_enums.py index d78152d5c..a9fda7630 100644 --- a/dsfr/test/test_enums.py +++ b/dsfr/test/test_enums.py @@ -1,4 +1,5 @@ -from enum import auto, nonmember +from enum import auto +from unittest import skipIf from django.db.models import IntegerChoices from django.test import SimpleTestCase @@ -8,7 +9,7 @@ from dsfr.enums import ExtendedChoices if PY311: - from enum import property as enum_property + from enum import property as enum_property, nonmember else: from types import DynamicClassAttribute as enum_property @@ -62,6 +63,7 @@ class TestExtendedChoices(ExtendedChoices, IntegerChoices): {it.additionnal_attribute_2 for it in TestExtendedChoices}, ) + @skipIf(not PY311, "'enum.nonmember' was added to Python 3.11") def test_nonmember_attributes(self): class TestExtendedChoices(ExtendedChoices, IntegerChoices): TEST_1 = {"value": auto()} diff --git a/dsfr/test/test_models.py b/dsfr/test/test_models.py deleted file mode 100644 index 2fe477841..000000000 --- a/dsfr/test/test_models.py +++ /dev/null @@ -1,126 +0,0 @@ -from enum import auto - -from django.db.models import TextChoices -from django.test import SimpleTestCase -from django.utils.safestring import mark_safe - -from dsfr import models -from dsfr.models import RichRadioButton - - -class ExtendedChoicesTestCase(SimpleTestCase): - def test_class_must_contain_value(self): - with self.assertRaises(ValueError): - - class TestExtendedChoices(models.ExtendedChoices): - ITEM_1 = {"label": "Item 1"} - - def test_class_must_contain_no_special_value(self): - with self.subTest("No _sunder_"): - with self.assertRaises(ValueError): - class TestExtendedChoices(models.ExtendedChoices): - ITEM_1 = {"value": "Item_1", "_label_": "Item 1"} - - with self.subTest("No __dunder_"): - with self.assertRaises(ValueError): - class TestExtendedChoices2(models.ExtendedChoices): - ITEM_1 = {"value": "Item_1", "__label__": "Item 1"} - - def test_class_declaration_no_parent(self): - class TestExtendedChoices(models.ExtendedChoices): - ITEM_1 = { - "value": "Item_1", - "label": "Item 1", - "img": "/static/images/item1.png", - "html_label": mark_safe("Item 1"), - } - ITEM_2 = { - "value": "Item_2", - "label": "Item 2", - "img": "/static/images/item1.png", - } - ITEM_3 = "ITEM_3", "Item 3" - ITEM_4 = "ITEM_4" - ITEM_5 = auto(), "Item 5" - - self.assertSetEqual( - { - (("Item_1",), "Item 1"), - (("Item_2",), "Item 2"), - ("ITEM_4", "Item 4"), - (("ITEM_3",), "Item 3"), - ((1,), "Item 5"), - }, - set(TestExtendedChoices.choices), - ) - - def test_class_declaration_TextChoices(self): - class TestExtendedChoices(models.ExtendedChoices, TextChoices): - ITEM_1 = { - "value": "Item_1", - "label": "Item 1", - "img": "/static/images/item1.png", - "html_label": mark_safe("Item 1"), - } - ITEM_2 = { - "value": "Item_2", - "label": "Item 2", - "img": "/static/images/item1.png", - } - ITEM_3 = "ITEM_3", "Item 3" - ITEM_4 = "ITEM_4" - ITEM_5 = auto(), "Item 5" - - self.assertSetEqual( - { - ("Item_1", "Item 1"), - ("Item_2", "Item 2"), - ("ITEM_3", "Item 3"), - ("ITEM_4", "Item 4"), - ("ITEM_5", "Item 5"), - }, - set(TestExtendedChoices.choices), - ) - - def test_dynamic_properties(self): - class TestExtendedChoices(models.ExtendedChoices): - ITEM_1 = { - "value": "Item_1", - "label": "Item 1", - "img": "/static/images/item1.png", - "html_label": mark_safe("Item 1"), - } - ITEM_2 = { - "value": "Item_2", - "label": "Item 2", - "img": "/static/images/item1.png", - } - ITEM_3 = "ITEM_3", "Item 3" - ITEM_4 = "ITEM_4" - ITEM_5 = auto(), "Item 5" - - self.assertEqual( - TestExtendedChoices.ITEM_2.img, - "/static/images/item1.png", - ) - - with self.assertRaises(AttributeError): - TestExtendedChoices.ITEM_3.img - - def test_rich_radio_button(self): - class TestRichRadioButton(RichRadioButton): - ITEM_1 = { - "value": "Item_1", - "label": "Item 1", - "pictogram": "/static/images/item1.png", - "html_label": mark_safe("Item 1"), - } - ITEM_2 = { - "value": "Item_2", - "label": "Item 2", - "pictogram": "/static/images/item2.png", - "html_label": mark_safe("Item 2"), - } - - self.assertEqual("/static/images/item1.png", TestRichRadioButton.ITEM_1.pictogram) - self.assertEqual("/static/images/item2.png", TestRichRadioButton.ITEM_2.pictogram) \ No newline at end of file