Setting permissions in a datamigration does not work.

The code below will not apply any migration to the selected group:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
def apply_migration(apps, schema_editor):
    Group = apps.get_model("auth", "Group")
    admin_permissions = Permission.objects.filter(codename__in=ADMIN_PERMISSION).values_list("id", flat=True)
    admin_group = Group.objects.get(name=MANAGEMENT_GROUP)
    admin_group.permissions.add(*admin_permissions)

class Migration(migrations.Migration):

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

    operations = [migrations.RunPython(apply_migration, migrations.RunPython.noop)]

This is really annoying as, despite my years of experience with Django, I keep forgetting this, and redescovering it after a lot of head desks

But why does this happens?

ContentType table (and permissions which relies on content types to work) is only created after the first pass of apply migrations have been run; if you try to fetch permissions objects from database before that, you’ll get nothing: Permission.objects.filter() will always be empty.

This is especially tricky if you create the datamigration after you have already applied the migrations: in this case the datamigration will work just fine, as ContentType and Permission objects will be there in the database.

But as soon as a colleague of yours setup the project starting from an empty database … 💥 … no default permissions.

For more details about this behavior, check this ticket.

How to solve this?

There are two options: creating a django command, or creating the permission in the data migration itseld

Using a django command

Just move the logic from the data migration function to a django command:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
from django.contrib.auth.models import Group, Permission
from django.core.management.base import BaseCommand

from .my_app.constant import ADMIN_PERMISSION, MANAGEMENT_GROUP


class Command(BaseCommand):
    help = "Set default permissions."  # noqa: A003

    def handle(self, *args, **options):
        admin_permissions = Permission.objects.filter(codename__in=ADMIN_PERMISSION).values_list("id", flat=True)
        admin_group = Group.objects.get(name=MANAGEMENT_GROUP)
        admin_group.permissions.add(*admin_permissions)

This is easy, but it’s something you have to run yourself and it will not be automated by Django.

Creating the permissions in the django migration

While the permissions are not there, you can still create them in your own datamigration: this will not conflict with the automatic creation by Django, as the ones create by your datamigration will be detected and they will be left intact.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
def apply_migration(apps, schema_editor):
    for app_config in apps.get_app_configs():
        app_config.models_module = True
        create_permissions(app_config, verbosity=0)

    Group = apps.get_model("auth", "Group")
    admin_permissions = Permission.objects.filter(codename__in=ADMIN_PERMISSION).values_list("id", flat=True)
    admin_group = Group.objects.get(name=MANAGEMENT_GROUP)
    admin_group.permissions.add(*admin_permissions)

class Migration(migrations.Migration):

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

    operations = [migrations.RunPython(apply_migration, migrations.RunPython.noop)]

The added lines will ensure that all permissions are created by looping over existing applications and creating the applications.

The best part of this approach is that you can create a test to ensure permission are assigned when the migrations are applied.