Setel Django IntegerField dengan pilihan =… name


109

Saat Anda memiliki bidang model dengan opsi pilihan, Anda cenderung memiliki beberapa nilai ajaib yang terkait dengan nama yang dapat dibaca manusia. Apakah ada di Django cara mudah untuk menyetel bidang ini dengan nama yang dapat dibaca manusia alih-alih nilainya?

Pertimbangkan model ini:

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

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

Di beberapa titik kami memiliki instance Thing dan kami ingin menetapkan prioritasnya. Jelas Anda bisa melakukannya,

thing.priority = 1

Tapi itu memaksa Anda untuk menghafal pemetaan Nama-Nilai PRIORITAS. Ini tidak berhasil:

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

Saat ini saya memiliki solusi konyol ini:

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

tapi itu kikuk. Mengingat betapa umum skenario ini, saya bertanya-tanya apakah ada yang punya solusi yang lebih baik. Apakah ada metode bidang untuk menyetel bidang dengan nama pilihan yang benar-benar saya abaikan?

Jawaban:


164

Lakukan seperti yang terlihat di sini . Kemudian Anda dapat menggunakan kata yang mewakili bilangan bulat yang tepat.

Seperti:

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

Kemudian mereka masih bilangan bulat di DB.

Penggunaan akan thing.priority = Thing.NORMAL


2
Itu adalah posting blog yang sangat rinci tentang masalah ini. Sulit ditemukan dengan Google juga jadi terima kasih.
Alexander Ljungberg

1
FWIW, jika Anda perlu mengaturnya dari string literal (mungkin dari formulir, input pengguna, atau serupa) Anda dapat melakukan: thing.priority = getattr (thing, strvalue.upper ()).
mrooney

1
Sangat menyukai bagian Enkapsulasi di blog.
Nathan Keller

Saya punya masalah: Saya selalu melihat nilai default di admin! Saya telah menguji bahwa nilainya benar-benar berubah! Apa yang harus saya lakukan sekarang?
Mahdi

Ini adalah cara yang harus dilakukan tetapi berhati-hatilah: jika Anda menambah atau menghapus pilihan di masa mendatang, nomor Anda tidak akan berurutan. Anda mungkin dapat mengomentari pilihan yang tidak berlaku lagi sehingga tidak akan ada kebingungan di masa mendatang dan Anda tidak akan mengalami benturan db.
grokpot

7

Saya mungkin akan menyiapkan dikt pencarian balik sekali dan untuk selamanya, tetapi jika tidak, saya akan menggunakan:

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

yang tampaknya lebih sederhana daripada membuat dict dengan cepat hanya untuk membuangnya lagi ;-).


Ya, membuang dikt agak konyol, sekarang Anda mengatakannya. :)
Alexander Ljungberg

7

Berikut adalah jenis bidang yang saya tulis beberapa menit yang lalu yang menurut saya sesuai dengan keinginan Anda. Konstruktornya memerlukan argumen 'pilihan', yang bisa berupa tuple 2-tupel dalam format yang sama dengan pilihan opsi untuk IntegerField, atau sebagai gantinya daftar nama sederhana (yaitu ChoiceField (('Low', 'Normal', 'Tinggi'), default = 'Rendah')). Kelas menangani pemetaan dari string ke int untuk Anda, Anda tidak pernah melihat 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
Itu tidak buruk, Allan. Terima kasih!
Alexander Ljungberg

5

Saya menghargai cara mendefinisikan yang konstan tetapi saya percaya tipe Enum jauh terbaik untuk tugas ini. Mereka dapat mewakili integer dan string untuk item dalam waktu yang sama, sekaligus menjaga kode Anda lebih mudah dibaca.

Enum diperkenalkan ke Python pada versi 3.4. Jika Anda menggunakan lebih rendah (seperti v2.x) Anda masih dapat memilikinya dengan menginstal paket 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++'

Ini cukup banyak. Anda dapat mewarisi ChoiceEnumuntuk membuat definisi Anda sendiri dan menggunakannya dalam definisi model seperti:

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

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

Kueri adalah lapisan gula pada kue karena Anda mungkin menebak:

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

Mewakili mereka dalam string juga menjadi mudah:

# 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
Jika Anda memilih untuk tidak memperkenalkan kelas dasar seperti ChoiceEnum, Anda dapat menggunakan .valuedan .nameseperti yang dijelaskan @kirpit dan mengganti penggunaan choices()dengan - tuple([(x.value, x.name) for x in cls])baik dalam fungsi (KERING) atau langsung di konstruktor bidang.
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)

Gunakan seperti ini:

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

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

3

Pada Django 3.0, Anda dapat menggunakan:

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

Cukup ganti nomor Anda dengan nilai yang dapat dibaca manusia yang Anda inginkan. Dengan demikian:

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

Ini membuatnya dapat dibaca manusia, namun, Anda harus menentukan pemesanan Anda sendiri.


1

Jawaban saya sangat terlambat dan mungkin tampak jelas bagi para ahli-Django saat ini, tetapi bagi siapa pun yang mendarat di sini, saya baru-baru ini menemukan solusi yang sangat elegan yang dibawa oleh django-model-utils: https://django-model-utils.readthedocs.io/ en / latest / utilities.html # choices

Paket ini memungkinkan Anda untuk menentukan Pilihan dengan tiga tupel di mana:

  • Item pertama adalah nilai database
  • Item kedua adalah nilai yang dapat dibaca kode
  • Item ketiga adalah nilai yang bisa dibaca manusia

Jadi, inilah yang dapat Anda lakukan:

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)

Cara ini:

  • Anda dapat menggunakan nilai yang dapat dibaca manusia untuk benar-benar memilih nilai bidang Anda (dalam kasus saya, ini berguna karena saya mengikis konten liar dan menyimpannya dengan cara yang dinormalisasi)
  • Nilai bersih disimpan dalam database Anda
  • Anda tidak memiliki sesuatu yang tidak KERING untuk dilakukan;)

Nikmati :)


1
Benar-benar valid untuk menampilkan django-model-utils. Satu saran kecil untuk meningkatkan keterbacaan; gunakan juga notasi titik pada parameter default: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(tentu saja, penugasan prioritas harus diindentasi agar berada di dalam kelas Thing untuk melakukannya). Juga pertimbangkan untuk menggunakan kata / karakter huruf kecil untuk pengenal python, karena Anda tidak merujuk ke kelas, tetapi sebagai gantinya parameter (Pilihan Anda akan menjadi: (0, 'low', 'Low'),dan seterusnya).
Kim

0

Awalnya saya menggunakan versi modifikasi dari jawaban @ 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))

Disederhanakan dengan django-enumfieldspaket paket yang sekarang saya gunakan:

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

Opsi pilihan model menerima urutan yang terdiri dari iterabel tepat dua item (misalnya [(A, B), (A, B) ...]) untuk digunakan sebagai pilihan untuk bidang ini.

Sebagai tambahan, Django menyediakan tipe pencacahan yang Anda dapat subkelas untuk mendefinisikan pilihan dengan cara yang ringkas:

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 mendukung penambahan nilai string ekstra ke akhir tupel ini untuk digunakan sebagai nama, atau label yang bisa dibaca manusia. Label dapat berupa string yang dapat diterjemahkan dengan malas.

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

Catatan: Anda dapat menggunakan menggunakan ThingPriority.HIGH, ThingPriority.['HIGH']atau ThingPriority(0)untuk mengakses atau anggota enum lookup.

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.