Opened 2 years ago

Last modified 30 hours ago

#27910 assigned New feature

Allow using an Enum class in model Field choices

Reported by: Marcel Hellwig Owned by: Shai Berger
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 (15)

comment:1 Changed 2 years ago by Simon Charette

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

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

comment:2 Changed 2 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 2 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 2 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 2 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 22 months 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 22 months ago by Tom Forbes

Cc: Tom Forbes added

comment:8 Changed 22 months 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 4 months ago by Ryan Hiebert

Cc: Ryan Hiebert added

comment:10 Changed 5 weeks 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 5 weeks ago by Shai Berger

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

comment:12 Changed 5 weeks 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 5 weeks ago by felixxm

Has patch: set

comment:14 Changed 32 hours 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 30 hours 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.

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