ChatGPT解决这个技术问题 Extra ChatGPT

How to properly use the "choices" field option in Django

I'm reading the tutorial here: https://docs.djangoproject.com/en/1.5/ref/models/fields/#choices and i'm trying to create a box where the user can select the month he was born in. What I tried was

 MONTH_CHOICES = (
    (JANUARY, "January"),
    (FEBRUARY, "February"),
    (MARCH, "March"),
    ....
    (DECEMBER, "December"),
)

month = CharField(max_length=9,
                  choices=MONTHS_CHOICES,
                  default=JANUARY)

Is this correct? I see that in the tutorial I was reading, they for some reason created variables first, like so

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

Why did they create those variables? Also, the MONTHS_CHOICES is in a model called People, so would the code I provided create a "Months Choices) column in the database called called "People" and would it say what month the user was born in after he clicks on of the months and submits the form?

Add this point I suggest you look into Django-Choices package.

A
Abdul Aziz Barkat

I think no one actually has answered to the first question:

Why did they create those variables?

Those variables aren't strictly necessary. It's true. You can perfectly do something like this:

MONTH_CHOICES = (
    ("JANUARY", "January"),
    ("FEBRUARY", "February"),
    ("MARCH", "March"),
    # ....
    ("DECEMBER", "December"),
)

month = models.CharField(max_length=9,
                  choices=MONTH_CHOICES,
                  default="JANUARY")

Why using variables is better? Error prevention and logic separation.

JAN = "JANUARY"
FEB = "FEBRUARY"
MAR = "MAR"
# (...)

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

Now, imagine you have a view where you create a new Model instance. Instead of doing this:

new_instance = MyModel(month='JANUARY')

You'll do this:

new_instance = MyModel(month=MyModel.JAN)

In the first option you are hardcoding the value. If there is a set of values you can input, you should limit those options when coding. Also, if you eventually need to change the code at the Model layer, now you don't need to make any change in the Views layer.


Thanks for the detailed explanation and the example.
You should seriously consider namespacing variables you use for choices in Django model fields; it should be apparent that the variable is related to a specific field in order to avoid confusing future programmers who could add similar choice fields to the model. In python 3, I use an IntEnum class for this and I name it in a way that makes it obvious which model field it relates to.
Starting from python3.8 it could be more convenient to declare variables just inside declaring MONTH_CHOICES, like MONTH_CHOICES = ((JAN := "JANUARY", "January")...).
K
Kai - Kazuya Ito

For Django3.0+, use models.TextChoices (see docs-v3.0 for enumeration types)

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
    )

Usage::

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

Let's say we known the label is 'JANUARY', how to get the name 'JAN' and the value '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'

Personally, I would rather use 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
    )

This is a good solution for more recent versions of Django - with the added benefit that TextChoices can also be defined as a main class and shared across models. Good choice for never changing data.
Note that you can simply use JANUARY = 1, FEBRUARY = 2, etc. without specifying the label and Django is smart enough to produce 'January', 'February', etc. from that in the choice option list.
R
Rajat Jain

According to the documentation:

Field.choices An iterable (e.g., a list or tuple) consisting itself of iterables of exactly two items (e.g. [(A, B), (A, B) ...]) to use as choices for this field. If this is given, the default form widget will be a select box with these choices instead of the standard text field. The first element in each tuple is the actual value to be stored, and the second element is the human-readable name.

So, your code is correct, except that you should either define variables JANUARY, FEBRUARY etc. or use calendar module to define 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')

B
Babken Vardanyan

The cleanest solution is to use the django-model-utils library:

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


Hmm..I am personally not comfortable in installing a library for such a simple task. look at @Waket Zheng's answer
d
dtar

I would suggest to use django-model-utils instead of Django built-in solution. The main advantage of this solution is the lack of string declaration duplication. All choice items are declared exactly once. Also this is the easiest way for declaring choices using 3 values and storing database value different than usage in source code.

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

And with usage IntegerField instead:

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

This method has one small disadvantage: in any IDE (eg. PyCharm) there will be no code completion for available choices (it’s because those values aren’t standard members of Choices class).


C
Community

You can't have bare words in the code, that's the reason why they created variables (your code will fail with NameError).

The code you provided would create a database table named month (plus whatever prefix django adds to that), because that's the name of the CharField.

But there are better ways to create the particular choices you want. See a previous Stack Overflow question.

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

K
Kai - Kazuya Ito

Mar, 2022 Update:

The simplest, easiest, best and new way is using "models.TextChoices" which is built-in which means "You don't need to install any packages".

"models.py":

from django.db import models

class MyModel(models.Model):

    class Months(models.TextChoices):
        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):
        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,
    )

I also rewrote the code above in the old way which is also built-in.

"models.py":

from django.db import models

class MyModel(models.Model):

    JANUARY = 'JAN'
    FEBRUARY = 'FEB'
    MARCH = 'MAR'
    APRIL = 'APR'
    MAY = 'MAY'

    MANTHS = [
        (JANUARY, 'January'),
        (FEBRUARY, 'February'),
        (MARCH, 'March'),
        (APRIL, 'April'),
        (MAY, 'May'),
    ]

    month = models.CharField(
        max_length=3,
        choices=MANTHS,
        default=APRIL # or "default=MANTHS[4]"
    )

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

    YEAR_IN_SCHOOL_CHOICES = [
        (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]"
    )

As you can see, the new way is much simpler than the old way.


V
VisioN

$ pip install django-better-choices

For those who are interested, I have created django-better-choices library, that provides a nice interface to work with Django choices for Python 3.7+. It supports custom parameters, lots of useful features and is very IDE friendly.

You can define your choices as a class:

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

Then do the following operations and much much more:

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

And of course, it is fully supported by Django and Django Migrations:

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

Full documentation here: https://pypi.org/project/django-better-choices/


Sorry but I still find this a useful duplication: "CREATED = 'Created'"