157 lines
5.8 KiB
Python
157 lines
5.8 KiB
Python
# 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
|