Establezca Django IntegerField por opciones =… nombre

109

Cuando tiene un campo modelo con una opción de opciones, tiende a tener algunos valores mágicos asociados con nombres legibles por humanos. ¿Existe en Django una forma conveniente de configurar estos campos por el nombre legible por humanos en lugar del valor?

Considere este modelo:

class Thing(models.Model):
  PRIORITIES = (
    (0, 'Low'),
    (1, 'Normal'),
    (2, 'High'),
  )

  priority = models.IntegerField(default=0, choices=PRIORITIES)

En algún momento tenemos una instancia de Thing y queremos establecer su prioridad. Obviamente podrías hacer

thing.priority = 1

Pero eso te obliga a memorizar el mapeo Valor-Nombre de PRIORIDADES. Esto no funciona:

thing.priority = 'Normal' # Throws ValueError on .save()

Actualmente tengo esta tonta solución:

thing.priority = dict((key,value) for (value,key) in Thing.PRIORITIES)['Normal']

pero eso es torpe. Dado lo común que podría ser este escenario, me preguntaba si alguien tenía una solución mejor. ¿Existe algún método de campo para configurar campos por nombre de elección que pasé por alto por completo?

Alexander Ljungberg
fuente

Respuestas:

164

Haz como se ve aquí . Luego puede usar una palabra que represente el número entero adecuado.

Al igual que:

LOW = 0
NORMAL = 1
HIGH = 2
STATUS_CHOICES = (
    (LOW, 'Low'),
    (NORMAL, 'Normal'),
    (HIGH, 'High'),
)

Entonces siguen siendo números enteros en la base de datos.

El uso sería thing.priority = Thing.NORMAL


fuente
2
Esa es una publicación de blog muy detallada sobre el tema. Difícil de encontrar con Google también, así que gracias.
Alexander Ljungberg
1
FWIW, si necesita configurarlo desde una cadena literal (tal vez desde un formulario, entrada de usuario o similar) puede simplemente hacer: thing.priority = getattr (thing, strvalue.upper ()).
mrooney
1
Realmente me gusta la sección de Encapsulación en el blog.
Nathan Keller
Tengo un problema: ¡siempre veo el valor predeterminado en el administrador! ¡He probado que el valor realmente cambia! ¿Qué debería hacer ahora?
Mahdi
Este es el camino a seguir, pero tenga cuidado: si agrega o elimina opciones en el futuro, sus números no serán secuenciales. Posiblemente podría comentar las opciones obsoletas para que no haya confusión en el futuro y no se encontrará con colisiones de db.
Grokpot
7

Probablemente establecería el dictado de búsqueda inversa de una vez por todas, pero si no lo hubiera hecho, simplemente usaría:

thing.priority = next(value for value, name in Thing.PRIORITIES
                      if name=='Normal')

que parece más simple que construir el dict sobre la marcha solo para tirarlo de nuevo ;-).

Alex Martelli
fuente
Sí, lanzar el dict es un poco tonto, ahora que lo dices. :)
Alexander Ljungberg
7

Aquí hay un tipo de campo que escribí hace unos minutos que creo que hace lo que quieres. Su constructor requiere un argumento 'opciones', que puede ser una tupla de 2 tuplas en el mismo formato que la opción de opciones para IntegerField, o una lista simple de nombres (es decir, ChoiceField (('Low', 'Normal', 'Alto'), predeterminado = 'Bajo')). La clase se encarga del mapeo de string a int por usted, nunca ve el int.

  class ChoiceField(models.IntegerField):
    def __init__(self, choices, **kwargs):
        if not hasattr(choices[0],'__iter__'):
            choices = zip(range(len(choices)), choices)

        self.val2choice = dict(choices)
        self.choice2val = dict((v,k) for k,v in choices)

        kwargs['choices'] = choices
        super(models.IntegerField, self).__init__(**kwargs)

    def to_python(self, value):
        return self.val2choice[value]

    def get_db_prep_value(self, choice):
        return self.choice2val[choice]

fuente
1
Eso no está mal, Allan. ¡Gracias!
Alexander Ljungberg
5

Aprecio la forma de definición constante, pero creo que el tipo Enum es mucho mejor para esta tarea. Pueden representar números enteros y una cadena para un elemento al mismo tiempo, mientras mantienen su código más legible.

Las enumeraciones se introdujeron en Python en la versión 3.4. Si usted está usando alguna inferior (como la versión 2.x) que aún puede tener que mediante la instalación del paquete portado : pip install enum34.

# myapp/fields.py
from enum import Enum    


class ChoiceEnum(Enum):

    @classmethod
    def choices(cls):
        choices = list()

        # Loop thru defined enums
        for item in cls:
            choices.append((item.value, item.name))

        # return as tuple
        return tuple(choices)

    def __str__(self):
        return self.name

    def __int__(self):
        return self.value


class Language(ChoiceEnum):
    Python = 1
    Ruby = 2
    Java = 3
    PHP = 4
    Cpp = 5

# Uh oh
Language.Cpp._name_ = 'C++'

Esto es prácticamente todo. Puede heredar el ChoiceEnumpara crear sus propias definiciones y usarlas en una definición de modelo como:

from django.db import models
from myapp.fields import Language

class MyModel(models.Model):
    language = models.IntegerField(choices=Language.choices(), default=int(Language.Python))
    # ...

Las consultas son la guinda del pastel, como puede imaginar:

MyModel.objects.filter(language=int(Language.Ruby))
# or if you don't prefer `__int__` method..
MyModel.objects.filter(language=Language.Ruby.value)

Representarlos en cadena también es fácil:

# Get the enum item
lang = Language(some_instance.language)

print(str(lang))
# or if you don't prefer `__str__` method..
print(lang.name)

# Same as get_FOO_display
lang.name == some_instance.get_language_display()
kirpit
fuente
1
Si prefiere no introducir una clase base como ChoiceEnum, puede usar .valuey .namecomo describe @kirpit y reemplazar el uso de choices()con - ya tuple([(x.value, x.name) for x in cls])sea ​​en una función (DRY) o directamente en el constructor del campo.
Set
4
class Sequence(object):
    def __init__(self, func, *opts):
        keys = func(len(opts))
        self.attrs = dict(zip([t[0] for t in opts], keys))
        self.choices = zip(keys, [t[1] for t in opts])
        self.labels = dict(self.choices)
    def __getattr__(self, a):
        return self.attrs[a]
    def __getitem__(self, k):
        return self.labels[k]
    def __len__(self):
        return len(self.choices)
    def __iter__(self):
        return iter(self.choices)
    def __deepcopy__(self, memo):
        return self

class Enum(Sequence):
    def __init__(self, *opts):
        return super(Enum, self).__init__(range, *opts)

class Flags(Sequence):
    def __init__(self, *opts):
        return super(Flags, self).__init__(lambda l: [1<<i for i in xrange(l)], *opts)

Úselo así:

Priorities = Enum(
    ('LOW', 'Low'),
    ('NORMAL', 'Normal'),
    ('HIGH', 'High')
)

priority = models.IntegerField(default=Priorities.LOW, choices=Priorities)
mpen
fuente
3

A partir de Django 3.0, puede usar:

class ThingPriority(models.IntegerChoices):
    LOW = 0, 'Low'
    NORMAL = 1, 'Normal'
    HIGH = 2, 'High'


class Thing(models.Model):
    priority = models.IntegerField(default=ThingPriority.LOW, choices=ThingPriority.choices)

# then in your code
thing = get_my_thing()
thing.priority = ThingPriority.HIGH
David Viejo
fuente
1

Simplemente reemplace sus números con los valores legibles por humanos que le gustaría. Como tal:

PRIORITIES = (
('LOW', 'Low'),
('NORMAL', 'Normal'),
('HIGH', 'High'),
)

Esto lo hace legible por humanos, sin embargo, tendrá que definir su propio orden.

AlbertoPL
fuente
1

Mi respuesta es muy tardía y puede parecer obvia para los expertos de Django de hoy en día, pero para quien aterrice aquí, recientemente descubrí una solución muy elegante traída por django-model-utils: https://django-model-utils.readthedocs.io/ es / latest / utilities.html # opciones

Este paquete le permite definir Choices con tres tuplas donde:

  • El primer elemento es el valor de la base de datos
  • El segundo elemento es un valor legible por código
  • El tercer elemento es un valor legible por humanos

Así que esto es lo que puede hacer:

from model_utils import Choices

class Thing(models.Model):
    PRIORITIES = Choices(
        (0, 'low', 'Low'),
        (1, 'normal', 'Normal'),
        (2, 'high', 'High'),
      )

    priority = models.IntegerField(default=PRIORITIES.normal, choices=PRIORITIES)

thing.priority = getattr(Thing.PRIORITIES.Normal)

De esta manera:

  • Puede usar su valor legible por humanos para elegir realmente el valor de su campo (en mi caso, es útil porque estoy raspando contenido salvaje y almacenándolo de una manera normalizada)
  • Un valor limpio se almacena en su base de datos
  • No tienes nada que hacer que no sea SECO;)

Disfruta :)

David Guillot
fuente
1
Totalmente válido para abrir django-model-utils. Una pequeña sugerencia para aumentar la legibilidad; use también la notación de puntos en el parámetro predeterminado: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(no hace falta decir que la asignación de prioridad debe tener sangría para estar dentro de la clase Thing para poder hacerlo). También considere usar palabras / caracteres en minúsculas para el identificador de Python, ya que no se está refiriendo a una clase, sino a un parámetro (sus Elecciones se convertirían en: (0, 'low', 'Low'),y así sucesivamente).
Kim
0

Originalmente utilicé una versión modificada de la respuesta de @ Allan:

from enum import IntEnum, EnumMeta

class IntegerChoiceField(models.IntegerField):
    def __init__(self, choices, **kwargs):
        if hasattr(choices, '__iter__') and isinstance(choices, EnumMeta):
            choices = list(zip(range(1, len(choices) + 1), [member.name for member in list(choices)]))

        kwargs['choices'] = choices
        super(models.IntegerField, self).__init__(**kwargs)

    def to_python(self, value):
        return self.choices(value)

    def get_db_prep_value(self, choice):
        return self.choices[choice]

models.IntegerChoiceField = IntegerChoiceField

GEAR = IntEnum('GEAR', 'HEAD BODY FEET HANDS SHIELD NECK UNKNOWN')

class Gear(Item, models.Model):
    # Safe to assume last element is largest value member of an enum?
    #type = models.IntegerChoiceField(GEAR, default=list(GEAR)[-1].name)
    largest_member = GEAR(max([member.value for member in list(GEAR)]))
    type = models.IntegerChoiceField(GEAR, default=largest_member)

    def __init__(self, *args, **kwargs):
        super(Gear, self).__init__(*args, **kwargs)

        for member in GEAR:
            setattr(self, member.name, member.value)

print(Gear().HEAD, (Gear().HEAD == GEAR.HEAD.value))

Simplificado con el django-enumfieldspaquete del paquete que ahora uso:

from enumfields import EnumIntegerField, IntEnum

GEAR = IntEnum('GEAR', 'HEAD BODY FEET HANDS SHIELD NECK UNKNOWN')

class Gear(Item, models.Model):
    # Safe to assume last element is largest value member of an enum?
    type = EnumIntegerField(GEAR, default=list(GEAR)[-1])
    #largest_member = GEAR(max([member.value for member in list(GEAR)]))
    #type = EnumIntegerField(GEAR, default=largest_member)

    def __init__(self, *args, **kwargs):
        super(Gear, self).__init__(*args, **kwargs)

        for member in GEAR:
            setattr(self, member.name, member.value)
atx
fuente
0

La opción de opciones del modelo acepta una secuencia que consiste en iterables de exactamente dos elementos (por ejemplo, [(A, B), (A, B) ...]) para usar como opciones para este campo.

Además, Django proporciona tipos de enumeración que puede subclasificar para definir opciones de una manera concisa:

class ThingPriority(models.IntegerChoices):
    LOW = 0, _('Low')
    NORMAL = 1, _('Normal')
    HIGH = 2, _('High')

class Thing(models.Model):
    priority = models.IntegerField(default=ThingPriority.NORMAL, choices=ThingPriority.choices)

Django admite la adición de un valor de cadena adicional al final de esta tupla para usarlo como nombre o etiqueta legible por humanos. La etiqueta puede ser una cadena traducible perezosa.

   # in your code 
   thing = get_thing() # instance of Thing
   thing.priority = ThingPriority.LOW

Nota: se puede usar que el uso de ThingPriority.HIGH, ThingPriority.['HIGH']o ThingPriority(0)para el acceso o los miembros de enumeración de búsqueda.

mmsilviu
fuente