-
Notifications
You must be signed in to change notification settings - Fork 9
Migrating to Django 3
Django 3 has introduced it's own enumeration types, which means there is no need to use djang-enum-choices
if you're starting your project or have already upgraded your old ones to Django 3:
https://docs.djangoproject.com/en/3.1/ref/models/fields/#enumeration-types
Let's take this example setup with using django-enum-choices
:
# models.py
from enum import Enum
from datetime import date
from django.db import models
from django_enum_choices.fields import EnumChoiceField
class StringEnum(Enum):
FIRST_OPTION = 'First Option'
SECOND_OPTION = 'Second Option'
class IntEnum(Enum):
FIRST_OPTION = 1
SECOND_OPTION = 2
class DateEnum(Enum):
START = date(2020, 1, 1)
END = date(2021, 1, 1)
class CustomModel(models.Model):
string_enum = EnumChoiceField(
StringEnum,
blank=True,
null=True
)
int_enum = EnumChoiceField(
IntEnum,
blank=True,
null=True
)
date_enum = EnumChoiceField(
DateEnum,
blank=True,
null=True
)
When using Django 3 we will want to migrate each EnumChoiceField
to it's corresponding django.db.models
field type while preserving the data we currently have:
string_enum -> CharField
int_enum -> IntegerField
date_enum -> DateField
First, we need to define our new enumeration types in enums.py
.
However, we need to preserve our old ones because we still need them in order to transfer our existing data into the new model fields.
# models.py
from enum import Enum
from datetime import date
from django.db import models
from django_enum_choices.fields import EnumChoiceField
class StringEnum(Enum):
FIRST_OPTION = 'First Option'
SECOND_OPTION = 'Second Option'
class IntEnum(Enum):
FIRST_OPTION = 1
SECOND_OPTION = 2
class DateEnum(Enum):
START = date(2020, 1, 1)
END = date(2021, 1, 1)
+class NewStringEnum(models.TextChoices):
+ FIRST_OPTION = 'First Option'
+ SECOND_OPTION = 'Second Option'
+
+
+class NewIntEnum(models.IntegerChoices):
+ FIRST_OPTION = 1
+ SECOND_OPTION = 2
+
+
+class NewDateEnum(date, models.Choices):
+ START = 2020, 1, 1, 'Start'
+ END = 2021, 1, 1, 'End'
+
class CustomModel(models.Model):
string_enum = EnumChoiceField(
StringEnum,
blank=True,
null=True
)
int_enum = EnumChoiceField(
IntEnum,
blank=True,
null=True
)
date_enum = EnumChoiceField(
DateEnum,
blank=True,
null=True
)
+
+ new_string_enum = models.CharField(
+ max_length=13,
+ choices=NewStringEnum.choices,
+ blank=True,
+ null=True
+ )
+
+ new_int_enum = models.IntegerField(
+ choices=NewIntEnum.choices,
+ blank=True,
+ null=True
+ )
+
+ new_date_enum = models.DateField(
+ choices=NewDateEnum.choices,
+ blank=True,
+ null=True
+ )
Run python manage.py makemigrations
and python manage.py migrate
to apply the changes to the models.
Now we'll need to create an empty migration, which we'll use for running our custom migration code. This process is thoroughly explained by Django's documentation on data migraions, but you can also follow the steps below:
Run python manage.py makemigrations enum_playground --empty
. Replace enum_playground
with the name of your app.
Note: If you have multiple apps with models, which contain EnumChoiceField
, you can either create multiple migrations or load all the models in a single migration.
Now open the newly created migration and create an operation for executing the data transfer:
# Generated by Django 3.1.6 on 2021-02-04 10:57
from django.db import migrations
from enum import Enum
def update_enum_fields(apps, schema_editor):
try:
# Ignore this migration in case the enums are removed or their names are changed
from enum_playground.enums import NewStringEnum, NewIntEnum, NewDateEnum
except ImportError:
return
CustomModel = apps.get_model('enum_playground', 'CustomModel')
fields = {
'string_enum': NewStringEnum,
'int_enum': NewIntEnum,
'date_enum': NewDateEnum
}
to_update = []
for custom_model_instance in CustomModel.objects.all():
for old_field_name, new_enum_class in fields.items():
new_field_name = f'new_{old_field_name}'
field_value = getattr(custom_model_instance, old_field_name)
# Extra check in case the value may be none or some blank value
if isinstance(field_value, Enum):
# Extract the value from the new enum class based on the old enum option name
field_value = getattr(
new_enum_class,
field_value.name
)
setattr(custom_model_instance, new_field_name, field_value)
to_update.append(custom_model_instance)
CustomModel.objects.bulk_update(
to_update,
[f'new_{old_field_name}' for old_field_name in fields.keys()]
)
class Migration(migrations.Migration):
dependencies = [
('enum_playground', '0002_auto_20210204_1038'),
]
operations = [
migrations.RunPython(update_enum_fields)
]
Note: You might need more complex data transformations depending on the old and new enumeration types that you have defined. Make sure to do a dry run of your migration code using a single model instance.
Run python manage.py migrate
to execute the data migration. Doing this NOW is important, because we're going to change the names of the enumeration classes later on and this migration won't be able to find them after we do that.
# models.py
-from enum import Enum
from datetime import date
from django.db import models
-from django_enum_choices.fields import EnumChoiceField
-
-class StringEnum(Enum):
- FIRST_OPTION = 'First Option'
- SECOND_OPTION = 'Second Option'
-
-
-class IntEnum(Enum):
- FIRST_OPTION = 1
- SECOND_OPTION = 2
-
-
-class DateEnum(Enum):
- START = date(2020, 1, 1)
- END = date(2021, 1, 1)
-
-
-class NewStringEnum(models.TextChoices):
+class StringEnum(models.TextChoices):
FIRST_OPTION = 'First Option'
SECOND_OPTION = 'Second Option'
-class NewIntEnum(models.IntegerChoices):
+class IntEnum(models.IntegerChoices):
FIRST_OPTION = 1
SECOND_OPTION = 2
class DateEnum(date, models.Choices):
START = 2020, 1, 1, 'Start'
END = 2021, 1, 1, 'End'
class CustomModel(models.Model):
- string_enum = EnumChoiceField(
- StringEnum,
- blank=True,
- null=True
- )
-
- int_enum = EnumChoiceField(
- IntEnum,
- blank=True,
- null=True
- )
-
- date_enum = EnumChoiceField(
- DateEnum,
- blank=True,
- null=True
- )
-
new_string_enum = models.CharField(
max_length=13,
- choices=NewStringEnum.choices,
+ choices=StringEnum.choices,
blank=True,
null=True
)
new_int_enum = models.IntegerField(
- choices=NewIntEnum.choices,
+ choices=IntEnum.choices,
blank=True,
null=True
)
new_date_enum = models.DateField(
- choices=NewDateEnum.choices,
+ choices=DateEnum.choices,
blank=True,
null=True
)
Run python manage.py makemigrations
. This will generate RemoveField
operations towards the old fields.
Now we can remove the "new" prefix from our model fields.
After doing that, run python manage.py makemigrations
and python manage.py migrate
to apply the changes
In order to remove the library completely from your dependencies, you'll need to remove it's usage from the migrations. Unless you specifacally need to look at your migration history, you can do that by removing all your existing migrations, running makemigrations
to create clean ones. You'll need to "fake" the newly created migrations by running python manage.py migrate --fake
.
Note: If you have migrations which create database views, triggers or extensions, you will want to keep them, because makemigrations
won't automatically create them. They might be needed if someone is just starting development on your project and is setting it up from scratch.
Our models have been updated and our database state has been adapted to Django's new enumeration types.
Let's move on to the other parts where EnumChoiceField
can be used.
You probably won't need to change anything here unless you're using the list_filter
functionality with EnumChoiceField
fields.
If that's the case, depending on the approach you've taken from the docs (https://github.com/HackSoftware/django-enum-choices#usage-in-the-admin-panel) you'll need to do the following:
- If you've defined your
list_filter
fields with theEnumChoiceListFilter
class, simply remove it and take the field name out of the tuple:
@admin.register(MyModel)
class MyModelAdmin(admin.ModelAdmin):
- list_filter = [(
- 'enumerated_field',
- EnumChoiceListFilter
- )]
+ list_filter = ('enumerated_field', )
- If you've taken the second approach using the
DJANGO_ENUM_CHOICES_REGISTER_LIST_FILTER
, just remove the variable from your settings.
-
forms.ModelForm
s don't require any changes -
forms.Form
s need theirEnumChoiceFields
changed toforms.ChoiceField
:
class CustomForm(forms.Form):
- string_enum = EnumChoiceField(StringEnum)
- int_enum = EnumChoiceField(IntEnum)
- date_enum = EnumChoiceField(DateEnum)
+ string_enum = forms.ChoiceField(choices=StringEnum.choices)
+ int_enum = forms.ChoiceField(choices=IntEnum.choices)
+ date_enum = forms.ChoiceField(choices=DateEnum.choices)
- For
serializers.Seralizer
allEnumChoiceFields
need to be replaced withserializers.ChoiceField
s:
class InputSerializer(serializers.Serializer):
- string_enum = EnumChoiceField(StringEnum)
- int_enum = EnumChoiceField(IntEnum)
- date_enum = EnumChoiceField(DateEnum)
+ string_enum = serializers.ChoiceField(choices=StringEnum.choices)
+ int_enum = serializers.ChoiceField(choices=IntEnum.choices)
+ date_enum = serializers.ChoiceField(choices=DateEnum.choices)
- For
serializer.ModelSerializer
theEnumChoiceModelSerializerMixin
needs to be removed:
-class InputSerializer(EnumChoiceModelSerializerMixin, serializers.ModelSerializer):
+class InputSerializer(serializers.ModelSerializer):
class Meta:
model = CustomModel
fields = ('string_enum', 'int_enum', 'date_enum')