Как добавить дополнительные данные в TextChoices?
Как добавить дополнительные данные в django.db.models.TextChoices?
class Fruit(models.TextChoices):
APPLE = ('myvalue', True, 'mylabel')
таким образом, что:
>>> Fruit.APPLE.is_tasty
True
>>> # And it still works otherwise
>>> Fruit.APPLE.value
'myvalue'
>>> Fruit.APPLE.label
'mylabel'
Вам нужно сделать нечто подобное тому, что предлагается в документации python Enum docs, но в отличие от python Enum, о метке уже позаботились models.Choices:
class Fruit(models.TextChoices):
APPLE = ('myvalue', True, 'mylabel')
def __new__(cls, value, is_tasty):
obj = str.__new__(cls, value)
obj._value_ = value
obj.is_tasty = is_tasty
return obj
Если вы используете это на IntegerChoices, вам понадобится int.__new__. Если вы используете __init__ вместо __new__, значение перечисления станет ('myvalue', True), которое используется в Fruit.choices и, вероятно, не подойдет для вашей модели Field.
Обратите внимание, что когда перечисление choices используется для поля модели, вы никогда не передаете ему перечисление, поэтому оно не знает о перечислении. Например, поле формы, производное от него с помощью ModelForm, будет рассматривать их как значения str, и после POST значение поля будет обычным str, а не значением перечисления. Для случая формы вы можете определить MyForm.clean_fruit или предоставить поле формы вручную TypedChoiceField(coerce=Fruit), в других случаях вам, возможно, придется снова искать значение перечисления с помощью Fruit(value) или вы можете добавить этот миксин к вашему полю:
class EnumMixin:
'Convert a DB value back to its Choices value'
def __init__(self, *args, enum : models.Choices, **kwargs):
self.__enum = enum
# it sets choices for you using the enum
super().__init__(*args, choices=enum.choices, **kwargs)
def deconstruct(self):
'Get constructor args to reconstruct this field with later'
name, path, args, kwargs = super().deconstruct()
kwargs['enum'] = self.__enum
del kwargs['choices']
return name, path, args, kwargs
def from_db_value(self, value, expression, connection):
# Convert from db value
return self.__to_enum(value)
def to_python(self, value):
'Called by deserialization and during clean() method used in forms'
return self.__to_enum(value)
def __to_enum(self, value):
if value is None:
return None
return self.__enum(value)
class EnumCharField(EnumMixin, models.CharField):
pass
class MyModel(models.Model):
field = EnumCharField(enum=Fruit, ...)
deconstruct используется миграциями django, но обратите внимание, что он не будет восстанавливать Enum в момент создания миграции, он будет использовать тот Enum, который у вас есть на момент применения миграции.
Почему бы не вызвать super?
Хотя super().__new__ работает нормально в общем случае, это не относится к подклассам Enum (models.TextChoices является models.Choices, который является Enum). В документации Python отмечается следующее:
Метод
__new__(), если он определен, используется при создании членов Enum; затем он заменяется методом__new__()Enum, который используется после создания класса для поиска существующих членов.
Итак, EnumMeta заменяет __new__ класса на Enum.__new__ на Fruit, TextChoices, .... Если вы вызываете super().__new__ в Fruit.__new__, то это вызывает TextChoices.__new__, который на самом деле является Enum.__new__ и который не будет ожидать передаваемых вами аргументов (и даже если он примет ваши аргументы, он никогда не вызовет super().__new__ сам).
Это повысит:
ValueError: 'myvalue' is not a valid Fruit