Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django 1.7 datamigration and user groups

I'm trying to implement a datamigration using django 1.7 native migration system. Here is what I've done.

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import migrations


def create_basic_user_group(apps, schema_editor):
    """Forward data migration that create the basic_user group

    """
    Group = apps.get_model('auth', 'Group')
    Permission = apps.get_model('auth', 'Permission')
    group = Group(name='basic_user')
    group.save()

    perm_codenames = (
        'add_stuff',
        '...',
    )

    # we prefere looping over all these in order to be sure to fetch them all
    perms = [Permission.objects.get(codename=codename)
             for codename in perm_codenames]

    group.permissions.add(*perms)
    group.save()


def remove_basic_user_group(apps, schema_editor):
    """Backward data migration that remove the basic_user group

    """
    group = Group.objects.get(name='basic_user')
    group.delete()


class Migration(migrations.Migration):
    """This migrations automatically create the basic_user group.

    """

    dependencies = [
    ]

    operations = [
        migrations.RunPython(create_basic_user_group, remove_basic_user_group),
    ]

But when I try to run the migration, I got a LookupError exception telling me that no app with label 'auth' could be found.

How can I create my groups in a clean way that could also be used in unit tests ?

like image 500
Nicolas Appriou Avatar asked Oct 01 '14 06:10

Nicolas Appriou


3 Answers

I've done what you are trying to do. The problems are:

  1. The documentation for 1.7 and 1.8 is quite clear: If you want to access a model from another app, you must list this app as a dependency:

    When writing a RunPython function that uses models from apps other than the one in which the migration is located, the migration’s dependencies attribute should include the latest migration of each app that is involved, otherwise you may get an error similar to: LookupError: No installed app with label 'myappname' when you try to retrieve the model in the RunPython function using apps.get_model().

    So you should have a dependency on the latest migration in auth.

  2. As you mentioned in a comment you will run into an issue whereby the permissions you want to use are not created yet. The problem is that the permissions are created by signal handler attached to the post_migrate signal. So the permissions associated with any new model created in a migration are not available until the migration is finished.

    You can fix this by doing this at the start of create_basic_user_group:

    from django.contrib.contenttypes.management import update_contenttypes
    from django.apps import apps as configured_apps
    from django.contrib.auth.management import create_permissions
    
    for app in configured_apps.get_app_configs():
        update_contenttypes(app, interactive=True, verbosity=0)
    
    for app in configured_apps.get_app_configs():
        create_permissions(app, verbosity=0)
    

    This will also create the content types for each model (which are also created after the migration), see below as to why you should care about that.

    Perhaps you could be more selective than I am in the code above: update just some key apps rather than update all apps. I've not tried to be selective. Also, it is possible that both loop could be merged into one. I've not tried it with a single loop.

  3. You get your Permission objects by searching by codename but codename is not guaranteed to be unique. Two apps can have models called Stuff and so you could have an add_stuff permission associated with two different apps. If this happens, your code will fail. What you should do is search by codename and content_type, which are guaranteed to be unique together. A unique content_type is associated with each model in the project: two models with the same name but in different apps will get two different content types.

    This means adding a dependency on the contenttypes app, and using the ContentType model: ContentType = apps.get_model("contenttypes", "ContentType").

like image 107
Louis Avatar answered Nov 14 '22 18:11

Louis


As said in https://code.djangoproject.com/ticket/23422, the signal post_migrate should be sent before dealing with Permission objects.

But there is a helper function already on Django to sent the needed signal: django.core.management.sql.emit_post_migrate_signal

Here, it worked this way:

# -*- coding: utf-8 -*-
from __future__ import unicode_literals

from django.db import models, migrations
from django.core.management.sql import emit_post_migrate_signal


PERMISSIONS_TO_ADD = [
    'view_my_stuff',
    ...
]


def create_group(apps, schema_editor):
    # Workarounds a Django bug: https://code.djangoproject.com/ticket/23422
    db_alias = schema_editor.connection.alias
    try:
        emit_post_migrate_signal(2, False, 'default', db_alias)
    except TypeError:  # Django < 1.8
        emit_post_migrate_signal([], 2, False, 'default', db_alias)

    Group = apps.get_model('auth', 'Group')
    Permission = apps.get_model('auth', 'Permission')

    group, created = Group.objects.get_or_create(name='MyGroup')
    permissions = [Permission.objects.get(codename=i) for i in PERMISSIONS_TO_ADD]
    group.permissions.add(*permissions)


class Migration(migrations.Migration):

    dependencies = [
        ('auth', '0001_initial'),
        ('myapp', '0002_mymigration'),
    ]

    operations = [
        migrations.RunPython(create_group),
    ]
like image 40
alanjds Avatar answered Nov 14 '22 19:11

alanjds


So, I figure out how to solve this problem and I get the following exit: get_model will only fetch Your model apps. I don't have sure about if this would be a good pratice, but it worked for me.

I just invoked the model Directly and made the changes.

# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.contrib.auth.models import Group


def create_groups(apps, schema_editor):
    g = Group(name='My New Group')
    g.save()


class Migration(migrations.Migration):

    operations = [
        migrations.RunPython(create_groups)
    ]

And then, just apply a /manage.py migrate to finish. I hope it helps.

like image 1
Joepreludian Avatar answered Nov 14 '22 19:11

Joepreludian