Встановіть Django IntegerField за вибором =… ім'я


109

Коли у вас є модельне поле з варіантом вибору, ви, як правило, мають деякі магічні значення, пов'язані з читаними людьми іменами. Чи є в Джанго зручний спосіб встановити ці поля за допомогою людського читаного імені замість значення?

Розглянемо цю модель:

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", який може бути або вкладеним з двох кортежів у тому ж форматі, що і параметр "вибір" для IntegerField, або замість цього простого списку імен (тобто ChoiceField (('Low', 'Normal', 'Висока'), за замовчуванням = 'низька')). Клас піклується про відображення для вас від рядка до 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()

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)

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-model-utils: https://django-model-utils.readthedocs.io/ en / latest / utilities.html # вибір

Цей пакет дозволяє визначити вибір з трьома корками, де:

  • Перший пункт - це значення бази даних
  • Другий елемент - значення, що читається кодом
  • Третій предмет - читабельна для людини цінність

Тож ось що можна зробити:

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
Цілком справедливий, щоб виховувати утиліти джанго-моделі. Одна невелика пропозиція щодо підвищення читабельності; також використовуйте крапки-нотації за параметром за замовчуванням: 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)

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)для доступу або підстановок членів перерахувань.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.