mirror of https://github.com/django/django.git
Fixed #34388 -- Allowed using choice enumeration types directly on model and form fields.
This commit is contained in:
parent
051d5944f8
commit
a2eaea8f22
|
@ -14,6 +14,7 @@ from django.conf import settings
|
||||||
from django.core import checks, exceptions, validators
|
from django.core import checks, exceptions, validators
|
||||||
from django.db import connection, connections, router
|
from django.db import connection, connections, router
|
||||||
from django.db.models.constants import LOOKUP_SEP
|
from django.db.models.constants import LOOKUP_SEP
|
||||||
|
from django.db.models.enums import ChoicesMeta
|
||||||
from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin
|
from django.db.models.query_utils import DeferredAttribute, RegisterLookupMixin
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.datastructures import DictWrapper
|
from django.utils.datastructures import DictWrapper
|
||||||
|
@ -216,6 +217,8 @@ class Field(RegisterLookupMixin):
|
||||||
self.unique_for_date = unique_for_date
|
self.unique_for_date = unique_for_date
|
||||||
self.unique_for_month = unique_for_month
|
self.unique_for_month = unique_for_month
|
||||||
self.unique_for_year = unique_for_year
|
self.unique_for_year = unique_for_year
|
||||||
|
if isinstance(choices, ChoicesMeta):
|
||||||
|
choices = choices.choices
|
||||||
if isinstance(choices, collections.abc.Iterator):
|
if isinstance(choices, collections.abc.Iterator):
|
||||||
choices = list(choices)
|
choices = list(choices)
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
|
|
|
@ -16,6 +16,7 @@ from urllib.parse import urlsplit, urlunsplit
|
||||||
|
|
||||||
from django.core import validators
|
from django.core import validators
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
|
from django.db.models.enums import ChoicesMeta
|
||||||
from django.forms.boundfield import BoundField
|
from django.forms.boundfield import BoundField
|
||||||
from django.forms.utils import from_current_timezone, to_current_timezone
|
from django.forms.utils import from_current_timezone, to_current_timezone
|
||||||
from django.forms.widgets import (
|
from django.forms.widgets import (
|
||||||
|
@ -857,6 +858,8 @@ class ChoiceField(Field):
|
||||||
|
|
||||||
def __init__(self, *, choices=(), **kwargs):
|
def __init__(self, *, choices=(), **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
if isinstance(choices, ChoicesMeta):
|
||||||
|
choices = choices.choices
|
||||||
self.choices = choices
|
self.choices = choices
|
||||||
|
|
||||||
def __deepcopy__(self, memo):
|
def __deepcopy__(self, memo):
|
||||||
|
|
|
@ -431,7 +431,7 @@ For each field, we describe the default widget used if you don't specify
|
||||||
.. attribute:: choices
|
.. attribute:: choices
|
||||||
|
|
||||||
Either an :term:`iterable` of 2-tuples to use as choices for this
|
Either an :term:`iterable` of 2-tuples to use as choices for this
|
||||||
field, :ref:`enumeration <field-choices-enum-types>` choices, or a
|
field, :ref:`enumeration type <field-choices-enum-types>`, or a
|
||||||
callable that returns such an iterable. This argument accepts the same
|
callable that returns such an iterable. This argument accepts the same
|
||||||
formats as the ``choices`` argument to a model field. See the
|
formats as the ``choices`` argument to a model field. See the
|
||||||
:ref:`model field reference documentation on choices <field-choices>`
|
:ref:`model field reference documentation on choices <field-choices>`
|
||||||
|
@ -439,6 +439,11 @@ For each field, we describe the default widget used if you don't specify
|
||||||
time the field's form is initialized, in addition to during rendering.
|
time the field's form is initialized, in addition to during rendering.
|
||||||
Defaults to an empty list.
|
Defaults to an empty list.
|
||||||
|
|
||||||
|
.. versionchanged:: 5.0
|
||||||
|
|
||||||
|
Support for using :ref:`enumeration types <field-choices-enum-types>`
|
||||||
|
directly in the ``choices`` was added.
|
||||||
|
|
||||||
``DateField``
|
``DateField``
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|
|
@ -210,7 +210,7 @@ choices in a concise way::
|
||||||
|
|
||||||
year_in_school = models.CharField(
|
year_in_school = models.CharField(
|
||||||
max_length=2,
|
max_length=2,
|
||||||
choices=YearInSchool.choices,
|
choices=YearInSchool,
|
||||||
default=YearInSchool.FRESHMAN,
|
default=YearInSchool.FRESHMAN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -235,8 +235,7 @@ modifications:
|
||||||
* A ``.label`` property is added on values, to return the human-readable name.
|
* A ``.label`` property is added on values, to return the human-readable name.
|
||||||
* A number of custom properties are added to the enumeration classes --
|
* A number of custom properties are added to the enumeration classes --
|
||||||
``.choices``, ``.labels``, ``.values``, and ``.names`` -- to make it easier
|
``.choices``, ``.labels``, ``.values``, and ``.names`` -- to make it easier
|
||||||
to access lists of those separate parts of the enumeration. Use ``.choices``
|
to access lists of those separate parts of the enumeration.
|
||||||
as a suitable value to pass to :attr:`~Field.choices` in a field definition.
|
|
||||||
|
|
||||||
.. warning::
|
.. warning::
|
||||||
|
|
||||||
|
@ -276,7 +275,7 @@ Django provides an ``IntegerChoices`` class. For example::
|
||||||
HEART = 3
|
HEART = 3
|
||||||
CLUB = 4
|
CLUB = 4
|
||||||
|
|
||||||
suit = models.IntegerField(choices=Suit.choices)
|
suit = models.IntegerField(choices=Suit)
|
||||||
|
|
||||||
It is also possible to make use of the `Enum Functional API
|
It is also possible to make use of the `Enum Functional API
|
||||||
<https://docs.python.org/3/library/enum.html#functional-api>`_ with the caveat
|
<https://docs.python.org/3/library/enum.html#functional-api>`_ with the caveat
|
||||||
|
@ -320,6 +319,10 @@ There are some additional caveats to be aware of:
|
||||||
|
|
||||||
__empty__ = _("(Unknown)")
|
__empty__ = _("(Unknown)")
|
||||||
|
|
||||||
|
.. versionchanged:: 5.0
|
||||||
|
|
||||||
|
Support for using enumeration types directly in the ``choices`` was added.
|
||||||
|
|
||||||
``db_column``
|
``db_column``
|
||||||
-------------
|
-------------
|
||||||
|
|
||||||
|
|
|
@ -167,7 +167,9 @@ File Uploads
|
||||||
Forms
|
Forms
|
||||||
~~~~~
|
~~~~~
|
||||||
|
|
||||||
* ...
|
* :attr:`.ChoiceField.choices` now accepts
|
||||||
|
:ref:`Choices classes <field-choices-enum-types>` directly instead of
|
||||||
|
requiring expansion with the ``choices`` attribute.
|
||||||
|
|
||||||
Generic Views
|
Generic Views
|
||||||
~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
@ -208,6 +210,10 @@ Models
|
||||||
of ``ValidationError`` raised during
|
of ``ValidationError`` raised during
|
||||||
:ref:`model validation <validating-objects>`.
|
:ref:`model validation <validating-objects>`.
|
||||||
|
|
||||||
|
* :attr:`.Field.choices` now accepts
|
||||||
|
:ref:`Choices classes <field-choices-enum-types>` directly instead of
|
||||||
|
requiring expansion with the ``choices`` attribute.
|
||||||
|
|
||||||
Requests and Responses
|
Requests and Responses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -211,7 +211,7 @@ ones:
|
||||||
class Runner(models.Model):
|
class Runner(models.Model):
|
||||||
MedalType = models.TextChoices("MedalType", "GOLD SILVER BRONZE")
|
MedalType = models.TextChoices("MedalType", "GOLD SILVER BRONZE")
|
||||||
name = models.CharField(max_length=60)
|
name = models.CharField(max_length=60)
|
||||||
medal = models.CharField(blank=True, choices=MedalType.choices, max_length=10)
|
medal = models.CharField(blank=True, choices=MedalType, max_length=10)
|
||||||
|
|
||||||
Further examples are available in the :ref:`model field reference
|
Further examples are available in the :ref:`model field reference
|
||||||
<field-choices>`.
|
<field-choices>`.
|
||||||
|
|
|
@ -95,7 +95,8 @@ class ChoiceFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
|
||||||
JOHN = "J", "John"
|
JOHN = "J", "John"
|
||||||
PAUL = "P", "Paul"
|
PAUL = "P", "Paul"
|
||||||
|
|
||||||
f = ChoiceField(choices=FirstNames.choices)
|
f = ChoiceField(choices=FirstNames)
|
||||||
|
self.assertEqual(f.choices, FirstNames.choices)
|
||||||
self.assertEqual(f.clean("J"), "J")
|
self.assertEqual(f.clean("J"), "J")
|
||||||
msg = "'Select a valid choice. 3 is not one of the available choices.'"
|
msg = "'Select a valid choice. 3 is not one of the available choices.'"
|
||||||
with self.assertRaisesMessage(ValidationError, msg):
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
|
|
|
@ -433,24 +433,20 @@ class WriterTests(SimpleTestCase):
|
||||||
DateChoices.DATE_1,
|
DateChoices.DATE_1,
|
||||||
("datetime.date(1969, 7, 20)", {"import datetime"}),
|
("datetime.date(1969, 7, 20)", {"import datetime"}),
|
||||||
)
|
)
|
||||||
field = models.CharField(default=TextChoices.B, choices=TextChoices.choices)
|
field = models.CharField(default=TextChoices.B, choices=TextChoices)
|
||||||
string = MigrationWriter.serialize(field)[0]
|
string = MigrationWriter.serialize(field)[0]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
string,
|
string,
|
||||||
"models.CharField(choices=[('A', 'A value'), ('B', 'B value')], "
|
"models.CharField(choices=[('A', 'A value'), ('B', 'B value')], "
|
||||||
"default='B')",
|
"default='B')",
|
||||||
)
|
)
|
||||||
field = models.IntegerField(
|
field = models.IntegerField(default=IntegerChoices.B, choices=IntegerChoices)
|
||||||
default=IntegerChoices.B, choices=IntegerChoices.choices
|
|
||||||
)
|
|
||||||
string = MigrationWriter.serialize(field)[0]
|
string = MigrationWriter.serialize(field)[0]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
string,
|
string,
|
||||||
"models.IntegerField(choices=[(1, 'One'), (2, 'Two')], default=2)",
|
"models.IntegerField(choices=[(1, 'One'), (2, 'Two')], default=2)",
|
||||||
)
|
)
|
||||||
field = models.DateField(
|
field = models.DateField(default=DateChoices.DATE_2, choices=DateChoices)
|
||||||
default=DateChoices.DATE_2, choices=DateChoices.choices
|
|
||||||
)
|
|
||||||
string = MigrationWriter.serialize(field)[0]
|
string = MigrationWriter.serialize(field)[0]
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
string,
|
string,
|
||||||
|
|
|
@ -69,11 +69,18 @@ class WhizIterEmpty(models.Model):
|
||||||
|
|
||||||
|
|
||||||
class Choiceful(models.Model):
|
class Choiceful(models.Model):
|
||||||
|
class Suit(models.IntegerChoices):
|
||||||
|
DIAMOND = 1, "Diamond"
|
||||||
|
SPADE = 2, "Spade"
|
||||||
|
HEART = 3, "Heart"
|
||||||
|
CLUB = 4, "Club"
|
||||||
|
|
||||||
no_choices = models.IntegerField(null=True)
|
no_choices = models.IntegerField(null=True)
|
||||||
empty_choices = models.IntegerField(choices=(), null=True)
|
empty_choices = models.IntegerField(choices=(), null=True)
|
||||||
with_choices = models.IntegerField(choices=[(1, "A")], null=True)
|
with_choices = models.IntegerField(choices=[(1, "A")], null=True)
|
||||||
empty_choices_bool = models.BooleanField(choices=())
|
empty_choices_bool = models.BooleanField(choices=())
|
||||||
empty_choices_text = models.TextField(choices=())
|
empty_choices_text = models.TextField(choices=())
|
||||||
|
choices_from_enum = models.IntegerField(choices=Suit)
|
||||||
|
|
||||||
|
|
||||||
class BigD(models.Model):
|
class BigD(models.Model):
|
||||||
|
|
|
@ -75,11 +75,11 @@ class ValidationTests(SimpleTestCase):
|
||||||
f.clean("not a", None)
|
f.clean("not a", None)
|
||||||
|
|
||||||
def test_enum_choices_cleans_valid_string(self):
|
def test_enum_choices_cleans_valid_string(self):
|
||||||
f = models.CharField(choices=self.Choices.choices, max_length=1)
|
f = models.CharField(choices=self.Choices, max_length=1)
|
||||||
self.assertEqual(f.clean("c", None), "c")
|
self.assertEqual(f.clean("c", None), "c")
|
||||||
|
|
||||||
def test_enum_choices_invalid_input(self):
|
def test_enum_choices_invalid_input(self):
|
||||||
f = models.CharField(choices=self.Choices.choices, max_length=1)
|
f = models.CharField(choices=self.Choices, max_length=1)
|
||||||
msg = "Value 'a' is not a valid choice."
|
msg = "Value 'a' is not a valid choice."
|
||||||
with self.assertRaisesMessage(ValidationError, msg):
|
with self.assertRaisesMessage(ValidationError, msg):
|
||||||
f.clean("a", None)
|
f.clean("a", None)
|
||||||
|
|
|
@ -301,11 +301,11 @@ class ValidationTests(SimpleTestCase):
|
||||||
f.clean("0", None)
|
f.clean("0", None)
|
||||||
|
|
||||||
def test_enum_choices_cleans_valid_string(self):
|
def test_enum_choices_cleans_valid_string(self):
|
||||||
f = models.IntegerField(choices=self.Choices.choices)
|
f = models.IntegerField(choices=self.Choices)
|
||||||
self.assertEqual(f.clean("1", None), 1)
|
self.assertEqual(f.clean("1", None), 1)
|
||||||
|
|
||||||
def test_enum_choices_invalid_input(self):
|
def test_enum_choices_invalid_input(self):
|
||||||
f = models.IntegerField(choices=self.Choices.choices)
|
f = models.IntegerField(choices=self.Choices)
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
f.clean("A", None)
|
f.clean("A", None)
|
||||||
with self.assertRaises(ValidationError):
|
with self.assertRaises(ValidationError):
|
||||||
|
|
|
@ -156,6 +156,7 @@ class ChoicesTests(SimpleTestCase):
|
||||||
cls.empty_choices_bool = Choiceful._meta.get_field("empty_choices_bool")
|
cls.empty_choices_bool = Choiceful._meta.get_field("empty_choices_bool")
|
||||||
cls.empty_choices_text = Choiceful._meta.get_field("empty_choices_text")
|
cls.empty_choices_text = Choiceful._meta.get_field("empty_choices_text")
|
||||||
cls.with_choices = Choiceful._meta.get_field("with_choices")
|
cls.with_choices = Choiceful._meta.get_field("with_choices")
|
||||||
|
cls.choices_from_enum = Choiceful._meta.get_field("choices_from_enum")
|
||||||
|
|
||||||
def test_choices(self):
|
def test_choices(self):
|
||||||
self.assertIsNone(self.no_choices.choices)
|
self.assertIsNone(self.no_choices.choices)
|
||||||
|
@ -192,6 +193,10 @@ class ChoicesTests(SimpleTestCase):
|
||||||
with self.subTest(field=field):
|
with self.subTest(field=field):
|
||||||
self.assertIsInstance(field.formfield(), forms.ChoiceField)
|
self.assertIsInstance(field.formfield(), forms.ChoiceField)
|
||||||
|
|
||||||
|
def test_choices_from_enum(self):
|
||||||
|
# Choices class was transparently resolved when given as argument.
|
||||||
|
self.assertEqual(self.choices_from_enum.choices, Choiceful.Suit.choices)
|
||||||
|
|
||||||
|
|
||||||
class GetFieldDisplayTests(SimpleTestCase):
|
class GetFieldDisplayTests(SimpleTestCase):
|
||||||
def test_choices_and_field_display(self):
|
def test_choices_and_field_display(self):
|
||||||
|
|
Loading…
Reference in New Issue