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-choicesfrom 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 implementationIf 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_valueattribute_valuevalue_attributeattribute_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
AttributeErrorocurring
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
AttributeErrorocurring
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
Nonevalue)
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 = 'apython 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_FILTERinside your settings toTrue, which will automatically set theEnumChoiceListFilterclass to alllist_filterfields that are instances ofEnumChoiceField. This way, they can be declared directly in thelist_filteriterable:
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 whatEnumandchoice_builderyou 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
EnumChoiceFieldin theMeta.fieldsfor a givenMeta.model, you need to inheritEnumChoiceFilterMixinin 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
Enumclass & 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
ModelSerializerand you inheritEnumChoiceModelSerializerMixin, everything will be taken care of automatically. - If you are using a
Serializer, you need to take theEnumclass &choice_builderinto 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.
EnumChoiceFieldis a subclass ofCharField.- Only subclasses of
Enumare valid arguments forEnumChoiceField. max_length, if passed, is ignored.max_lengthis automatically calculated from the longest choice.choicesare generated using a specialchoice_builderfunction, 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_valuechoice builder is used. It produces the choices from the values in the enumeration class, like(enumeration.value, enumeration.value) choice_buildercan be overriden by passing a callable to thechoice_builderkeyword 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.gitInstall the requirements:
pip install -e .[dev]Linting and running the tests:
tox