Работа с Enum полями

This commit is contained in:
2026-07-30 20:12:39 +03:00
parent abd999d245
commit 50f6638416
26 changed files with 601 additions and 38 deletions

View File

@@ -0,0 +1,156 @@
# DRF Rack
# Библиотека классов расширений для Django REST Framework
#
# Автор: Николай В. Анохин <n.anokhin@xeaf.net>
# Все права защищены. Лицензия: 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