Impostare Django IntegerField con le scelte = ... nome


109

Quando hai un campo modello con un'opzione di scelta, tendi ad avere alcuni valori magici associati a nomi leggibili dagli umani. Esiste in Django un modo conveniente per impostare questi campi in base al nome leggibile dall'uomo invece che al valore?

Considera questo modello:

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

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

Ad un certo punto abbiamo un'istanza Thing e vogliamo impostare la sua priorità. Ovviamente potresti fare,

thing.priority = 1

Ma questo ti costringe a memorizzare la mappatura valore-nome delle PRIORITÀ. Questo non funziona:

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

Attualmente ho questa stupida soluzione alternativa:

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

ma questo è goffo. Dato quanto comune potesse essere questo scenario, mi chiedevo se qualcuno avesse una soluzione migliore. Esiste un metodo di campo per impostare i campi in base al nome della scelta che ho completamente trascurato?

Risposte:


164

Fai come visto qui . Quindi puoi usare una parola che rappresenta il numero intero corretto.

Così:

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

Quindi sono ancora numeri interi nel DB.

L'utilizzo sarebbe thing.priority = Thing.NORMAL


2
Questo è un post sul blog ben dettagliato sull'argomento. Difficile da trovare anche con Google, quindi grazie.
Alexander Ljungberg

1
FWIW, se hai bisogno di impostarlo da una stringa letterale (forse da un form, input utente o simili) puoi semplicemente fare: thing.priority = getattr (thing, strvalue.upper ()).
mrooney

1
Mi piace molto la sezione Incapsulamento del blog.
Nathan Keller

Ho un problema: vedo sempre il valore predefinito sull'admin! Ho verificato che il valore cambia davvero! Cosa dovrei fare ora?
Mahdi

Questa è la strada da percorrere, ma attenzione: se aggiungi o rimuovi scelte in futuro, i tuoi numeri non saranno sequenziali. Potresti eventualmente commentare le scelte deprecate in modo che non ci sarà confusione futura e non incorrerai in collisioni db.
grokpot

7

Probabilmente avrei impostato il dict di ricerca inversa una volta per tutte, ma se non l'avessi fatto userei semplicemente:

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

che sembra più semplice che costruire il dict al volo solo per buttarlo via di nuovo ;-).


Sì, lanciare il dict è un po 'sciocco, ora che lo dici. :)
Alexander Ljungberg

7

Ecco un tipo di campo che ho scritto pochi minuti fa che penso faccia quello che vuoi. Il suo costruttore richiede un argomento "scelte", che può essere una tupla di 2 tuple nello stesso formato dell'opzione di scelte di IntegerField, o invece un semplice elenco di nomi (ad esempio ChoiceField (("Basso", "Normale", 'Alto'), impostazione predefinita = 'Basso')). La classe si occupa della mappatura da stringa a int per te, non vedi mai l'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]

1
Non è male, Allan. Grazie!
Alexander Ljungberg,

5

Apprezzo il modo costante di definizione, ma credo che il tipo Enum sia di gran lunga il migliore per questo compito. Possono rappresentare un numero intero e una stringa per un elemento allo stesso tempo, mantenendo il codice più leggibile.

Gli enum sono stati introdotti in Python nella versione 3.4. Se si utilizza più in basso (come v2.x) si può ancora avere installando il pacchetto di backport : 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++'

Questo è praticamente tutto. È possibile ereditare il ChoiceEnumper creare le proprie definizioni e utilizzarle in una definizione di modello come:

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

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

Interrogare è la ciliegina sulla torta come puoi immaginare:

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

Anche rappresentarli in stringa è facile:

# 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()

1
Se preferisci non introdurre una classe base come ChoiceEnum, puoi usare .valuee .namecome descrive @kirpit e sostituire l'uso di choices()con - tuple([(x.value, x.name) for x in cls])o in una funzione (DRY) o direttamente nel costruttore del campo.
Seth

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)

Usalo in questo modo:

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

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

3

A partire da Django 3.0, puoi usare:

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

1

Sostituisci semplicemente i tuoi numeri con i valori leggibili dall'uomo che desideri. Come tale:

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

Questo lo rende leggibile dall'uomo, tuttavia, dovresti definire il tuo ordinamento.


1

La mia risposta è molto tardi e potrebbe sembrare ovvia agli esperti di Django di oggi, ma a chiunque atterra qui, ho recentemente scoperto una soluzione molto elegante portata da django-model-utils: https://django-model-utils.readthedocs.io/ it / Novità / scelte utilities.html #

Questo pacchetto ti consente di definire le scelte con tre tuple dove:

  • Il primo elemento è il valore del database
  • Il secondo elemento è un valore leggibile dal codice
  • Il terzo elemento è un valore leggibile dall'uomo

Quindi ecco cosa puoi fare:

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)

Per di qua:

  • Puoi utilizzare il tuo valore leggibile dall'uomo per scegliere effettivamente il valore del tuo campo (nel mio caso, è utile perché sto raschiando il contenuto selvaggio e lo memorizzo in modo normalizzato)
  • Un valore pulito viene memorizzato nel database
  • Non hai nulla da fare non ASCIUTTO;)

Godere :)


1
Totalmente valido per far apparire django-model-utils. Un piccolo suggerimento per aumentare la leggibilità; usa la notazione punto anche sul parametro predefinito: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(inutile dire che l'assegnazione della priorità deve essere indentata per essere all'interno della classe Thing per farlo). Considera anche l'utilizzo di parole / caratteri minuscoli per l'identificatore Python, poiché non ti riferisci a una classe, ma invece a un parametro (le tue scelte diventerebbero: (0, 'low', 'Low'),e così via).
Kim

0

Inizialmente ho usato una versione modificata della risposta di @ 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))

Semplificato con il django-enumfieldspacchetto pacchetto che ora utilizzo:

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)

0

L'opzione delle scelte del modello accetta una sequenza costituita da iterabili di esattamente due elementi (ad esempio [(A, B), (A, B) ...]) da utilizzare come scelte per questo campo.

Inoltre, Django fornisce tipi di enumerazione che puoi sottoclassare per definire le scelte in modo conciso:

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 supporta l'aggiunta di un valore stringa aggiuntivo alla fine di questa tupla da utilizzare come nome leggibile o etichetta. L'etichetta può essere una stringa traducibile in modo pigro.

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

Nota: è possibile utilizzare tale uso ThingPriority.HIGH, ThingPriority.['HIGH']o ThingPriority(0)per l'accesso o membri enum di ricerca.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.