Setzen Sie Django IntegerField nach Auswahl =… Name

109

Wenn Sie ein Modellfeld mit einer Auswahloption haben, sind in der Regel einige magische Werte mit von Menschen lesbaren Namen verknüpft. Gibt es in Django eine bequeme Möglichkeit, diese Felder durch den vom Menschen lesbaren Namen anstelle des Werts festzulegen?

Betrachten Sie dieses Modell:

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

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

Irgendwann haben wir eine Thing-Instanz und möchten ihre Priorität festlegen. Offensichtlich könnten Sie tun,

thing.priority = 1

Dies zwingt Sie jedoch dazu, sich die Wert-Name-Zuordnung von PRIORITÄTEN zu merken. Das funktioniert nicht:

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

Derzeit habe ich diese dumme Problemumgehung:

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

aber das ist klobig. Angesichts der Häufigkeit dieses Szenarios fragte ich mich, ob jemand eine bessere Lösung hätte. Gibt es eine Feldmethode zum Festlegen von Feldern nach Auswahlnamen, die ich völlig übersehen habe?

Alexander Ljungberg
quelle

Antworten:

164

Mach wie hier gesehen . Dann können Sie ein Wort verwenden, das die richtige Ganzzahl darstellt.

Wie so:

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

Dann sind sie noch ganze Zahlen in der DB.

Verwendung wäre thing.priority = Thing.NORMAL


quelle
2
Das ist ein sehr detaillierter Blog-Beitrag zu diesem Thema. Auch bei Google schwer zu finden, danke.
Alexander Ljungberg
1
FWIW, wenn Sie es aus einer Literalzeichenfolge (möglicherweise aus einem Formular, einer Benutzereingabe oder ähnlichem) festlegen müssen, können Sie einfach Folgendes tun: thing.priority = getattr (thing, strvalue.upper ()).
Mrooney
1
Wirklich wie der Abschnitt Encapsulation im Blog.
Nathan Keller
Ich habe ein Problem: Ich sehe immer den Standardwert auf dem Admin! Ich habe getestet, dass sich der Wert wirklich ändert! Was sollte ich jetzt tun?
Mahdi
Dies ist der richtige Weg, aber Vorsicht: Wenn Sie in Zukunft Auswahlmöglichkeiten hinzufügen oder entfernen, sind Ihre Nummern nicht fortlaufend. Sie könnten möglicherweise veraltete Entscheidungen auskommentieren, damit es in Zukunft nicht zu Verwirrung kommt und Sie nicht auf Datenbankkollisionen stoßen.
Grokpot
7

Ich würde wahrscheinlich das Reverse-Lookup-Diktat ein für alle Mal einrichten, aber wenn ich es nicht getan hätte, würde ich einfach verwenden:

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

Das scheint einfacher zu sein, als das Diktat spontan zu erstellen, nur um es wieder wegzuwerfen ;-).

Alex Martelli
quelle
Ja, das Diktat zu werfen ist ein bisschen albern, jetzt wo du es sagst. :)
Alexander Ljungberg
7

Hier ist ein Feldtyp, den ich vor ein paar Minuten geschrieben habe und der meiner Meinung nach das tut, was Sie wollen. Sein Konstruktor erfordert ein Argument 'Auswahl', das entweder ein Tupel von 2 Tupeln im gleichen Format wie die Auswahloption für IntegerField sein kann, oder stattdessen eine einfache Liste von Namen (dh ChoiceField (('Niedrig', 'Normal', 'Hoch'), Standard = 'Niedrig')). Die Klasse kümmert sich für Sie um die Zuordnung von String zu Int. Sie sehen das Int nie.

  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]

quelle
1
Das ist nicht schlecht, Allan. Vielen Dank!
Alexander Ljungberg
5

Ich schätze die konstante Art der Definition, aber ich glaube, dass der Enum- Typ für diese Aufgabe bei weitem am besten geeignet ist. Sie können eine Ganzzahl und eine Zeichenfolge für ein Element gleichzeitig darstellen und gleichzeitig Ihren Code besser lesbar halten.

In Version 3.4 wurden Enums in Python eingeführt. Wenn Sie ein niedrigeres (z. B. v2.x) verwenden, können Sie es weiterhin haben, indem Sie das backportierte Paket installieren : 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++'

Das ist so ziemlich alles. Sie können das erben ChoiceEnum, um eigene Definitionen zu erstellen und diese in einer Modelldefinition wie folgt zu verwenden:

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

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

Fragen sind das i-Tüpfelchen, wie Sie sich vorstellen können:

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

Die Darstellung in einer Zeichenfolge wird ebenfalls vereinfacht:

# 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
quelle
1
Wenn Sie keine Basisklasse wie einführen möchten ChoiceEnum, können Sie das .valueund verwenden, .namewie @kirpit beschreibt, und die Verwendung von choices()durch - tuple([(x.value, x.name) for x in cls])entweder in einer Funktion (DRY) oder direkt im Konstruktor des Felds ersetzen .
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)

Verwenden Sie es so:

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

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

Ab Django 3.0 können Sie Folgendes verwenden:

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
quelle
1

Ersetzen Sie einfach Ihre Zahlen durch die von Menschen lesbaren Werte, die Sie möchten. So wie:

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

Dies macht es für den Menschen lesbar, Sie müssten jedoch Ihre eigene Reihenfolge definieren.

AlbertoPL
quelle
1

Meine Antwort ist sehr spät und mag heutzutage-Django-Experten offensichtlich erscheinen, aber für jeden, der hier landet, habe ich kürzlich eine sehr elegante Lösung entdeckt, die von Django-Modell-Utils gebracht wurde: https://django-model-utils.readthedocs.io/ de / latest / utilities.html # Auswahlmöglichkeiten

Mit diesem Paket können Sie Auswahlmöglichkeiten mit drei Tupeln definieren, wobei:

  • Das erste Element ist der Datenbankwert
  • Das zweite Element ist ein Code-lesbarer Wert
  • Der dritte Punkt ist ein für Menschen lesbarer Wert

Folgendes können Sie also tun:

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)

Diesen Weg:

  • Sie können Ihren für Menschen lesbaren Wert verwenden, um den Wert Ihres Felds tatsächlich auszuwählen (in meinem Fall ist dies nützlich, weil ich wilde Inhalte kratzt und sie auf normalisierte Weise speichere).
  • Ein sauberer Wert wird in Ihrer Datenbank gespeichert
  • Du hast nichts Nicht-TROCKENES zu tun;)

Genießen :)

David Guillot
quelle
1
Völlig gültig, um Django-Modell-Utils aufzurufen. Ein kleiner Vorschlag zur Verbesserung der Lesbarkeit; Verwenden Sie auch die Punktnotation für den Standardparameter: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(Dazu muss natürlich die Prioritätszuweisung eingerückt werden, damit sie sich innerhalb der Thing-Klasse befindet.) Erwägen Sie auch die Verwendung von Wörtern / Zeichen in Kleinbuchstaben für die Python-ID, da Sie sich nicht auf eine Klasse, sondern auf einen Parameter beziehen (Ihre Auswahl würde dann: (0, 'low', 'Low'),und so weiter).
Kim
0

Ursprünglich habe ich eine modifizierte Version von @ Allans Antwort verwendet:

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

Vereinfacht mit dem django-enumfieldsPaketpaket, das ich jetzt benutze:

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
quelle
0

Die Auswahloption des Modells akzeptiert eine Sequenz, die aus Iterablen von genau zwei Elementen besteht (z. B. [(A, B), (A, B) ...]), um sie als Auswahl für dieses Feld zu verwenden.

Darüber hinaus bietet Django Aufzählungstypen , die Sie in Unterklassen unterteilen können, um Auswahlmöglichkeiten auf übersichtliche Weise zu definieren:

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 unterstützt das Hinzufügen eines zusätzlichen Zeichenfolgenwerts am Ende dieses Tupels, der als lesbarer Name oder Bezeichnung verwendet werden soll. Das Etikett kann eine faul übersetzbare Zeichenfolge sein.

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

Hinweis: Sie verwenden können , dass die Verwendung von ThingPriority.HIGH, ThingPriority.['HIGH']oder für ThingPriority(0)den Zugriff oder Lookup - Enumeration Mitglieder.

mmsilviu
quelle