Установите Django IntegerField с помощью choices =… name

109

Когда у вас есть поле модели с опцией выбора, у вас, как правило, есть некоторые магические значения, связанные с понятными человеку именами. Есть ли в Django удобный способ установить эти поля по удобочитаемому имени вместо значения?

Рассмотрим эту модель:

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

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

В какой-то момент у нас есть экземпляр Thing, и мы хотим установить его приоритет. Очевидно, вы могли бы сделать,

thing.priority = 1

Но это заставляет вас запоминать сопоставление ПРИОРИТЕТОВ имени значения. Это не работает:

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

В настоящее время у меня есть этот глупый обходной путь:

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

но это неуклюже. Учитывая, насколько распространен этот сценарий, мне было интересно, есть ли у кого-нибудь лучшее решение. Есть ли какой-нибудь полевой метод для установки полей по имени выбора, который я полностью упустил?

Александр Юнгберг
источник

Ответы:

164

Сделайте как здесь показано . Затем вы можете использовать слово, представляющее правильное целое число.

Вот так:

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

Тогда они все еще целые числа в БД.

Использование будет thing.priority = Thing.NORMAL


источник
2
Это красиво подробное сообщение в блоге по этой теме. Трудно найти в Google, спасибо.
Александр Юнгберг,
1
FWIW, если вам нужно установить его из буквальной строки (возможно, из формы, пользовательского ввода или аналогичного), вы можете просто сделать: thing.priority = getattr (thing, strvalue.upper ()).
mrooney
1
Очень нравится раздел "Инкапсуляция" в блоге.
Натан Келлер
У меня проблема: я всегда вижу в админке значение по умолчанию! Я проверил, что значение действительно меняется! Что мне теперь делать?
Махди
Это правильный путь, но будьте осторожны: если вы добавите или удалите варианты выбора в будущем, ваши числа не будут последовательными. Вы могли бы прокомментировать устаревшие варианты, чтобы не было путаницы в будущем и не столкнуться с конфликтами db.
Grokpot
7

Я бы, наверное, установил обратный поиск раз и навсегда, но если бы я этого не делал, я бы просто использовал:

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

что кажется проще, чем создать диктант на лету, чтобы просто выбросить его снова ;-).

Алекс Мартелли
источник
Да, теперь, когда вы говорите, это немного глупо. :)
Александр Юнгберг
7

Вот тип поля, который я написал несколько минут назад, и я думаю, он делает то, что вы хотите. Для его конструктора требуется аргумент choices, который может быть либо кортежем из двух кортежей в том же формате, что и параметр choices для IntegerField, либо вместо этого простым списком имен (например, ChoiceField (('Low', 'Normal', 'High'), по умолчанию = 'Low')). Класс заботится о преобразовании строки в int за вас, вы никогда не увидите 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
Это неплохо, Аллан. Спасибо!
Александр Юнгберг,
5

Я ценю постоянный способ определения, но считаю, что тип Enum лучше всего подходит для этой задачи. Они могут одновременно представлять целое число и строку для элемента, сохраняя при этом ваш код более читабельным.

Перечисления были введены в Python в версии 3.4. Если вы используете какой - либо ниже (например, v2.x) , вы все равно можете иметь его, установив портированном пакет : 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++'

Это почти все. Вы можете наследовать, ChoiceEnumчтобы создавать свои собственные определения и использовать их в определении модели, например:

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

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

Запросы - это вишенка на торте, как вы могли догадаться:

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

Их также легко представить в виде строки:

# 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
источник
1
Если вы предпочитаете не вводить базовый класс , как ChoiceEnumвы можете использовать .valueи в .nameкачестве @kirpit описывает и заменить использование choices()с tuple([(x.value, x.name) for x in cls])--either в функции (DRY) или непосредственно в конструкторе поля.
Сет,
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)

Используйте это так:

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

priority = models.IntegerField(default=Priorities.LOW, choices=Priorities)
mpen
источник
3

Начиная с Django 3.0, вы можете использовать:

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

Просто замените свои числа желаемыми для человека значениями. В качестве таких:

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

Это делает его удобочитаемым, однако вам придется определять свой собственный порядок.

АльбертоПЛ
источник
1

Мой ответ очень запоздалый и может показаться очевидным нынешним экспертам по Django, но тем, кто сюда попадает, я недавно обнаружил очень элегантное решение, предложенное django-model-utils: https://django-model-utils.readthedocs.io/ ru / latest / utilities.html # choices

Этот пакет позволяет вам определять варианты выбора с помощью трех кортежей, где:

  • Первый элемент - это значение базы данных
  • Второй элемент - значение, читаемое в коде.
  • Третий элемент - это удобочитаемое значение.

Итак, вот что вы можете сделать:

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)

Сюда:

  • Вы можете использовать свое удобочитаемое значение, чтобы фактически выбрать значение вашего поля (в моем случае это полезно, потому что я очищаю дикий контент и сохраняю его в нормализованном виде)
  • Чистое значение хранится в вашей базе данных
  • Вам нечего делать кроме СУХОГО;)

Наслаждаться :)

Дэвид Гийо
источник
1
Абсолютно актуально для вывода django-model-utils. Одно небольшое предложение по повышению читабельности; также используйте точечную нотацию в параметре по умолчанию: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(разумеется, для этого необходимо, чтобы присвоение приоритета находилось внутри класса Thing). Также рассмотрите возможность использования строчных слов / символов для идентификатора python, поскольку вы ссылаетесь не на класс, а на параметр (тогда ваш выбор будет выглядеть следующим образом: (0, 'low', 'Low'),и т. Д.).
Ким
0

Первоначально я использовал модифицированную версию ответа @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))

Упрощенный django-enumfieldsпакет, который я сейчас использую:

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
источник
0

Опция выбора модели принимает последовательность, состоящую из итераций ровно двух элементов (например, [(A, B), (A, B) ...]) для использования в качестве вариантов выбора для этого поля.

Кроме того, Django предоставляет типы перечисления , которые можно подклассифицировать для краткого определения вариантов:

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 поддерживает добавление дополнительного строкового значения в конец этого кортежа для использования в качестве удобочитаемого имени или метки. Метка может быть ленивой переводимой строкой.

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

Примечание: Вы можете использовать это , используя ThingPriority.HIGH, ThingPriority.['HIGH']или ThingPriority(0)для доступа или подстановок членов перечислений.

mmsilviu
источник