web-dev-qa-db-fra.com

Définir Django IntegerField par choix = ... nom

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é. De toute évidence, vous pourriez le 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 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 définition des champs par nom de choix que j'ai totalement ignorée?

93
Alexander Ljungberg

Faites comme vu ici . Ensuite, vous pouvez utiliser un mot qui représente l'entier approprié.

Ainsi:

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

Ils sont alors toujours des entiers dans la base de données.

L'utilisation serait thing.priority = Thing.NORMAL

151
user136565

J'aurais probablement configuré la dictée 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 ;-).

7
Alex Martelli

Voici un type de champ que j'ai écrit il y a quelques minutes et qui, je pense, fait ce que vous voulez. Son constructeur requiert un argument 'choix', qui peut être soit un Tuple de 2-tuples dans le même format que l'option choix d'IntegerField, ou plutôt une simple liste de noms (ie ChoiceField (('Low', 'Normal', 'High'), par défaut = 'Low')). La classe s'occupe du mappage de la chaîne en entier pour vous, vous ne voyez jamais 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]
6
Allan
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)
3
mpen

J'apprécie la façon constante de définir, mais je crois 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 en 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 une version inférieure (comme v2.x), vous pouvez toujours l'avoir en installant package rétroporté : 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 du ChoiceEnum pour 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))
    # ...

L'interrogation 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 facilité:

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

Ma réponse est très tardive et peut sembler évidente aux experts de Django de nos jours, 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/en/latest/utilities.html#choices

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

Voici donc 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 parce que j'élimine le 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-SEC à faire;)

Prendre plaisir :)

1
David Guillot

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 commande.

1
AlbertoPL

À 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-enumfields package 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)
0
atx