Defina Django IntegerField por escolhas = ... nome

109

Quando você tem um campo de modelo com uma opção de escolha, você tende a ter alguns valores mágicos associados a nomes legíveis por humanos. Existe no Django uma maneira conveniente de definir esses campos pelo nome legível por humanos em vez do valor?

Considere este modelo:

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

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

Em algum ponto, temos uma instância de Thing e queremos definir sua prioridade. Obviamente, você poderia fazer,

thing.priority = 1

Mas isso força você a memorizar o mapeamento Nome-Valor de PRIORIDADES. Isso não funciona:

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

Atualmente, tenho esta solução boba:

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

mas isso é desajeitado. Dado o quão comum esse cenário pode ser, eu queria saber se alguém teria uma solução melhor. Existe algum método de campo para definir campos por nome de escolha que eu ignorei totalmente?

Alexander Ljungberg
fonte

Respostas:

164

Faça como visto aqui . Então você pode usar uma palavra que represente o número inteiro adequado.

Igual a:

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

Então eles ainda são inteiros no banco de dados.

O uso seria thing.priority = Thing.NORMAL


fonte
2
Essa é uma postagem de blog bem detalhada sobre o assunto. Difícil de encontrar no Google também, então obrigado.
Alexander Ljungberg
1
FWIW, se você precisar defini-lo a partir de uma string literal (talvez de um formulário, entrada do usuário ou similar), você pode simplesmente fazer: thing.priority = getattr (thing, strvalue.upper ()).
mrooney
1
Gosto muito da seção Encapsulamento do blog.
Nathan Keller
Estou com um problema: sempre vejo o valor padrão no admin! Eu testei se o valor realmente muda! O que eu deveria fazer agora?
Mahdi
Este é o caminho a seguir, mas cuidado: se você adicionar ou remover opções no futuro, seus números não serão sequenciais. Você pode comentar as opções obsoletas para que não haja confusão futura e você não tenha colisões de banco de dados.
grokpot
7

Eu provavelmente configuraria o dict de pesquisa reversa de uma vez por todas, mas se não tivesse, eu apenas usaria:

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

o que parece mais simples do que construir o dicionário na hora apenas para jogá-lo fora novamente ;-).

Alex Martelli
fonte
Sim, jogar o dict é um pouco bobo, agora que você o disse. :)
Alexander Ljungberg
7

Este é um tipo de campo que escrevi há alguns minutos que acho que faz o que você deseja. Seu construtor requer um argumento 'escolhas', que pode ser uma tupla de 2 tuplas no mesmo formato que a opção de escolhas para IntegerField, ou em vez de uma lista simples de nomes (ou seja, ChoiceField (('Low', 'Normal', 'Alto'), padrão = 'Baixo')). A classe cuida do mapeamento de string para int para você, você nunca vê o 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]

fonte
1
Isso não é ruim, Allan. Obrigado!
Alexander Ljungberg
5

Eu aprecio a forma constante de definição, mas acredito que o tipo Enum é muito melhor para essa tarefa. Eles podem representar um inteiro e uma string para um item ao mesmo tempo, mantendo seu código mais legível.

Enums foram introduzidos no Python na versão 3.4. Se você estiver usando qualquer (como v2.x) menor você ainda pode tê-lo através da instalação do pacote backported : 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++'

Isso é quase tudo. Você pode herdar o ChoiceEnumpara criar suas próprias definições e usá-las em uma definição 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))
    # ...

Consultar é a cereja do bolo, como você pode imaginar:

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

Representá-los em string também é 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
fonte
1
Se você preferir não introduzir uma classe base como ChoiceEnum, você pode usar o .valuee .namecomo @kirpit descreve e substituir o uso de choices()por tuple([(x.value, x.name) for x in cls])- em uma função (DRY) ou diretamente no construtor do 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)

Use-o assim:

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

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

A partir do Django 3.0, você pode 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
fonte
1

Simplesmente substitua seus números pelos valores legíveis humanos que você deseja. Assim sendo:

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

Isso o torna legível por humanos, no entanto, você terá que definir seu próprio pedido.

AlbertoPL
fonte
1

Minha resposta está muito atrasada e pode parecer óbvia para os especialistas em Django de hoje, mas para quem quer que esteja aqui, recentemente descobri uma solução muito elegante trazida por django-model-utils: https://django-model-utils.readthedocs.io/ en / mais recente / utilities.html # escolhas

Este pacote permite que você defina escolhas com três tuplas, onde:

  • O primeiro item é o valor do banco de dados
  • O segundo item é um valor legível por código
  • O terceiro item é um valor legível por humanos

Então aqui está o que você pode fazer:

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)

Deste jeito:

  • Você pode usar seu valor legível para realmente escolher o valor do seu campo (no meu caso, é útil porque estou copiando conteúdo selvagem e armazenando-o de uma forma normalizada)
  • Um valor limpo é armazenado em seu banco de dados
  • Você não tem nada que não seja SECO para fazer;)

Aproveitar :)

David Guillot
fonte
1
Totalmente válido para trazer django-model-utils. Uma pequena sugestão para aumentar a legibilidade; use a notação de ponto no parâmetro padrão também: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(desnecessário dizer, a atribuição de prioridade deve ser indentada para estar dentro da classe Thing para fazer isso). Considere também o uso de palavras / caracteres minúsculos para o identificador python, já que você não está se referindo a uma classe, mas a um parâmetro (suas escolhas seriam então: (0, 'low', 'Low'),e assim por diante).
Kim
0

Originalmente, usei uma versão modificada da resposta 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 com o django-enumfieldspacote de pacote que agora 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
fonte
0

A opção de escolhas do modelo aceita uma sequência consistindo de iteráveis ​​de exatamente dois itens (por exemplo, [(A, B), (A, B) ...]) para usar como escolhas para este campo.

Além disso, o Django fornece tipos de enumeração que você pode criar em subclasses para definir as opções de maneira 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 suporta a adição de um valor de string extra no final desta tupla para ser usado como o nome legível por humanos, ou rótulo. O rótulo pode ser uma string traduzível preguiçosa.

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

Nota: você pode usar que o uso ThingPriority.HIGH, ThingPriority.['HIGH']ou ThingPriority(0)para o acesso ou membros de enumeração de pesquisa.

mmsilviu
fonte