A custom Django choice field to use with Python enums.
- 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.
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
git clone https://github.com/HackSoftware/django-enum-choices.git
cd django_enum_choices
pip install -e .[dev]
Linting and running the tests:
tox