Opened 7 years ago

Closed 5 years ago

Last modified 4 years 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: dev
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 by Simon Charette, 7 years ago

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

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

comment:2 by Marcel Hellwig, 7 years ago

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 by Simon Charette, 7 years ago

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 by Marcel Hellwig, 7 years ago

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 by Tim Graham, 7 years ago

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 by Tom Forbes, 7 years ago

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 by Tom Forbes, 7 years ago

Cc: Tom Forbes added

comment:8 by Marcel Hellwig, 7 years ago

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 by Ryan Hiebert, 5 years ago

Cc: Ryan Hiebert added

comment:10 by Shai Berger, 5 years ago

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 by Shai Berger, 5 years ago

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

comment:12 by Carlton Gibson, 5 years ago

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 by Mariusz Felisiak, 5 years ago

Has patch: set

comment:14 by Luke Plant, 5 years ago

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 by Simon Charette, 5 years ago

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 by Mariusz Felisiak, 5 years ago

Needs documentation: set

comment:17 by Nick Pope, 5 years ago

Owner: changed from Shai Berger to Nick Pope

New PR

comment:18 by Nick Pope, 5 years ago

Needs documentation: unset

comment:19 by Mariusz Felisiak <felisiak.mariusz@…>, 5 years ago

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 by Mariusz Felisiak <felisiak.mariusz@…>, 5 years ago

In 1c66767d:

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

comment:21 by Mariusz Felisiak <felisiak.mariusz@…>, 5 years ago

In 4d72c14b:

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

Backport of 1c66767d4e472fea27798812817b8a47c6ae22b3 from master

comment:22 by Mariusz Felisiak <felisiak.mariusz@…>, 4 years ago

In aad46ee2:

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

comment:23 by Mariusz Felisiak <felisiak.mariusz@…>, 4 years ago

In fcaaff9:

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

Backport of aad46ee274b0e294ac055cc199e6595de4ef4164 from master

comment:24 by GitHub <noreply@…>, 4 years ago

In 1877ec1:

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

comment:25 by Mariusz Felisiak <felisiak.mariusz@…>, 4 years ago

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