Django IntegerField'ı seçimler =… ada göre ayarla


109

Seçim seçeneği olan bir model alanınız olduğunda, insan tarafından okunabilir adlarla ilişkili bazı sihirli değerlere sahip olma eğilimindesinizdir. Django'da bu alanları değer yerine insan tarafından okunabilir ada göre ayarlamanın uygun bir yolu var mı?

Bu modeli düşünün:

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

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

Bir noktada bir Thing örneğimiz var ve önceliğini belirlemek istiyoruz. Açıkçası yapabilirsin,

thing.priority = 1

Ancak bu sizi ÖNCELİKLERİN Değer-Adı eşlemesini ezberlemeye zorlar. Bu çalışmıyor:

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

Şu anda bu aptalca çözümüm var:

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

ama bu hantal. Bu senaryonun ne kadar yaygın olabileceği düşünüldüğünde, birinin daha iyi bir çözümü olup olmadığını merak ediyordum. Alanları seçim adına göre ayarlamak için tamamen gözden kaçırdığım bir alan yöntemi var mı?

Yanıtlar:


164

Burada görüldüğü gibi yapın . O zaman doğru tamsayıyı temsil eden bir kelime kullanabilirsiniz.

Şöyle:

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

O zaman onlar hala DB'de tamsayılardır.

Kullanım olacaktır thing.priority = Thing.NORMAL


2
Bu, konuyla ilgili güzel ayrıntılı bir blog yazısı. Google ile bulmak da zor, bu yüzden teşekkürler.
Alexander Ljungberg

1
FWIW, eğer onu değişmez bir dizeden (belki bir formdan, kullanıcı girdisinden veya benzerinden) ayarlamanız gerekiyorsa, o zaman sadece şunu yapabilirsiniz: thing.priority = getattr (şey, strvalue.upper ()).
mrooney

1
Blogdaki Kapsülleme bölümü gibi.
Nathan Keller

Bir sorunum var: Yönetici üzerinde her zaman varsayılan değeri görüyorum! Değerin gerçekten değiştiğini test ettim! Ben şimdi ne yapmalıyım?
Mahdi

Gitmenin yolu budur, ancak dikkatli olun: Gelecekte seçenekler ekler veya çıkarırsanız, numaralarınız sıralı olmayacaktır. Muhtemelen kullanımdan kaldırılan seçenekleri yorumlayabilirsiniz, böylece gelecekte kafa karışıklığı olmaz ve db çarpışmalarıyla karşılaşmazsınız.
grokpot

7

Muhtemelen geriye doğru arama diktesini ilk ve son olarak ayarlardım, ama yapmasaydım sadece şunu kullanırdım:

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

bu, dikteyi anında tekrar atmaktan daha basit görünüyor ;-).


Evet, şimdi söylediğinize göre dikte atmak biraz aptalca. :)
Alexander Ljungberg

7

İşte birkaç dakika önce yazdığım ve istediğini yaptığını düşündüğüm bir alan türü. Yapıcısı, IntegerField seçenekleriyle aynı formatta 2-tuple'lık bir demet veya bunun yerine basit bir isim listesi (yani ChoiceField (('Düşük', 'Normal', 'Yüksek'), varsayılan = 'Düşük')). Sınıf sizin için dizeden int'e olan eşlemeyi sizin için halleder, int'i asla görmezsiniz.

  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
Bu fena değil Allan. Teşekkürler!
Alexander Ljungberg

5

Sürekli tanımlama şeklini takdir ediyorum ama Enum tipinin bu görev için çok daha iyi olduğuna inanıyorum . Kodunuzu daha okunaklı tutarken aynı anda bir öğe için tamsayı ve bir dizeyi temsil edebilirler.

Numaralar Python 3.4 sürümünde tanıtıldı. Eğer herhangi bir alt (v2.x gibi) kullanıyorsanız yine yükleyerek ona sahip olabilir backported paketi : 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++'

Bu hemen hemen hepsi. ChoiceEnumKendi tanımlarınızı oluşturmak ve bunları aşağıdaki gibi bir model tanımında kullanmak için öğesini devralabilirsiniz :

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

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

Sorgu, tahmin edebileceğiniz gibi pastanın üzerine krema yapmaktır:

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

Bunları dizede temsil etmek de kolaylaştı:

# 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
Bir temel sınıf gibi tanıtmak tercih etmiyorsanız ChoiceEnum, kullanabileceğiniz .valueve .name@kirpit olarak tanımladığı ve kullanımını değiştirmek choices()ile tuple([(x.value, x.name) for x in cls])alanın yapıcı doğrudan bir fonksiyonu (KURU) içinde --either veya.
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)

Bunu şu şekilde kullanın:

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

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

3

Django 3.0'dan itibaren şunları kullanabilirsiniz:

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

Sayılarınızı, istediğiniz insan tarafından okunabilir değerlerle değiştirin. Gibi:

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

Bu, onu okunabilir hale getirir, ancak kendi sıralamanızı tanımlamanız gerekir.


1

Cevabım çok geç ve bugünlerde Django uzmanlarına açık görünebilir, ancak buraya kim gelirse gelsin, son zamanlarda django-model-utils tarafından getirilen çok zarif bir çözüm keşfettim: https://django-model-utils.readthedocs.io/ tr / son / utilities.html # seçimler

Bu paket, aşağıdaki durumlarda üç taneli Seçenekleri tanımlamanıza olanak tanır:

  • İlk öğe veritabanı değeridir
  • İkinci öğe, kodla okunabilir bir değerdir
  • Üçüncü öğe, insan tarafından okunabilir bir değerdir

İşte yapabilecekleriniz:

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)

Bu yoldan:

  • Alanınızın değerini gerçekten seçmek için insan tarafından okunabilir değerinizi kullanabilirsiniz (benim durumumda yararlıdır çünkü vahşi içeriği kazıyıp normalleştirilmiş bir şekilde depoluyorum)
  • Veritabanınızda temiz bir değer saklanır
  • KURU olmayan bir işiniz yok;)

Zevk almak :)


1
Django-model-utils'i getirmek için tamamen geçerlidir. Okunabilirliği artırmak için küçük bir öneri; varsayılan parametrede de noktalı gösterimi kullanın: priority = models.IntegerField(default=PRIORITIES.Low, choices=PRIORITIES)(söylemeye gerek yok, bunu yapmak için öncelik atamasının Thing sınıfı içinde girintilenmesi gerekir). Ayrıca, bir sınıfa değil, bunun yerine bir parametreye atıfta bulunduğunuz için python tanımlayıcısı için küçük harfli kelimeler / karakterler kullanmayı düşünün (Seçenekleriniz daha sonra: (0, 'low', 'Low'),ve benzeri hale gelir ).
Kim

0

Başlangıçta @ Allan'ın cevabının değiştirilmiş bir versiyonunu kullandım:

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Şimdi kullandığım paket paketiyle basitleştirildi :

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

Modelin seçimler seçeneği, bu alan için seçenekler olarak kullanmak üzere tam olarak iki öğeden (örneğin [(A, B), (A, B) ...]) oluşan yinelemelerden oluşan bir diziyi kabul eder.

Ek olarak, Django, seçimleri kısa bir şekilde tanımlamak için alt sınıflara ayırabileceğiniz numaralandırma türleri sağlar :

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, insan tarafından okunabilir ad veya etiket olarak kullanılmak üzere bu demetin sonuna fazladan bir dize değeri eklemeyi destekler. Etiket, tembel çevrilebilir bir dize olabilir.

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

Not: Kullanabileceğiniz kullanarak o ThingPriority.HIGH, ThingPriority.['HIGH']ya ThingPriority(0)erişim veya arama enum üyelerine.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.