A custom Django choice field to use with Python enums.
django.forms.ModelForm
django.forms.Form
django-filter
Meta
inner class and inheriting from EnumChoiceFilterMixin
FilterSet
serializers.ModelSerializer
with EnumChoiceModelSerializerMixin
serializers.ModelSerializer
without EnumChoiceModelSerializerMixin
serializers.Serializer
enum.auto
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:
AttributeError
ocurringExample:
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),
]
```bash
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
Removing options from the enumeration includes several operations as well:
1. Optional: Making the field nullable (if we want our existing instances' values to be `None`)
2. Migrating all instances to a new option (or None)
3. Removing the option from the enumeration
4. Removing the custom data migration code, so migrations can be run on a clean database without an `AttributeError` ocurring
Example:
Initial setup:
```python
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),
]
```bash
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),
]
## Usage in the admin panel
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`
```python
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)]
DJANGO_ENUM_CHOICES_REGISTER_LIST_FILTER
inside your settings to True
, which will automatically set the EnumChoiceListFilter
class to all
list_filter
fields that are instances of EnumChoiceField
. This way, they can be declared directly in the list_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:
ModelForm
, everything will be taken care of automatically.Form
, you need to take into account what Enum
and choice_builder
you are using.django.forms.ModelForm
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)>
django.forms.Form
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'>}
django-filter
As with forms, there are 2 general rules of thumb:
EnumChoiceField
in the Meta.fields
for a given Meta.model
, you need to inherit EnumChoiceFilterMixin
in your filter class & everything will be taken care of automatically.Enum
class & the choice_builder
, if a custom one is used.Meta
inner class and inheriting from EnumChoiceFilterMixin
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'>]>
FilterSet
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:
ModelSerializer
and you inherit EnumChoiceModelSerializerMixin
, everything will be taken care of automatically.Serializer
, you need to take the Enum
class & choice_builder
into acount.serializers.ModelSerializer
with EnumChoiceModelSerializerMixin
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.
serializers.ModelSerializer
without EnumChoiceModelSerializerMixin
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'}
serializers.Serializer
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 of CharField
.Enum
are valid arguments for EnumChoiceField
.max_length
, if passed, is ignored. max_length
is automatically calculated from the longest choice.choices
are generated using a special choice_builder
function, which accepts an enumeration and returns a tuple of 2 items.
django_enum_choices.choice_builders
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 the choice_builder
keyword argument of EnumChoiceField
.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
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
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