¿Cómo se pueden usar enumeraciones como campo de elección en un modelo de Django?

8 minutos de lectura

Avatar de usuario de Paras
Paras

Tengo una clase modelo en la que quiero que dos campos sean campos de elección, por lo que para completar esas opciones estoy usando una enumeración como se indica a continuación:

#models.py
class Transaction(models.Model):
    transaction_status = models.CharField(max_length=255, choices=TransactionStatus.choices())
    transaction_type = models.CharField(max_length=255, choices=TransactionType.choices())

#enums.py
class TransactionType(Enum):

    IN = "IN",
    OUT = "OUT"

    @classmethod
    def choices(cls):
        print(tuple((i.name, i.value) for i in cls))
        return tuple((i.name, i.value) for i in cls)

class TransactionStatus(Enum):

    INITIATED = "INITIATED",
    PENDING = "PENDING",
    COMPLETED = "COMPLETED",
    FAILED = "FAILED"
    ERROR = "ERROR"

    @classmethod
    def choices(cls):
        print(tuple((i.name, i.value) for i in cls))
        return tuple((i.name, i.value) for i in cls)

Sin embargo, cuando intento acceder a este modelo a través de Django Admin, aparece el siguiente error:

Django Version: 1.11
Exception Type: ValueError
Exception Value:    
too many values to unpack (expected 2)

Seguí dos artículos que describían cómo usar enumeraciones:

  • Tienes una coma después de “IN” y después de “INICIADO”…

    – dirkgroten

    21 de febrero de 2019 a las 8:42


  • Cuatro líneas de su código tienen comas no deseadas al final.

    – khelwood

    21 de febrero de 2019 a las 8:45

Avatar de usuario de Cesar Canassa
César Canassa

Django 3.0 tiene soporte incorporado para Enums

Ejemplo:

from django.utils.translation import gettext_lazy as _

class Student(models.Model):

    class YearInSchool(models.TextChoices):
        FRESHMAN = 'FR', _('Freshman')
        SOPHOMORE = 'SO', _('Sophomore')
        JUNIOR = 'JR', _('Junior')
        SENIOR = 'SR', _('Senior')
        GRADUATE = 'GR', _('Graduate')

    year_in_school = models.CharField(
        max_length=2,
        choices=YearInSchool.choices,
        default=YearInSchool.FRESHMAN,
    )

Estos funcionan de manera similar a enumeración de la biblioteca estándar de Python, pero con algunas modificaciones:

  • Los valores de los miembros de enumeración son una tupla de argumentos para usar al construir el tipo de datos concreto. Django admite agregar un valor de cadena adicional al final de esta tupla para usar como nombre legible por humanos, o label. El label puede ser una cadena traducible perezosa. Por lo tanto, en la mayoría de los casos, el valor del miembro será un (value, label) dos tuplas. Si no se proporciona una tupla, o el último elemento no es una cadena (perezosa), la etiqueta es generada automáticamente del nombre del miembro.
  • A .label La propiedad se agrega a los valores, para devolver el nombre legible por humanos. Se agregan varias propiedades personalizadas a las clases de enumeración: .choices, .labels, .valuesy .names – para facilitar el acceso a las listas de esas partes separadas de la enumeración. Usar .choices como un valor adecuado para pasar a opciones en una definición de campo.
  • El uso de enum.unique() se aplica para garantizar que los valores no se puedan definir varias veces. Es poco probable que esto se espere en las elecciones de un campo.

Para más información, revisa la documentación

Nota:

Como señaló @Danielle Madeley, si intenta acceder a la year_in_school atributo directamente Django todavía devuelve la cadena sin procesar en lugar del objeto Enum:

>>> student.year_in_school
'FR'

Lo que suelo hacer es crear un método auxiliar que devuelva el objeto Enum:

class Student(models.Model):
    ...

    def get_year_in_school(self) -> YearInSchool:
        # Get value from choices enum
        return self.YearInSchool[self.year_in_school]

  • Solo una advertencia aquí, student.year_in_school devuelve una cadena, no una enumeración. Tienes que devolver manualmente el resultado o usar ==/!=

    – Danielle Madeley

    24 de diciembre de 2020 a las 2:47

  • @DavidPiao Me temo que no entiendo tu pregunta. Pero aquí hay una respuesta a la pregunta “a”: Django, lamentablemente, nunca devolverá un tipo de enumeración y siempre devolverá una cadena. Entonces foo.state == State.DRAFT funcionará en la comparación de cadenas, pero los documentos de Python prefieren foo.state is State.DRAFT no lo haré Tu puedes hacer State(foo.state) is State.DRAFT.

    – Danielle Madeley

    10 oct 2021 a las 21:45

  • @CesarCanassa Si se copia algo de la documentación textualmente, cítelo.

    –Abdul Aziz Barkat

    13 de noviembre de 2021 a las 7:34

  • return self.YearInSchool[self.year_in_school] debe tener corchetes en lugar de corchetes: return self.YearInSchool(self.year_in_school)de lo contrario obtendrá un KeyError.

    – código de salida

    11 de marzo de 2022 a las 11:54

  • Gracias por esto. Nota: creo que al método auxiliar le falta ‘self’ en los parámetros del método. Debería decir: def get_year_in_school(self) -> YearInSchool:

    – Levitybot

    30 de julio de 2022 a las 8:43

avatar de usuario de dirkgroten
dirkgroten

Para Django 2.x y versiones anteriores:

Tú defines un Enum configurando las diversas opciones como se documenta aquí:

class TransactionStatus(Enum):

    INITIATED = "INITIATED"
    PENDING = "PENDING"
    COMPLETED = "COMPLETED"
    FAILED = "FAILED"
    ERROR = "ERROR"

Tenga en cuenta que no hay comas! Esto le permite más adelante en su código referirse a TransactionStatus.ERROR o TransactionStatus.PENDING.

El resto de tu código es correcto. obtienes el choices creando tuplas de option.name, option.value.

ACTUALIZACIÓN: para Django 3.x y superioruse los tipos incorporados TextChoices, IntegerChoices y Choices como se describe aquí. De esa manera usted no tiene que construir el choices tupla tú mismo.

avatar de usuario de anjaneyulubatta505
anjaneyulubatta505

El problema en tu código es que INITIATED = "INITIATED", una coma despues INITIATED opción y otras opciones. cuando agregamos una coma después de cualquier cadena, se convertirá en una tupla. Vea un ejemplo a continuación

s="my str"
print(type(s))
# output: str

s="my str",
print(type(s))
# output: tuple

#modelos.py

class Transaction(models.Model):
    trasaction_status = models.CharField(max_length=255, choices=TransactionStatus.choices())
    transaction_type = models.CharField(max_length=255, choices=TransactionType.choices())

#enums.py

class TransactionType(Enum):

    IN = "IN"
    OUT = "OUT"

    @classmethod
    def choices(cls):
        print(tuple((i.name, i.value) for i in cls))
        return tuple((i.name, i.value) for i in cls)

class TransactionStatus(Enum):

    INITIATED = "INITIATED"
    PENDING = "PENDING"
    COMPLETED = "COMPLETED"
    FAILED = "FAILED"
    ERROR = "ERROR"

    @classmethod
    def choices(cls):
        print(tuple((i.name, i.value) for i in cls))
        return tuple((i.name, i.value) for i in cls)

Para Django > 3.0 https://docs.djangoproject.com/en/4.0/ref/models/fields/#field-choices-enum-types

  • No es así como se define un Enum. Solo elimina las comas.

    – dirkgroten

    21 de febrero de 2019 a las 11:24

Avatar de usuario de TheRightChoyce
laeleccióncorrecta

Si está recibiendo este error:

‘opciones’ debe ser un iterable que contiene (valor real, nombre legible por humanos) tuplas

Y está usando Django3, entonces probablemente se encuentre con el mismo problema que tuve: los “Enums” deben estar incrustados en el modelo donde está tratando de usarlos y no pueden declararse fuera del modelo. Por ejemplo, esto será no trabajar:

class YearInSchool(models.TextChoices):
    FRESHMAN = 'FR', _('Freshman')
    SOPHOMORE = 'SO', _('Sophomore')
    JUNIOR = 'JR', _('Junior')
    SENIOR = 'SR', _('Senior')
    GRADUATE = 'GR', _('Graduate')

class Student(models.Model):
   year_in_school = models.CharField(
        max_length=2,
        choices=YearInSchool.choices,
        default=YearInSchool.FRESHMAN,
    )

Donde como este ejemplo de los documentos:

class Student(models.Model):

    class YearInSchool(models.TextChoices):
        FRESHMAN = 'FR', _('Freshman')
        SOPHOMORE = 'SO', _('Sophomore')
        JUNIOR = 'JR', _('Junior')
        SENIOR = 'SR', _('Senior')
        GRADUATE = 'GR', _('Graduate')

    year_in_school = models.CharField(
        max_length=2,
        choices=YearInSchool.choices,
        default=YearInSchool.FRESHMAN,
    )

avatar de usuario de rail.khayrullin
carril.khayrullin

Un ejemplo de mi proyecto:

import enum

from django.contrib.postgres.fields import ArrayField
from django.db import models
from django.utils.translation import gettext_lazy as _


class NotificationTemplate(models.Model):
class Meta:
    verbose_name = _('notification template')
    verbose_name_plural = _('notification templates')

@enum.unique
class Name(str, enum.Enum):
    ONBOARDING = 'onboarding'
    TG_ERROR = 'tg_error'
    FB_ERROR = 'fb_error'

    @classmethod
    def choices(cls):
        return [(item.value, item.name) for item in cls]

@enum.unique
class Type(int, enum.Enum):
    PUSH = 1
    EMAIL = 2
    TELEGRAM = 3
    VK = 4
    OTHER = 5

    @classmethod
    def choices(cls):
        return [(item.value, item.name) for item in cls]

name = models.CharField(_('notification name'), max_length=64, unique=True, choices=Name.choices(), default=Name.ONBOARDING)
template_type = ArrayField(models.PositiveSmallIntegerField(_('type'), choices=Type.choices()))
max_count = models.PositiveSmallIntegerField(default=1)

def __str__(self):
    return self.Name(self.name).name

Avatar de usuario de Hardik Gajjar
Hardik Gajjar

class YearInSchool(models.TextChoices):
        FRESHMAN = 'FR', _('Freshman')
        SOPHOMORE = 'SO', _('Sophomore')
        JUNIOR = 'JR', _('Junior')
        SENIOR = 'SR', _('Senior')
        GRADUATE = 'GR', _('Graduate')

    year_in_school = models.CharField(
        max_length=2,
        choices=YearInSchool.choices,
        default=YearInSchool.FRESHMAN,
    )

Para Django 3.0 anterior, puede usar el ejemplo anterior.

Para opciones enteras, puede usar el siguiente código.

class Suit(models.IntegerChoices):
        DIAMOND = 1
        SPADE = 2
        HEART = 3
        CLUB = 4

    suit = models.IntegerField(choices=Suit.choices)

Por cierto, Djanog también admite el auto() de Python 3 como el valor Enum. Puede usar la siguiente clase de ayuda para hacer su vida más fácil.

from django.db.models.enums import TextChoices

class AutoEnumChoices(TextChoices):
    def _generate_next_value_(name, start, count, last_values):  # @NoSelf
        return name.lower()
    
    @property
    def choices(cls):  # @NoSelf
        empty = [(None, cls.__empty__)] if hasattr(cls, '__empty__') else []
        return empty + [(member.value, member.label) for member in cls]

Luego utilícelo en su definición de opciones:

class TransferBasicStatus(AutoEnumChoices):
    NONE = auto()
    WAITING = auto()
    PENDING = auto()
    PROGRESS = auto()
    SUCCESS = auto()
    DECLINED = auto()
    ENDED =  'ended', _('Ended - The transfer has ended with mixed states')

¿Ha sido útil esta solución?