Skip to content
This repository has been archived by the owner on Jan 29, 2023. It is now read-only.

Migrating to Django 3

Vasil Slavov edited this page Feb 9, 2021 · 14 revisions

Migrating to Django 3's Enumeration types

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

Migrating your model fields

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

Step 1: Defining our new enumeration types and our new model fields

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.

Step 2: Migrating the data from the old fields to the new

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.

Step 3: Cleaning up the state of models.py

 # 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.

The admin panel

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 the EnumChoiceListFilter 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

  • forms.ModelForms don't require any changes
  • forms.Forms need their EnumChoiceFields changed to forms.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)

Serializers

  • For serializers.Seralizer all EnumChoiceFields need to be replaced with serializers.ChoiceFields:
 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 the EnumChoiceModelSerializerMixin needs to be removed:
-class InputSerializer(EnumChoiceModelSerializerMixin, serializers.ModelSerializer):
+class InputSerializer(serializers.ModelSerializer):
     class Meta:
         model = CustomModel
         fields = ('string_enum', 'int_enum', 'date_enum')