A custom Django choice field to use with Python enums.
Starting with version 3.0, Django started supporting Enumerations for model field choices and we recommend using this as a native Django feature, instead of django-enum-choices
If you are using django-enum-choices
and you want to upgrade your project to Django >= 3.0
you can refer to the guide in the wiki: Migrating to Django 3
- django-enum-choices
pip install django-enum-choices
from enum import Enum
from django.db import models
from django_enum_choices.fields import EnumChoiceField
class MyEnum(Enum):
A = 'a'
B = 'b'
class MyModel(models.Model):
enumerated_field = EnumChoiceField(MyEnum)
Model creation
instance = MyModel.objects.create(enumerated_field=MyEnum.A)
Changing enum values
instance.enumerated_field = MyEnum.B
instance.save()
Filtering
MyModel.objects.filter(enumerated_field=MyEnum.A)
EnumChoiceField
extends CharField
and generates choices internally. Each choice is generated using something, called a choice_builder
.
A choice builder function looks like that:
def choice_builder(enum: Enum) -> Tuple[str, str]:
# Some implementation
If a choice_builder
argument is passed to a model's EnumChoiceField
, django_enum_choices
will use it to generate the choices.
The choice_builder
must be a callable that accepts an enumeration choice and returns a tuple,
containing the value to be saved and the readable value.
By default django_enum_choices
uses one of the four choice builders defined in django_enum_choices.choice_builders
, named value_value
.
It returns a tuple containing the enumeration's value twice:
from django_enum_choices.choice_builders import value_value
class MyEnum(Enum):
A = 'a'
B = 'b'
print(value_value(MyEnum.A)) # ('a', 'a')
You can use one of the four default ones that fits your needs:
value_value
attribute_value
value_attribute
attribute_attribute
For example:
from django_enum_choices.choice_builders import attribute_value
class MyEnum(Enum):
A = 'a'
B = 'b'
class CustomReadableValueEnumModel(models.Model):
enumerated_field = EnumChoiceField(
MyEnum,
choice_builder=attribute_value
)
The resulting choices for enumerated_field
will be (('A', 'a'), ('B', 'b'))
You can also define your own choice builder:
class MyEnum(Enum):
A = 'a'
B = 'b'
def choice_builder(choice: Enum) -> Tuple[str, str]:
return choice.value, choice.value.upper() + choice.value
class CustomReadableValueEnumModel(models.Model):
enumerated_field = EnumChoiceField(
MyEnum,
choice_builder=choice_builder
)
Which will result in the following choices (('a', 'Aa'), ('b', 'Bb'))
The values in the returned from choice_builder
tuple will be cast to strings before being used.
At any given point of time all instances of a model that has EnumChoiceField
must have a value that is currently present in the enumeration.
When changing or removing an option from the enumeration, a custom database migration must be made prior to the enumeration change.
When chaging options we'll need several operations:
- Inserting a new option with the new value that we want
- Migrating all instances from the old option to the new one
- Removing the old option and renaming the old one
- Removing the custom data migration code, so migrations can be run on a clean database without an
AttributeError
ocurring
Example:
Initial setup:
class MyEnum(Enum):
A = 'a'
B = 'b'
# Desired change:
# A = 'a_updated'
class MyModel(models.Model):
enumerated_field = EnumChoiceField(MyEnum)
- Insert a new option with the desired new value:
class MyEnum:
A_UPDATED = 'a_updated'
A = 'a'
B = 'b'
python manage.py makemigrations
- Migrate model instances
python manage.py makemigrations app_label --empty
# migration_name.py
def forwards(apps, schema_editor):
MyModel = apps.get_model('app_label', 'MyModel')
MyModel.objects.filter(enumerated_field=MyEnum.A).update(enumerated_field=MyEnum.A_UPDATED)
class Migration(migrations.Migration):
...
operations = [
migrations.RunPython(forwards),
]
python manage.py migrate
- Remove old option and rename new one
class MyEnum:
A = 'a_updated'
B = 'b'
python manage.py makemigrations
python manage.py migrate
- Remove custom data migration code
# migration_name.py
def forwards(apps, schema_editor):
pass
class Migration(migrations.Migration):
...
operations = [
migrations.RunPython(forwards),
]
Removing options from the enumeration includes several operations as well:
- Optional: Making the field nullable (if we want our existing instances' values to be
None
) - Migrating all instances to a new option (or None)
- Removing the option from the enumeration
- Removing the custom data migration code, so migrations can be run on a clean database without an
AttributeError
ocurring
Example:
Initial setup:
class MyEnum(Enum):
A = 'a'
B = 'b'
# Desired change:
# class MyEnum(Enum):
# A = 'a'
class MyModel(models.Model):
enumerated_field = EnumChoiceField(MyEnum)
- Optional: Make the field nullable (if you want your existing instances to have a
None
value)
class MyModel(models.Model):
enumerated_field = EnumChoiceField(MyEnum, blank=True, null=True)
python manage.py makemigrations
- Migrate model instances
python manage.py makemigrations app_label --empty
# migration_name.py
def forwards(apps, schema_editor):
MyModel = apps.get_model('app_label', 'MyModel')
MyModel.objects.filter(enumerated_field=MyEnum.B).update(enumerated_field=MyEnum.A)
# OR MyModel.objects.filter(enumerated_field=MyEnum.B).update(enumerated_field=None)
class Migration(migrations.Migration):
...
operations = [
migrations.RunPython(forwards),
]
python manage.py migrate
- Remove old option
class MyEnum:
A = 'a
python manage.py makemigrations
python manage.py migrate
- Remove custom data migration code
# migration_name.py
def forwards(apps, schema_editor):
pass
class Migration(migrations.Migration):
...
operations = [
migrations.RunPython(forwards),
]
Model fields, defined as EnumChoiceField
can be used with almost all of the admin panel's
standard functionallities.
One exception from this their usage in list_filter
.
If you need an EnumChoiceField
inside a ModelAdmin
's list_filter
, you can use the following
options:
- Define the entry insite the list filter as a tuple, containing the field's name and
django_enum_choices.admin.EnumChoiceListFilter
from django.contrib import admin
from django_enum_choices.admin import EnumChoiceListFilter
from .models import MyModel
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
list_filter = [('enumerated_field', EnumChoiceListFilter)]
- Set
DJANGO_ENUM_CHOICES_REGISTER_LIST_FILTER
inside your settings toTrue
, which will automatically set theEnumChoiceListFilter
class to alllist_filter
fields that are instances ofEnumChoiceField
. This way, they can be declared directly in thelist_filter
iterable:
from django.contrib import admin
from .models import MyModel
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
list_filter = ('enumerated_field', )
There are 2 rules of thumb:
- If you use a
ModelForm
, everything will be taken care of automatically. - If you use a
Form
, you need to take into account whatEnum
andchoice_builder
you are using.
from .models import MyModel
class ModelEnumForm(forms.ModelForm):
class Meta:
model = MyModel
fields = ['enumerated_field']
form = ModelEnumForm({
'enumerated_field': 'a'
})
form.is_valid()
print(form.save(commit=True)) # <MyModel: MyModel object (12)>
If you are using the default value_value
choice builder, you can just do that:
from django_enum_choices.forms import EnumChoiceField
from .enumerations import MyEnum
class StandardEnumForm(forms.Form):
enumerated_field = EnumChoiceField(MyEnum)
form = StandardEnumForm({
'enumerated_field': 'a'
})
form.is_valid()
print(form.cleaned_data) # {'enumerated_field': <MyEnum.A: 'a'>}
If you are passing a different choice builder, you have to also pass it to the form field:
from .enumerations import MyEnum
def custom_choice_builder(choice):
return 'Custom_' + choice.value, choice.value
class CustomChoiceBuilderEnumForm(forms.Form):
enumerated_field = EnumChoiceField(
MyEnum,
choice_builder=custom_choice_builder
)
form = CustomChoiceBuilderEnumForm({
'enumerated_field': 'Custom_a'
})
form.is_valid()
print(form.cleaned_data) # {'enumerated_field': <MyEnum.A: 'a'>}
As with forms, there are 2 general rules of thumb:
- If you have declared an
EnumChoiceField
in theMeta.fields
for a givenMeta.model
, you need to inheritEnumChoiceFilterMixin
in your filter class & everything will be taken care of automatically. - If you are declaring an explicit field, without a model, you need to specify the
Enum
class & thechoice_builder
, if a custom one is used.
import django_filters as filters
from django_enum_choices.filters import EnumChoiceFilterMixin
class ImplicitFilterSet(EnumChoiceFilterSetMixin, filters.FilterSet):
class Meta:
model = MyModel
fields = ['enumerated_field']
filters = {
'enumerated_field': 'a'
}
filterset = ImplicitFilterSet(filters)
print(filterset.qs.values_list('enumerated_field', flat=True))
# <QuerySet [<MyEnum.A: 'a'>, <MyEnum.A: 'a'>, <MyEnum.A: 'a'>]>
The choice_builder
argument can be passed to django_enum_choices.filters.EnumChoiceFilter
as well when using the field explicitly. When using EnumChoiceFilterSetMixin
, the choice_builder
is determined from the model field, for the fields defined inside the Meta
inner class.
import django_filters as filters
from django_enum_choices.filters import EnumChoiceFilter
def custom_choice_builder(choice):
return 'Custom_' + choice.value, choice.value
class ExplicitCustomChoiceBuilderFilterSet(filters.FilterSet):
enumerated_field = EnumChoiceFilter(
MyEnum,
choice_builder=custom_choice_builder
)
filters = {
'enumerated_field': 'Custom_a'
}
filterset = ExplicitCustomChoiceBuilderFilterSet(filters, MyModel.objects.all())
print(filterset.qs.values_list('enumerated_field', flat=True)) # <QuerySet [<MyEnum.A: 'a'>, <MyEnum.A: 'a'>, <MyEnum.A: 'a'>]>
import django_filters as filters
from django_enum_choices.filters import EnumChoiceFilter
class ExplicitFilterSet(filters.FilterSet):
enumerated_field = EnumChoiceFilter(MyEnum)
filters = {
'enumerated_field': 'a'
}
filterset = ExplicitFilterSet(filters, MyModel.objects.all())
print(filterset.qs.values_list('enumerated_field', flat=True)) # <QuerySet [<MyEnum.A: 'a'>, <MyEnum.A: 'a'>, <MyEnum.A: 'a'>]>
You can use EnumChoiceField
as a child field of an Postgres ArrayField
.
from django.db import models
from django.contrib.postgres.fields import ArrayField
from django_enum_choices.fields import EnumChoiceField
from enum import Enum
class MyEnum(Enum):
A = 'a'
B = 'b'
class MyModelMultiple(models.Model):
enumerated_field = ArrayField(
base_field=EnumChoiceField(MyEnum)
)
Model Creation
instance = MyModelMultiple.objects.create(enumerated_field=[MyEnum.A, MyEnum.B])
Changing enum values
instance.enumerated_field = [MyEnum.B]
instance.save()
As with forms & filters, there are 2 general rules of thumb:
- If you are using a
ModelSerializer
and you inheritEnumChoiceModelSerializerMixin
, everything will be taken care of automatically. - If you are using a
Serializer
, you need to take theEnum
class &choice_builder
into acount.
from rest_framework import serializers
from django_enum_choices.serializers import EnumChoiceModelSerializerMixin
class ImplicitMyModelSerializer(
EnumChoiceModelSerializerMixin,
serializers.ModelSerializer
):
class Meta:
model = MyModel
fields = ('enumerated_field', )
By default ModelSerializer.build_standard_field
coerces any field that has a model field with choices to ChoiceField
which returns the value directly.
Since enum values resemble EnumClass.ENUM_INSTANCE
they won't be able to be encoded by the JSONEncoder
when being passed to a Response
.
That's why we need the mixin.
When using the EnumChoiceModelSerializerMixin
with DRF's serializers.ModelSerializer
, the choice_builder
is automatically passed from the model field to the serializer field.
from rest_framework import serializers
from django_enum_choices.serializers import EnumChoiceField
class MyModelSerializer(serializers.ModelSerializer):
enumerated_field = EnumChoiceField(MyEnum)
class Meta:
model = MyModel
fields = ('enumerated_field', )
# Serialization:
instance = MyModel.objects.create(enumerated_field=MyEnum.A)
serializer = MyModelSerializer(instance)
data = serializer.data # {'enumerated_field': 'a'}
# Saving:
serializer = MyModelSerializer(data={
'enumerated_field': 'a'
})
serializer.is_valid()
serializer.save()
If you are using a custom choice_builder
, you need to pass that too.
def custom_choice_builder(choice):
return 'Custom_' + choice.value, choice.value
class CustomChoiceBuilderSerializer(serializers.Serializer):
enumerted_field = EnumChoiceField(
MyEnum,
choice_builder=custom_choice_builder
)
serializer = CustomChoiceBuilderSerializer({
'enumerated_field': MyEnum.A
})
data = serializer.data # {'enumerated_field': 'Custom_a'}
from rest_framework import serializers
from django_enum_choices.serializers import EnumChoiceField
class MySerializer(serializers.Serializer):
enumerated_field = EnumChoiceField(MyEnum)
# Serialization:
serializer = MySerializer({
'enumerated_field': MyEnum.A
})
data = serializer.data # {'enumerated_field': 'a'}
# Deserialization:
serializer = MySerializer(data={
'enumerated_field': 'a'
})
serializer.is_valid()
data = serializer.validated_data # OrderedDict([('enumerated_field', <MyEnum.A: 'a'>)])
If you are using a custom choice_builder
, you need to pass that too.
django-enum-choices
exposes a MultipleEnumChoiceField
that can be used for serializing arrays of enumerations.
Using a subclass of serializers.Serializer
from rest_framework import serializers
from django_enum_choices.serializers import MultipleEnumChoiceField
class MultipleMySerializer(serializers.Serializer):
enumerated_field = MultipleEnumChoiceField(MyEnum)
# Serialization:
serializer = MultipleMySerializer({
'enumerated_field': [MyEnum.A, MyEnum.B]
})
data = serializer.data # {'enumerated_field': ['a', 'b']}
# Deserialization:
serializer = MultipleMySerializer(data={
'enumerated_field': ['a', 'b']
})
serializer.is_valid()
data = serializer.validated_data # OrderedDict([('enumerated_field', [<MyEnum.A: 'a'>, <MyEnum.B: 'b'>])])
Using a subclass of serializers.ModelSerializer
class ImplicitMultipleMyModelSerializer(
EnumChoiceModelSerializerMixin,
serializers.ModelSerializer
):
class Meta:
model = MyModelMultiple
fields = ('enumerated_field', )
# Serialization:
instance = MyModelMultiple.objects.create(enumerated_field=[MyEnum.A, MyEnum.B])
serializer = ImplicitMultipleMyModelSerializer(instance)
data = serializer.data # {'enumerated_field': ['a', 'b']}
# Saving:
serializer = ImplicitMultipleMyModelSerializer(data={
'enumerated_field': ['a', 'b']
})
serializer.is_valid()
serializer.save()
The EnumChoiceModelSerializerMixin
does not need to be used if enumerated_field
is defined on the serializer class explicitly.
EnumChoiceField
is a subclass ofCharField
.- Only subclasses of
Enum
are valid arguments forEnumChoiceField
. max_length
, if passed, is ignored.max_length
is automatically calculated from the longest choice.choices
are generated using a specialchoice_builder
function, which accepts an enumeration and returns a tuple of 2 items.- Four choice builder functions are defined inside
django_enum_choices.choice_builders
- By default the
value_value
choice builder is used. It produces the choices from the values in the enumeration class, like(enumeration.value, enumeration.value)
choice_builder
can be overriden by passing a callable to thechoice_builder
keyword argument ofEnumChoiceField
.- All values returned from the choice builder will be cast to strings when generating choices.
- Four choice builder functions are defined inside
For example, lets have the following case:
class Value:
def __init__(self, value):
self.value = value
def __str__(self):
return self.value
class CustomObjectEnum(Enum):
A = Value(1)
B = Value('B')
# The default choice builder `value_value` is being used
class SomeModel(models.Model):
enumerated_field = EnumChoiceField(CustomObjectEnum)
We'll have the following:
SomeModel.enumerated_field.choices == (('1', '1'), ('B', 'B'))
SomeModel.enumerated_field.max_length == 3
enum.auto
can be used for shorthand enumeration definitions:
from enum import Enum, auto
class AutoEnum(Enum):
A = auto() # 1
B = auto() # 2
class SomeModel(models.Model):
enumerated_field = EnumChoiceField(Enum)
This will result in the following:
SomeModel.enumerated_field.choices == (('1', '1'), ('2', '2'))
Overridinng auto
behaviour
Custom values for enumerations, created by auto
, can be defined by
subclassing an Enum
that defines _generate_next_value_
:
class CustomAutoEnumValueGenerator(Enum):
def _generate_next_value_(name, start, count, last_values):
return {
'A': 'foo',
'B': 'bar'
}[name]
class CustomAutoEnum(CustomAutoEnumValueGenerator):
A = auto()
B = auto()
The above will assign the values mapped in the dictionary as values to attributes in CustomAutoEnum
.
Prerequisites
- SQLite3
- PostgreSQL server
- Python >= 3.5 virtual environment
Fork the repository
git clone https://github.com/your-user-name/django-enum-choices.git django-enum-choices-yourname
cd django-enum-choices-yourname
git remote add upstream https://github.com/HackSoftware/django-enum-choices.git
Install the requirements:
pip install -e .[dev]
Linting and running the tests:
tox