如何在 Django 中正确使用 "choices" 字段选项



我正在阅读这里的教程:https://docs.djangoproject.com/en/1.5/ref/models/fields/#choices我正在尝试创建一个框,用户可以在其中选择他出生的月份。我尝试的是

 MONTH_CHOICES = (
    (JANUARY, "January"),
    (FEBRUARY, "February"),
    (MARCH, "March"),
    ....
    (DECEMBER, "December"),
)
month = CharField(max_length=9,
                  choices=MONTHS_CHOICES,
                  default=JANUARY)

这是对的吗?我看到在我正在阅读的教程中,他们出于某种原因首先创建了变量,如下所示

FRESHMAN = 'FR'
SOPHOMORE = 'SO'
JUNIOR = 'JR'
SENIOR = 'SR'

他们为什么要创建这些变量?此外,MONTHS_CHOICES位于一个名为 People 的模型中,所以我提供的代码是否会在数据库中创建一个名为"People"的"月份选择")列,并且它会说用户在点击月份并提交表单后出生在哪个月份?

我认为没有人真正回答过第一个问题:

他们为什么要创建这些变量?

这些变量不是绝对必需的。是真的。你可以完美地做这样的事情:

MONTH_CHOICES = (
    ("JANUARY", "January"),
    ("FEBRUARY", "February"),
    ("MARCH", "March"),
    # ....
    ("DECEMBER", "December"),
)
month = models.CharField(max_length=9,
                  choices=MONTH_CHOICES,
                  default="JANUARY")

为什么使用变量更好?错误预防和逻辑分离。

JAN = "JANUARY"
FEB = "FEBRUARY"
MAR = "MAR"
# (...)
MONTH_CHOICES = (
    (JAN, "January"),
    (FEB, "February"),
    (MAR, "March"),
    # ....
    (DEC, "December"),
)

现在,假设您有一个视图,您可以在其中创建新的模型实例。而不是这样做:

new_instance = MyModel(month='JANUARY')

您将执行以下操作:

new_instance = MyModel(month=MyModel.JAN)

在第一个选项中,对值进行硬编码。如果可以输入一组值,则应在编码时限制这些选项。此外,如果您最终需要在模型层更改代码,现在您无需在视图层中进行任何更改。

对于 Django3.0+,请使用 models.TextChoices(有关枚举类型,请参阅 docs-v3.0)

from django.db import models
class MyModel(models.Model):
    class Month(models.TextChoices):
        JAN = "1", "JANUARY"
        FEB = "2", "FEBRUARY"
        MAR = "3", "MAR"
        # (...)
    month = models.CharField(
        max_length=2,
        choices=Month.choices,
        default=Month.JAN
    )

用法::

>>> obj = MyModel.objects.create(month='1')
>>> assert obj.month == obj.Month.JAN == '1'
>>> assert MyModel.Month(obj.month) is obj.Month.JAN
>>> assert MyModel.Month(obj.month).value is '1'
>>> assert MyModel.Month(obj.month).label == 'JANUARY'
>>> assert MyModel.Month(obj.month).name == 'JAN'
>>> assert MyModel.objects.filter(month=MyModel.Month.JAN).count() >= 1
>>> obj2 = MyModel(month=MyModel.Month.FEB)
>>> assert obj2.get_month_display() == obj2.Month(obj2.month).label

假设我们知道标签是"JAN",如何获得名称"JAN"和值"1"?

label = "JANUARY"
name = {i.label: i.name for i in MyModel.Month}[label]
print(repr(name))  # 'JAN'
value = {i.label: i.value for i in MyModel.Month}[label]
print(repr(value))  # '1'

就个人而言,我宁愿使用models.IntegerChoices

class MyModel(models.Model):
    class Month(models.IntegerChoices):
        JAN = 1, "JANUARY"
        FEB = 2, "FEBRUARY"
        MAR = 3, "MAR"
        # (...)
    month = models.PositiveSmallIntegerField(
        choices=Month.choices,
        default=Month.JAN
    )

根据文档:

字段选择权

可迭代对象(例如,列表或元组)由自身组成 正好两个项目(例如 [(A, B), (A, B) ...]) 的迭代对象用作 此字段的选项。如果给出,默认表单小部件将 成为具有这些选项的选择框,而不是标准文本字段。

每个元组中的第一个元素是要存储的实际值,并且 第二个元素是人类可读的名称。

所以,你的代码是正确的,除了你应该定义变量JANUARYFEBRUARY等或使用calendar模块来定义MONTH_CHOICES

import calendar
...
class MyModel(models.Model):
    ...
    MONTH_CHOICES = [(str(i), calendar.month_name[i]) for i in range(1,13)]
    month = models.CharField(max_length=9, choices=MONTH_CHOICES, default='1')

最干净的解决方案是使用 django-model-utils 库:

from model_utils import Choices
class Article(models.Model):
    STATUS = Choices('draft', 'published')
    status = models.CharField(choices=STATUS, default=STATUS.draft, max_length=20)

https://django-model-utils.readthedocs.io/en/latest/utilities.html#choices

2023 年 1 月更新:

新方法是使用模型。TextChoices如下所示,它是内置的,因此您无需安装任何软件包。*第一个值是实际值,第二个值显示在 Django Admin 上:

# "models.py"
from django.db import models
class MyModel(models.Model):
    class Months(models.TextChoices):
     # Actual value ↓      # ↓ Displayed on Django Admin  
        JANUARY = 'JAN', 'January'
        FEBRUARY = 'FEB', 'February'
        MARCH = 'MAR', 'March'
        APRIL = 'APR', 'April'
        MAY = 'MAY', 'May'
    month = models.CharField(
        max_length=3,
        choices=Months.choices,
        default=Months.APRIL 
    )
    class YearInSchool(models.TextChoices):
     # Actual value ↓      # ↓ Displayed on Django Admin
        FRESHMAN = 'FR', 'Freshman'
        SOPHOMORE = 'SO', 'Sophomore'
        JUNIOR = 'JR', 'Junior'
        SENIOR = 'SR', 'Senior'
        GRADUATE = 'GR', 'Graduate'
    year_in_school = models.CharField(
        max_length=2,
        choices=YearInSchool.choices,
        default=YearInSchool.SOPHOMORE,
    )

我还以内置的旧方式重写了上面的代码。

# "models.py"
from django.db import models
class MyModel(models.Model):
              # ↓ Actual value
    JANUARY = 'JAN'
    FEBRUARY = 'FEB'
    MARCH = 'MAR'
    APRIL = 'APR'
    MAY = 'MAY'
    MONTHS = [      # ↓ Displayed on Django Admin
        (JANUARY, 'January'),
        (FEBRUARY, 'February'),
        (MARCH, 'March'),
        (APRIL, 'April'),
        (MAY, 'May'),
    ]
    month = models.CharField(
        max_length=3,
        choices=MONTHS,
        default=APRIL # Or "default=MONTHS[3]"
    )
              # ↓ Actual value
    FRESHMAN = 'FR'
    SOPHOMORE = 'SO'
    JUNIOR = 'JR'
    SENIOR = 'SR'
    GRADUATE = 'GR'
    YEAR_IN_SCHOOL_CHOICES = [
                     # ↓ Displayed on Django Admin
        (FRESHMAN, 'Freshman'),
        (SOPHOMORE, 'Sophomore'),
        (JUNIOR, 'Junior'),
        (SENIOR, 'Senior'),
        (GRADUATE, 'Graduate'),
    ]
    year_in_school = models.CharField(
        max_length=2,
        choices=YEAR_IN_SCHOOL_CHOICES,
        default=SOPHOMORE # Or "default=YEAR_IN_SCHOOL_CHOICES[1]"
    )

我建议使用django-model-utils而不是Django内置解决方案。此解决方案的主要优点是缺少字符串声明重复。所有选择项只声明一次。此外,这是使用 3 个值声明选择并存储与源代码中的用法不同的数据库值的最简单方法。

from django.utils.translation import ugettext_lazy as _
from model_utils import Choices
class MyModel(models.Model):
   MONTH = Choices(
       ('JAN', _('January')),
       ('FEB', _('February')),
       ('MAR', _('March')),
   )
   # [..]
   month = models.CharField(
       max_length=3,
       choices=MONTH,
       default=MONTH.JAN,
   )

并使用 IntegerField 代替:

from django.utils.translation import ugettext_lazy as _
from model_utils import Choices
class MyModel(models.Model):
   MONTH = Choices(
       (1, 'JAN', _('January')),
       (2, 'FEB', _('February')),
       (3, 'MAR', _('March')),
   )
   # [..]
   month = models.PositiveSmallIntegerField(
       choices=MONTH,
       default=MONTH.JAN,
   )
  • 这种方法有一个小缺点:在任何IDE中(例如。PyCharm),可用选项将没有代码完成(这是因为这些值不是 Choices 类的标准成员)。

代码中不能有裸词,这就是他们创建变量的原因(您的代码将失败并NameError)。

您提供的代码将创建一个名为 month 的数据库表(加上 django 添加到其中的任何前缀),因为这是CharField的名称。

但是有更好的方法来创建您想要的特定选择。请参阅前面的堆栈溢出问题。

import calendar
tuple((m, m) for m in calendar.month_name[1:])

$ pip install django-better-choices

对于那些感兴趣的人,我创建了django-better-choices库,它提供了一个很好的界面来处理Python 3.7+的Django选项。它支持自定义参数,许多有用的功能,并且对IDE非常友好。

您可以将选择定义为类:

from django_better_choices import Choices

class PAGE_STATUS(Choices):
    CREATED = 'Created'
    PENDING = Choices.Value('Pending', help_text='This set status to pending')
    ON_HOLD = Choices.Value('On Hold', value='custom_on_hold')
    VALID = Choices.Subset('CREATED', 'ON_HOLD')
    class INTERNAL_STATUS(Choices):
        REVIEW = 'On Review'
    @classmethod
    def get_help_text(cls):
        return tuple(
            value.help_text
            for value in cls.values()
            if hasattr(value, 'help_text')
        )

然后执行以下操作以及更多操作:

print( PAGE_STATUS.CREATED )                # 'created'
print( PAGE_STATUS.ON_HOLD )                # 'custom_on_hold'
print( PAGE_STATUS.PENDING.display )        # 'Pending'
print( PAGE_STATUS.PENDING.help_text )      # 'This set status to pending'
'custom_on_hold' in PAGE_STATUS.VALID       # True
PAGE_STATUS.CREATED in PAGE_STATUS.VALID    # True
PAGE_STATUS.extract('CREATED', 'ON_HOLD')   # ~= PAGE_STATUS.VALID
for value, display in PAGE_STATUS:
    print( value, display )
PAGE_STATUS.get_help_text()
PAGE_STATUS.VALID.get_help_text()

当然,它完全支持Django和Django Migrations:

class Page(models.Model):
    status = models.CharField(choices=PAGE_STATUS, default=PAGE_STATUS.CREATED)

完整文档在这里:https://pypi.org/project/django-better-choices/

最新更新