How To: Use Improved User in Data Migrations

Creating users in data migrations is discouraged as doing so represents a potential security risk, as passwords are stored in plaintext in the migration. However, doing so in proof-of-concepts or in special cases may be necessary, and the steps below will demonstrate how to create and remove new users in a Django data migration.

The django-improved-user package intentionally disallows use of UserManager in data migrations (we forgo the use of model managers in migrations). The create_user() and create_superuser() methods are thus both unavailable when using data migrations. Both of these methods rely on User model methods which are unavailable in Historical models, so we could not use them even if we wanted to (short of refactoring large parts of code currently inherited by Django).

We therefore rely on the standard Manager, and supplement the password creation behavior.

In an existing Django project, you will start by creating a new and empty migration file. Replace APP_NAME in the command below with the name of the app for which you wish to create a migration.

$ python manage.py makemigrations --empty --name=add_user APP_NAME

We start by importing the necessary tools

from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.db import migrations

We will use RunPython to run our code. RunPython expects two functions with specific parameters. Our first function creates a new user.

def add_user(apps, schema_editor):
    User = apps.get_model(*settings.AUTH_USER_MODEL.split("."))
    User.objects.create(
        email="migrated@jambonsw.com",
        password=make_password("s3cr3tp4ssw0rd!"),
        short_name="Migrated",
        full_name="Migrated Improved User",
    )

NB: Due to the lack of UserManager or User methods, the email field is not validated or normalized. What’s more, the password field is not validated against the project’s password validators. It is up to the developer coding the migration file to provide proper values.

The second function is technically optional, but providing one makes our lives easier and is considered best-practice. This function undoes the first, and deletes the user we created.

def remove_user(apps, schema_editor):
    User = apps.get_model(*settings.AUTH_USER_MODEL.split("."))
    User.objects.get(email="migrated@jambonsw.com").delete()

Finally, we use our migration functions via RunPython in a django.db.migrations.Migration subclass. Please note the addition of the dependency below. If your file already had a dependency, please add the tuple below, but do not remove the existing tuple(s).

class Migration(migrations.Migration):

    dependencies = [
        ("improved_user", "0001_initial"),
    ]

    operations = [
        migrations.RunPython(add_user, remove_user),
    ]

The final migration file is printed in totality below.

 1from django.conf import settings
 2from django.contrib.auth.hashers import make_password
 3from django.db import migrations
 4
 5
 6def add_user(apps, schema_editor):
 7    User = apps.get_model(*settings.AUTH_USER_MODEL.split("."))
 8    User.objects.create(
 9        email="migrated@jambonsw.com",
10        password=make_password("s3cr3tp4ssw0rd!"),
11        short_name="Migrated",
12        full_name="Migrated Improved User",
13    )
14
15
16def remove_user(apps, schema_editor):
17    User = apps.get_model(*settings.AUTH_USER_MODEL.split("."))
18    User.objects.get(email="migrated@jambonsw.com").delete()
19
20
21class Migration(migrations.Migration):
22
23    dependencies = [
24        ("improved_user", "0001_initial"),
25    ]
26
27    operations = [
28        migrations.RunPython(add_user, remove_user),
29    ]

You may wish to read more about Django Data Migrations and RunPython.