Как добавить дополнительные данные в 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