# DRF Rack # Библиотека классов расширений для Django REST Framework # # Автор: Николай В. Анохин # Все права защищены. Лицензия: MIT """ Описание класса ExtensibleEnumChoiceField """ from typing import Any from typing import Sequence from typing import Type from django.conf import settings from django.core.exceptions import ValidationError from django.db import models from net.xeaf.rack.core.core_enum import CoreEnum class ExtensibleEnumChoiceField(models.CharField): """ Конфигурируемое поле выбора перечисления, класс которого полностью настраивается пользователем через settings.py. """ MAX_DEFAULT_LENGTH = 64 """ Максимальный размер поля по умолчанию """ _settings_key: str """ Ключ в settings.py для получения замещающего класса перечисления """ _active_enum_cls: Type[CoreEnum] """ Активный в текущем рантайме класс перечисления """ def __init__(self, choices: str, *args: Any, **kwargs: Any) -> None: """ Инициализация """ if not isinstance(choices, str): raise TypeError( f"The 'choices' parameter in {self.__class__.__name__} must be a string containing " f"the settings key name, got {type(choices).__name__} instead." ) self._settings_key = choices user_enum = getattr(settings, self._settings_key, None) if user_enum is None: raise ValueError( f"The required configuration setting '{self._settings_key}' " f"is missing or not set in your settings.py!" ) if not isinstance(user_enum, type) or not issubclass(user_enum, CoreEnum): raise TypeError( f"The '{self._settings_key}' setting in settings.py " f"must be a subclass of CoreEnum to maintain strict typing." ) self._active_enum_cls = user_enum kwargs.setdefault('max_length', self.MAX_DEFAULT_LENGTH) kwargs['choices'] = self._active_enum_cls.choices() super().__init__(*args, **kwargs) def _normalize_and_validate(self, value: Any, action_context: str) -> CoreEnum | str | None: """ Внутренний метод для проверки ограничений и приведения значения к активному классу CoreEnum. """ if value is None: if not self.null: raise ValueError(f"Cannot {action_context} null; the field '{self.name}' is not nullable.") return None if value == "": if not self.blank: raise ValueError(f"Cannot {action_context} blank string; the field '{self.name}' does not allow blanks.") return "" if isinstance(value, CoreEnum): if isinstance(value, self._active_enum_cls): return value str_val = value.value else: str_val = str(value) try: return self._active_enum_cls.from_string(str_val) except ValueError: raise ValueError( f"Cannot {action_context} value '{value}'; it cannot be resolved by active " f"enum class '{self._active_enum_cls.__name__}' configured via '{self._settings_key}'." ) # noinspection unused-parameter def from_db_value[E: CoreEnum](self, value: Any, expression: Any, connection: Any) -> E | str | None: """ Преобразует сырое значение из базы данных в объект замещенного перечисления """ return self._normalize_and_validate(value, action_context="read") def to_python[E: CoreEnum](self, value: Any) -> E | str | None: """ Строго преобразует входящее значение в объект замещенного перечисления """ return self._normalize_and_validate(value, action_context="clean") def validate(self, value: Any, model_instance: Any) -> None: """ Валидация значения поля по замещенному списку """ if value is None or value == "": return super().validate(value, model_instance) lookup_value = value.value if isinstance(value, CoreEnum) else value if lookup_value not in self._active_enum_cls.values(): raise ValidationError( self.error_messages['invalid_choice'], code='invalid_choice', params={'value': value}, ) return super().validate(value, model_instance) def get_prep_value(self, value: Any) -> str | None: """ Преобразует объект CoreEnum обратно в строку перед сохранением в БД """ value = super().get_prep_value(value) resolved = self._normalize_and_validate(value, action_context="save") if isinstance(resolved, self._active_enum_cls): return resolved.value return resolved def deconstruct(self) -> tuple[str, str, Sequence[Any], dict[str, Any]]: """ Возвращает параметры для сериализации поля :return: Кортеж с параметрами для сериализации поля """ name, path, args, kwargs = super().deconstruct() kwargs['choices'] = self._settings_key return name, path, args, kwargs