Opened 3 years ago

Closed 3 months ago

Last modified 7 weeks ago

#27910 closed New feature (fixed)

Allow using an Enum class in model Field choices

Reported by: Marcel Hellwig Owned by: Nick Pope
Component: Database layer (models, ORM) Version: master
Severity: Normal Keywords: enum model choices
Cc: Tom Forbes, Ryan Hiebert Triage Stage: Accepted
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

I will simply stick to your example here: https://docs.djangoproject.com/en/dev/ref/models/fields/#choices

I want to limit the input to certain choices so I create a list/tuple of tuples/lists. Since Python 3.4 there is a class called Enum and some nice decorators like @unique.

Currently, if you want to use this you have to do 1/2 dirty hacks, descriped in this article, which I would prefer over the current solution.

It would be nice to have native support for the Enum class, e.g. you can directly pass the class to choices (instead of using the class method choices() ), also when refering to an element in the enum just using Student.Freshmann instead of Student.Freshmann.value

A simple example would be this:

from enum import Enum

class Student(models.Model):
    class YearInSchoolChoices(Enum):
        Freshman = 'FR'
        Sophomore = 'SO'
        Junior = 'JR'
        Senior = 'SR'
    
    year_in_school = models.CharField(
        max_length=2,
        choices=YearInSchoolChoices,
        default=YearInSchoolChoices.Freshman,
    )

    def is_upperclass(self):
        return self.year_in_school in (self.YearInSchoolChoices.Junior, self.YearInSchoolChoices.Senior)

Also this could be adopted (if accepted) to any type of choices in django, e.g. Choicefield etc.

Change History (25)

comment:1 Changed 3 years ago by Simon Charette

Do you have a suggestion about how translated choices labels should be defined?

Last edited 3 years ago by Simon Charette (previous) (diff)

comment:2 Changed 3 years ago by Marcel Hellwig

To be honest, I haven't though about this, because I don't translate them. Hmm.. Good question. Implicit translation?! :/ Decorator? That are not so good ideas.
Or you could swap the positions, e.g. FR = _("Freshman") , because you'll never translate the key which is stored in the DB, right?!

comment:3 Changed 3 years ago by Simon Charette

Or you could swap the positions.

That would work for text based fields (CharField, TextField) but what about choices defined for a integer based field?

I feel like choices are a different concept than enums and that while the later is useful for referring to choices values as constants, choices and label mapping should be defined independently.

class StudentType(enum.Enum):
    Freshman = 'FR'
    Sophomore = 'SO'
    Junior = 'JR'
    Senior = 'SR'

class Student(models.Model):
    type = models.CharField(choices=[
        (StudentType.Freshman, _('Freshman')),
        (StudentType.Sophomore, _('Sophomore')),
        (StudentType.Junior, _('Junior')),
        (StudentType.Senior, _('Senior')),
    ])

comment:4 Changed 3 years ago by Marcel Hellwig

Hmm.. your solution won't bring any advantages over the current one, so.. I don't like that concept. I think the first one was okay, but we will have Problems when you want to use translation or any non-variable-names. So.. should be discard this idea (although I really do like it!)

comment:5 Changed 3 years ago by Tim Graham

Resolution: wontfix
Status: newclosed
Summary: Use Enum class in model choicesAllow using an Enum class in model Field choices

I agree with Simon's analysis. An automatic translation based on the enum value doesn't seem like a proper separate of concerns.

comment:6 Changed 2 years ago by Tom Forbes

I know this is a closed ticket, but a pattern I've has success with is a custom enum subclass that combines the values with the display:

class StudentType(EnumChoices):
    Freshman = 'FR', _('Freshman')
    Sophomore = 'SO', _('Sophomore')
    Junior = 'JR', _('Junior')
    Senior = 'SR', _('Senior')

It's somewhat simple to do, the EnumChoices class splits the initial enum values into value, display, and exposes the display text as an attribute: StudentType.Freshman.display (and in the str method). list(StudentType) returns a list of tuples that is suitable for choices.

Could this not be an OK compromise? In the applications I've seen with large numbers of choices managing the display values gets to be a burden, the values should indeed be separate from the display and not auto-generated but there is a tangible benefit in keeping their definitions as close as possible together. Splitting the definition of an enum out into two parts in the django-way seems to violate DRY, no?

comment:7 Changed 2 years ago by Tom Forbes

Cc: Tom Forbes added

comment:8 Changed 2 years ago by Marcel Hellwig

I like the solution Tim brought up. Maybe making the second item on the enum optional, so that you could still write

    Freshman = 'FR',

and for default just use an untranslated name of the field (e.g. Freshman).

How do you like it?

comment:9 Changed 11 months ago by Ryan Hiebert

Cc: Ryan Hiebert added

comment:10 Changed 8 months ago by Shai Berger

Resolution: wontfix
Status: closednew

Reopening, following the discussion on the developers list and oral discussions with technical-team members on the DC-EU sprints.

comment:11 Changed 8 months ago by Shai Berger

Owner: changed from nobody to Shai Berger
Status: newassigned
Version: 1.10master

comment:12 Changed 8 months ago by Carlton Gibson

Triage Stage: UnreviewedAccepted

Given the extended discussion on django-developers to re-open this ticket I'll move it to "accepted": (group) review will decide finally if we're going to merge. (But if I read the thread right, the consensus seemed to be in favour...)

comment:13 Changed 8 months ago by felixxm

Has patch: set

comment:14 Changed 7 months ago by Luke Plant

This ticket really ought to consider the use of database level enums for the field. (e.g. https://www.postgresql.org/docs/9.1/datatype-enum.html ). They provide a lot of efficiency (storage and searching), while allowing the DB to contain human readable values (from the point of view of anyone querying it), and also having tight data integrity - you get the best of everything. MySQL also has support, but it looks a bit different.

django_enum implements this, but it requires a significant number of patches to Django machinery, which django_enum does by monkey patching. In particular, you need to track changes to the enum in order to generate 'CREATE TYPE' and 'ALTER TYPE' (for Postgres at least). It would be great to have a solution in Django core so that we could avoid that kind of monkey patching.

We might want to consider this as a separate ticket, but it would be a shame to solve the problem half-way. At the very least we should ensure that the solution we adopt would be future compatible with using DB-level enums.

There may well be some good ideas from Mike Bayer's post about enums that we can make use of. His DeclEnum base class is very similar in interface to EnumChoices, which is a good sign at least!

comment:15 Changed 7 months ago by Simon Charette

This ticket really ought to consider the use of database level enums for the field. (e.g. ​https://www.postgresql.org/docs/9.1/datatype-enum.html ). They provide a lot of efficiency (storage and searching), while allowing the DB to contain human readable values (from the point of view of anyone querying it), and also having tight data integrity - you get the best of everything. MySQL also has support, but it looks a bit different.

Just wanted to drop a note to mention that PostgreSQL's enum type doesn't support value removal (as of 11) and that MySQL stores enum as tinyint index of the values so if you perform an ordering change you pretty much corrupt your column. The index based value on MySQL poses notable problems when joining similar values across tables or ordering by such a column. I believe these two reasons make uses database level enums in a database agnostic way really challenging.

comment:16 Changed 5 months ago by felixxm

Needs documentation: set

comment:17 Changed 5 months ago by Nick Pope

Owner: changed from Shai Berger to Nick Pope

New PR

comment:18 Changed 4 months ago by Nick Pope

Needs documentation: unset

comment:19 Changed 3 months ago by Mariusz Felisiak <felisiak.mariusz@…>

Resolution: fixed
Status: assignedclosed

In 72ebe85:

Fixed #27910 -- Added enumeration helpers for use in Field.choices.

These classes can serve as a base class for user enums, supporting
translatable human-readable names, or names automatically inferred
from the enum member name.

Additional properties make it easy to access the list of names, values
and display labels.

Thanks to the following for ideas and reviews:

Carlton Gibson, Fran Hrženjak, Ian Foote, Mariusz Felisiak, Shai Berger.

Co-authored-by: Shai Berger <shai@…>
Co-authored-by: Nick Pope <nick.pope@…>
Co-authored-by: Mariusz Felisiak <felisiak.mariusz@…>

comment:20 Changed 3 months ago by Mariusz Felisiak <felisiak.mariusz@…>

In 1c66767d:

Refs #27910 -- Improved documentation for model field choice enumeration types.

comment:21 Changed 3 months ago by Mariusz Felisiak <felisiak.mariusz@…>

In 4d72c14b:

[3.0.x] Refs #27910 -- Improved documentation for model field choice enumeration types.

Backport of 1c66767d4e472fea27798812817b8a47c6ae22b3 from master

comment:22 Changed 2 months ago by Mariusz Felisiak <felisiak.mariusz@…>

In aad46ee2:

Refs #27910 -- Doc'd support for enumeration types serialization in migrations.

comment:23 Changed 2 months ago by Mariusz Felisiak <felisiak.mariusz@…>

In fcaaff9:

[3.0.x] Refs #27910 -- Doc'd support for enumeration types serialization in migrations.

Backport of aad46ee274b0e294ac055cc199e6595de4ef4164 from master

comment:24 Changed 7 weeks ago by GitHub <noreply@…>

In 1877ec1:

Refs #27910 -- Added init.py file for model_enums tests.

comment:25 Changed 7 weeks ago by Mariusz Felisiak <felisiak.mariusz@…>

In 74c5ddc6:

[3.0.x] Refs #27910 -- Added init.py file for model_enums tests.

Backport of 1877ec18753947795b7821f8b9176bd7ea7c03a3 from master

Note: See TracTickets for help on using tickets.
Back to Top