Ustaw Django IntegerField przez opcje =… nazwa

109

Kiedy masz pole modelu z opcją wyborów, zwykle masz pewne magiczne wartości związane z nazwami czytelnymi dla człowieka. Czy w Django istnieje wygodny sposób ustawiania tych pól na podstawie czytelnej dla człowieka nazwy zamiast wartości?

Rozważ ten model:

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

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

W pewnym momencie mamy instancję Thing i chcemy ustawić jej priorytet. Oczywiście możesz to zrobić

thing.priority = 1

Ale to zmusza cię do zapamiętania mapowania nazwy wartości dla PRIORYTETÓW. To nie działa:

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

Obecnie mam to głupie obejście:

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

ale to jest niezgrabne. Biorąc pod uwagę, jak powszechny może być ten scenariusz, zastanawiałem się, czy ktoś ma lepsze rozwiązanie. Czy jest jakaś metoda ustawiania pól według nazwy wyboru, którą całkowicie przeoczyłem?

Alexander Ljungberg
źródło

Odpowiedzi:

164

Zrób tak, jak tutaj . Następnie możesz użyć słowa, które reprezentuje właściwą liczbę całkowitą.

Tak jak to:

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

Wtedy nadal są liczbami całkowitymi w DB.

Wykorzystanie byłoby thing.priority = Thing.NORMAL


źródło
2
To ładnie szczegółowy wpis na blogu na ten temat. Trudno też znaleźć w Google, więc dzięki.
Alexander Ljungberg
1
FWIW, jeśli potrzebujesz ustawić go z literału ciągu (być może z formularza, danych wejściowych użytkownika lub podobnego), możesz po prostu zrobić: thing.priority = getattr (thing, strvalue.upper ()).
mrooney,
1
Naprawdę podoba mi się sekcja Encapsulation na blogu.
Nathan Keller
Mam problem: zawsze widzę wartość domyślną na admin! Przetestowałem, że wartość naprawdę się zmienia! Co mam teraz zrobić?
Mahdi
To jest droga, ale uważaj: jeśli dodasz lub usuniesz opcje w przyszłości, twoje liczby nie będą sekwencyjne. Możesz ewentualnie skomentować nieaktualne wybory, aby w przyszłości nie było zamieszania i nie doszło do kolizji bazy danych.
grokpot
7

Prawdopodobnie skonfigurowałbym dyktando wyszukiwania wstecznego raz na zawsze, ale gdybym tego nie zrobił, użyłbym po prostu:

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

co wydaje się prostsze niż budowanie dyktu w locie tylko po to, aby go ponownie wyrzucić ;-).

Alex Martelli
źródło
Tak, rzucanie dyktatu jest trochę głupie, teraz, kiedy to mówisz. :)
Alexander Ljungberg
7

Oto typ pola, który napisałem kilka minut temu i myślę, że robi to, co chcesz. Jego konstruktor wymaga argumentu „choice”, który może być albo krotką z 2 krotkami w tym samym formacie co opcja choice dla IntegerField, lub zamiast tego prostą listą nazw (np. ChoiceField (('Low', 'Normal', „Wysoki”), domyślnie = „Niski”)). Klasa dba o mapowanie ciągu znaków na int, nigdy nie zobaczysz 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]

źródło
1
Nieźle, Allan. Dzięki!
Alexander Ljungberg
5

Doceniam ciągłe definiowanie, ale uważam, że typ Enum jest najlepszy do tego zadania. Mogą reprezentować liczbę całkowitą i ciąg znaków dla elementu w tym samym czasie, zachowując jednocześnie bardziej czytelny kod.

Wyliczenia zostały wprowadzone do Pythona w wersji 3.4. Jeśli używasz niżej (jak w wersji 2.x) nadal można mieć go instalując backportowaną pakiet : 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++'

To prawie wszystko. Możesz dziedziczyć, ChoiceEnumaby tworzyć własne definicje i używać ich w definicji modelu, na przykład:

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

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

Zapytanie jest wisienką na torcie, jak można się domyślić:

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

Reprezentowanie ich w łańcuchu jest również łatwe:

# 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
źródło
1
Jeśli wolisz nie wprowadzać klasy bazowej, takiej jak ChoiceEnum, możesz użyć .valuei, .namejak opisuje @kirpit, i zastąpić użycie choices()z - tuple([(x.value, x.name) for x in cls])albo w funkcji (DRY), albo bezpośrednio w konstruktorze pola.
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)

Użyj tego w ten sposób:

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

priority = models.IntegerField(default=Priorities.LOW, choices=Priorities)
mpen
źródło
3

Od Django 3.0 możesz używać:

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
źródło
1

Po prostu zastąp swoje liczby wartościami czytelnymi dla człowieka, które chcesz. Takie jak:

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

To sprawia, że ​​jest czytelny dla człowieka, jednak musiałbyś zdefiniować własną kolejność.

AlbertoPL
źródło
1

Moja odpowiedź jest bardzo późna i może wydawać się oczywista dla dzisiejszych ekspertów od Django, ale dla każdego, kto tu wyląduje, niedawno odkryłem bardzo eleganckie rozwiązanie wprowadzone przez django-model-utils: https://django-model-utils.readthedocs.io/ pl / latest / utilities.html # choice

Ten pakiet umożliwia zdefiniowanie wyborów za pomocą trzech krotek, gdzie:

  • Pierwsza pozycja to wartość bazy danych
  • Druga pozycja to wartość czytelna w kodzie
  • Trzecia pozycja to wartość czytelna dla człowieka

Oto, co możesz zrobić:

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)

Tą drogą:

  • Możesz użyć wartości czytelnej dla człowieka, aby faktycznie wybrać wartość swojego pola (w moim przypadku jest to przydatne, ponieważ zbieram dziką zawartość i przechowuję ją w znormalizowany sposób)
  • Czysta wartość jest przechowywana w Twojej bazie danych
  • Nie masz nic innego niż SUCHA do roboty;)

Cieszyć się :)

David Guillot
źródło
1
Całkowicie poprawne, aby wywołać django-model-utils. Jedna mała sugestia, aby zwiększyć czytelność; użyj notacji z kropką również na parametrze domyślnym: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(nie trzeba dodawać, że przypisanie priorytetu musi być wcięte, aby znajdowało się wewnątrz klasy rzeczy, aby to zrobić). Rozważ również użycie małych liter / znaków dla identyfikatora Pythona, ponieważ nie odnosisz się do klasy, ale zamiast tego parametru (twoje Wybory stałyby się wtedy: (0, 'low', 'Low'),i tak dalej).
Kim,
0

Pierwotnie użyłem zmodyfikowanej wersji odpowiedzi @ Allana:

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))

Uproszczony django-enumfieldspakiet, którego teraz używam:

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
źródło
0

Opcja wyborów modelu akceptuje sekwencję składającą się z iterowalnych dokładnie dwóch elementów (np. [(A, B), (A, B) ...]) do wyboru dla tego pola.

Ponadto Django zapewnia typy wyliczeniowe , które można podklasy definiować w celu zdefiniowania wyborów w zwięzły sposób:

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 obsługuje dodawanie dodatkowej wartości ciągu na końcu tej krotki, która będzie używana jako nazwa czytelna dla człowieka lub etykieta. Etykieta może być leniwym ciągiem do tłumaczenia.

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

Uwaga: można użyć, że używanie ThingPriority.HIGH, ThingPriority.['HIGH']lub ThingPriority(0)do dostępu lub odnośników członków enum.

mmsilviu
źródło