Fixed #34388 -- Allowed using choice enumeration types directly on model and form fields.

This commit is contained in:
T. Franzel 2023-03-11 00:34:13 +01:00 committed by Mariusz Felisiak
parent 051d5944f8
commit a2eaea8f22
12 changed files with 48 additions and 19 deletions

View File

@ -14,6 +14,7 @@ from django.conf import settings
from django.core import checks, exceptions, validators
from django.db import connection, connections, router
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.utils import timezone
from django.utils.datastructures import DictWrapper
@ -216,6 +217,8 @@ class Field(RegisterLookupMixin):
self.unique_for_date = unique_for_date
self.unique_for_month = unique_for_month
self.unique_for_year = unique_for_year
if isinstance(choices, ChoicesMeta):
choices = choices.choices
if isinstance(choices, collections.abc.Iterator):
choices = list(choices)
self.choices = choices

View File

@ -16,6 +16,7 @@ from urllib.parse import urlsplit, urlunsplit
from django.core import validators
from django.core.exceptions import ValidationError
from django.db.models.enums import ChoicesMeta
from django.forms.boundfield import BoundField
from django.forms.utils import from_current_timezone, to_current_timezone
from django.forms.widgets import (
@ -857,6 +858,8 @@ class ChoiceField(Field):
def __init__(self, *, choices=(), **kwargs):
super().__init__(**kwargs)
if isinstance(choices, ChoicesMeta):
choices = choices.choices
self.choices = choices
def __deepcopy__(self, memo):

View File

@ -431,7 +431,7 @@ For each field, we describe the default widget used if you don't specify
.. attribute:: choices
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
formats as the ``choices`` argument to a model field. See the
: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.
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``
-------------

View File

@ -210,7 +210,7 @@ choices in a concise way::
year_in_school = models.CharField(
max_length=2,
choices=YearInSchool.choices,
choices=YearInSchool,
default=YearInSchool.FRESHMAN,
)
@ -235,8 +235,7 @@ modifications:
* A ``.label`` property is added on values, to return the human-readable name.
* A number of custom properties are added to the enumeration classes --
``.choices``, ``.labels``, ``.values``, and ``.names`` -- to make it easier
to access lists of those separate parts of the enumeration. Use ``.choices``
as a suitable value to pass to :attr:`~Field.choices` in a field definition.
to access lists of those separate parts of the enumeration.
.. warning::
@ -276,7 +275,7 @@ Django provides an ``IntegerChoices`` class. For example::
HEART = 3
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
<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)")
.. versionchanged:: 5.0
Support for using enumeration types directly in the ``choices`` was added.
``db_column``
-------------

View File

@ -167,7 +167,9 @@ File Uploads
Forms
~~~~~
* ...
* :attr:`.ChoiceField.choices` now accepts
:ref:`Choices classes <field-choices-enum-types>` directly instead of
requiring expansion with the ``choices`` attribute.
Generic Views
~~~~~~~~~~~~~
@ -208,6 +210,10 @@ Models
of ``ValidationError`` raised during
: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
~~~~~~~~~~~~~~~~~~~~~~

View File

@ -211,7 +211,7 @@ ones:
class Runner(models.Model):
MedalType = models.TextChoices("MedalType", "GOLD SILVER BRONZE")
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
<field-choices>`.

View File

@ -95,7 +95,8 @@ class ChoiceFieldTest(FormFieldAssertionsMixin, SimpleTestCase):
JOHN = "J", "John"
PAUL = "P", "Paul"
f = ChoiceField(choices=FirstNames.choices)
f = ChoiceField(choices=FirstNames)
self.assertEqual(f.choices, FirstNames.choices)
self.assertEqual(f.clean("J"), "J")
msg = "'Select a valid choice. 3 is not one of the available choices.'"
with self.assertRaisesMessage(ValidationError, msg):

View File

@ -433,24 +433,20 @@ class WriterTests(SimpleTestCase):
DateChoices.DATE_1,
("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]
self.assertEqual(
string,
"models.CharField(choices=[('A', 'A value'), ('B', 'B value')], "
"default='B')",
)
field = models.IntegerField(
default=IntegerChoices.B, choices=IntegerChoices.choices
)
field = models.IntegerField(default=IntegerChoices.B, choices=IntegerChoices)
string = MigrationWriter.serialize(field)[0]
self.assertEqual(
string,
"models.IntegerField(choices=[(1, 'One'), (2, 'Two')], default=2)",
)
field = models.DateField(
default=DateChoices.DATE_2, choices=DateChoices.choices
)
field = models.DateField(default=DateChoices.DATE_2, choices=DateChoices)
string = MigrationWriter.serialize(field)[0]
self.assertEqual(
string,

View File

@ -69,11 +69,18 @@ class WhizIterEmpty(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)
empty_choices = models.IntegerField(choices=(), null=True)
with_choices = models.IntegerField(choices=[(1, "A")], null=True)
empty_choices_bool = models.BooleanField(choices=())
empty_choices_text = models.TextField(choices=())
choices_from_enum = models.IntegerField(choices=Suit)
class BigD(models.Model):

View File

@ -75,11 +75,11 @@ class ValidationTests(SimpleTestCase):
f.clean("not a", None)
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")
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."
with self.assertRaisesMessage(ValidationError, msg):
f.clean("a", None)

View File

@ -301,11 +301,11 @@ class ValidationTests(SimpleTestCase):
f.clean("0", None)
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)
def test_enum_choices_invalid_input(self):
f = models.IntegerField(choices=self.Choices.choices)
f = models.IntegerField(choices=self.Choices)
with self.assertRaises(ValidationError):
f.clean("A", None)
with self.assertRaises(ValidationError):

View File

@ -156,6 +156,7 @@ class ChoicesTests(SimpleTestCase):
cls.empty_choices_bool = Choiceful._meta.get_field("empty_choices_bool")
cls.empty_choices_text = Choiceful._meta.get_field("empty_choices_text")
cls.with_choices = Choiceful._meta.get_field("with_choices")
cls.choices_from_enum = Choiceful._meta.get_field("choices_from_enum")
def test_choices(self):
self.assertIsNone(self.no_choices.choices)
@ -192,6 +193,10 @@ class ChoicesTests(SimpleTestCase):
with self.subTest(field=field):
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):
def test_choices_and_field_display(self):