Définir Django IntegerField par choix =… nom

109

Lorsque vous avez un champ de modèle avec une option de choix, vous avez tendance à avoir des valeurs magiques associées à des noms lisibles par l'homme. Existe-t-il dans Django un moyen pratique de définir ces champs par le nom lisible par l'homme au lieu de la valeur?

Considérez ce modèle:

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

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

À un moment donné, nous avons une instance Thing et nous voulons définir sa priorité. Evidemment tu pourrais faire,

thing.priority = 1

Mais cela vous oblige à mémoriser le mappage Valeur-Nom des PRIORITÉS. Cela ne fonctionne pas:

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

Actuellement, j'ai cette solution de contournement stupide:

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

mais c'est maladroit. Étant donné la fréquence de ce scénario, je me demandais si quelqu'un avait une meilleure solution. Existe-t-il une méthode de champ pour définir les champs par nom de choix que j'ai totalement négligé?

Alexander Ljungberg
la source

Réponses:

164

Faites comme vu ici . Ensuite, vous pouvez utiliser un mot qui représente le bon entier.

Ainsi:

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

Ensuite, ce sont toujours des nombres entiers dans la base de données.

L'utilisation serait thing.priority = Thing.NORMAL


la source
2
C'est un article de blog bien détaillé sur le sujet. Difficile à trouver avec Google aussi, merci.
Alexander Ljungberg
1
FWIW, si vous avez besoin de le définir à partir d'une chaîne littérale (peut-être à partir d'un formulaire, d'une entrée utilisateur ou similaire), vous pouvez alors simplement faire: thing.priority = getattr (thing, strvalue.upper ()).
mrooney
1
Vraiment comme la section Encapsulation sur le blog.
Nathan Keller
J'ai un problème: je vois toujours la valeur par défaut sur l'admin! J'ai testé que la valeur change vraiment! Qu'est-ce que je devrais faire maintenant?
Mahdi
C'est la voie à suivre, mais attention: si vous ajoutez ou supprimez des choix à l'avenir, vos numéros ne seront pas séquentiels. Vous pouvez éventuellement commenter les choix obsolètes afin d'éviter toute confusion future et de ne pas rencontrer de collisions de base de données.
grokpot
7

J'aurais probablement configuré le dict de recherche inversée une fois pour toutes, mais si je ne l'avais pas fait, j'utiliserais simplement:

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

ce qui semble plus simple que de construire le dict à la volée juste pour le jeter à nouveau ;-).

Alex Martelli
la source
Oui, lancer le dict est un peu idiot, maintenant que vous le dites. :)
Alexander Ljungberg
7

Voici un type de champ que j'ai écrit il y a quelques minutes qui, je pense, fait ce que vous voulez. Son constructeur nécessite un argument 'choix', qui peut être soit un tuple de 2-tuples dans le même format que l'option de choix d'IntegerField, soit une simple liste de noms (ie ChoiceField (('Low', 'Normal', 'High'), par défaut = 'Low')). La classe s'occupe du mappage de string à int pour vous, vous ne voyez jamais le 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]

la source
1
Ce n'est pas mal Allan. Merci!
Alexander Ljungberg le
5

J'apprécie la manière constante de définir, mais je pense que le type Enum est de loin le meilleur pour cette tâche. Ils peuvent représenter un entier et une chaîne pour un élément dans le même temps, tout en gardant votre code plus lisible.

Les énumérations ont été introduites dans Python dans la version 3.4. Si vous utilisez un bas (comme 2.x) , vous pouvez toujours en installant le package rétroportés : 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++'

C'est à peu près tout. Vous pouvez hériter de ChoiceEnumpour créer vos propres définitions et les utiliser dans une définition de modèle comme:

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

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

Interroger est la cerise sur le gâteau comme vous pouvez le deviner:

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

Les représenter en chaîne est également simplifié:

# 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
la source
1
Si vous préférez ne pas introduire de classe de base comme ChoiceEnum, vous pouvez utiliser .valueet .namecomme le décrit @kirpit et remplacer l'utilisation de choices()par - tuple([(x.value, x.name) for x in cls])soit dans une fonction (DRY) ou directement dans le constructeur du champ.
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)

Utilisez-le comme ceci:

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

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

Depuis Django 3.0, vous pouvez utiliser:

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
la source
1

Remplacez simplement vos chiffres par les valeurs lisibles par l'homme que vous souhaitez. En tant que tel:

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

Cela le rend lisible par l'homme, cependant, vous devrez définir votre propre ordre.

AlbertoPL
la source
1

Ma réponse est très tardive et peut sembler évidente aux experts de Django d'aujourd'hui, mais pour quiconque atterrit ici, j'ai récemment découvert une solution très élégante apportée par django-model-utils: https://django-model-utils.readthedocs.io/ fr / latest / utilities.html # choix

Ce package vous permet de définir des choix avec trois tuples où:

  • Le premier élément est la valeur de la base de données
  • Le deuxième élément est une valeur lisible par code
  • Le troisième élément est une valeur lisible par l'homme

Alors, voici ce que vous pouvez faire:

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)

Par ici:

  • Vous pouvez utiliser votre valeur lisible par l'homme pour choisir réellement la valeur de votre champ (dans mon cas, c'est utile car je racle du contenu sauvage et le stocke de manière normalisée)
  • Une valeur propre est stockée dans votre base de données
  • Vous n'avez rien de non-DRY à faire;)

Prendre plaisir :)

David Guillot
la source
1
Totalement valide pour afficher django-model-utils. Une petite suggestion pour augmenter la lisibilité; utilisez également la notation par points sur le paramètre par défaut: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(il va sans dire que l'affectation de priorité doit être indentée pour être à l'intérieur de la classe Thing pour ce faire). Pensez également à utiliser des mots / caractères minuscules pour l'identifiant python, car vous ne faites pas référence à une classe, mais à un paramètre à la place (vos choix deviendraient alors: (0, 'low', 'Low'),et ainsi de suite).
Kim du
0

À l'origine, j'ai utilisé une version modifiée de la réponse 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))

Simplifié avec le django-enumfieldspackage package que j'utilise maintenant:

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
la source
0

L'option de choix du modèle accepte une séquence constituée elle-même d'itérables d'exactement deux éléments (par exemple [(A, B), (A, B) ...]) à utiliser comme choix pour ce champ.

De plus, Django fournit des types d'énumération que vous pouvez sous-classer pour définir des choix de manière concise:

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 prend en charge l'ajout d'une valeur de chaîne supplémentaire à la fin de ce tuple à utiliser comme nom ou étiquette lisible par l'homme. L'étiquette peut être une chaîne traduisible paresseusement.

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

Remarque: vous pouvez l' utiliser à l' aide ThingPriority.HIGH, ThingPriority.['HIGH']ou ThingPriority(0)d'accéder ou de consultation des membres de ENUM.

mmsilviu
la source