50

I have a data migration that updates some permissions. I know there are some known issues with permissions in migrations and i was able to avoid some trouble by creating the permissions in the migration it self (rather then using the tuple shortcut in the model).

The migration :

from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings

def create_feature_groups(apps, schema_editor):
    app = models.get_app('myauth')

    Group = apps.get_model("auth", "Group")
    pro = Group.objects.create(name='pro')

    Permission = apps.get_model("auth", "Permission")
    ContentType = apps.get_model("contenttypes", "ContentType")
    invitation_contenttype = ContentType.objects.get(name='Invitation')

    send_invitation = Permission.objects.create(
         codename='send_invitation',
         name='Can send Invitation',
         content_type=invitation_contenttype)

    pro.permissions.add(receive_invitation)    

class Migration(migrations.Migration):

    dependencies = [
        ('myauth', '0002_initial_data'),
    ]

    operations = [
            migrations.RunPython(create_feature_groups),
    ]

After some trial and error i was able to make this work using manage.py migrate but i'm getting errors in the test manage.py test.

__fake__.DoesNotExist: ContentType matching query does not exist.

Debugging a bit discovered that there are no ContentType at this point in the migration when run in test (not sure why). Following the advice in this post i tried updating the content types manually in the migration it self. Added :

from django.contrib.contenttypes.management import update_contenttypes
update_contenttypes(app, models.get_models())

before fetching the content type for the Invitation model. Got the following error

  File "C:\Python27\lib\site-packages\django-1.7-py2.7.egg\django\contrib\contenttypes\management.py", line 14, in update_contenttypes
    if not app_config.models_module:
AttributeError: 'module' object has no attribute 'models_module'

There must be some way to create/update permissions in data migrations in a testable way.

Thanks.

EDIT

Finally made it work by adding

from django.contrib.contenttypes.management import update_all_contenttypes
update_all_contenttypes() 

oddly enough this one was not sufficient

update_contenttypes(apps.app_configs['contenttypes'])

I would love to know why all of this is necessary

maazza
  • 7,016
  • 15
  • 63
  • 96
haki
  • 9,389
  • 15
  • 62
  • 110
  • 1
    For those on Django 1.8 who want update_all_contenttypes, refer to this question: http://stackoverflow.com/questions/29550102/importerror-cannot-import-name-update-all-contenttypes – jstaab Sep 09 '15 at 23:31
  • 1
    I swear out of the 3 years I've been working with Django I've never hated it as much as when having to deal with this (OK, I'm lying, there's been other rough patches in our relationship in the past as well). Anyway, these Q/As are getting bookmarked as the holy grail, thanks a lot! – Sebastián Vansteenkiste Jul 31 '20 at 15:21
  • 1
    Stay strong Sebastián – haki Aug 01 '20 at 16:49

8 Answers8

18

The answer is:

apps.get_model('contenttypes', 'ContentType') 

:) Needed it myself today.

gabn88
  • 781
  • 8
  • 23
16

For Django 2.1 I had to import apps from global registry because passed apps into migration were instances of django.db.migrations.state.AppConfigStub without populated models_module attribute. And create_contenttypes is checking this attribute.

from django.apps.registry import Apps, apps as global_apps
from django.contrib.contenttypes.management import create_contenttypes
from django.db import migrations


def add_permision(apps: Apps, schema_editor):
    my_app_config = global_apps.get_app_config('my_app')
    create_contenttypes(my_app_config)

    ...
Petr Přikryl
  • 1,641
  • 4
  • 22
  • 34
  • 2
    Note that this is necessary if you run data migrations that need the content types to be present, as e.g. in the test database, the content type table is initially empty. To actually get the content types in the data migration, you can then use the answer from gabn88. – Andreas Profous Apr 21 '21 at 13:19
  • This worked well for me! In addition to tests, my use case was new users trying to migrate the project from scratch. – rbennell Jan 05 '22 at 10:47
  • 1
    This works in Django 4.x as well, thanks! – berto Feb 08 '22 at 19:49
8

Since, I ended up spending 3-4 hours on this I am adding my solution.

The problem was ContentType and Permission objects were not getting created when I ran multiple migrations together. Since I was referencing these content type and migration in next migration, this was causing problem.)

However they work fine if I run them one by one using migration number. (which were referenced in future migrations)

To solve it I added a extra migration in between to create ContentType and Permission objects.

# -*- coding: utf-8 -*-
# Generated by Django 1.10.6 on 2017-03-11 05:59
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations


def update_all_contenttypes(**kwargs):
    from django.apps import apps
    from django.contrib.contenttypes.management import update_contenttypes

    for app_config in apps.get_app_configs():
        update_contenttypes(app_config, **kwargs)


def create_all_permissions(**kwargs):
    from django.contrib.auth.management import create_permissions
    from django.apps import apps

    for app_config in apps.get_app_configs():
        create_permissions(app_config, **kwargs)


def forward(apps, schema_editor):
    update_all_contenttypes()
    create_all_permissions()


def backward(apps, schema_editor):
    pass


class Migration(migrations.Migration):
    dependencies = [
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
        ('contenttypes', '0002_remove_content_type_name'),
        ('MY_APP', '0123_LAST_MIGRATION'),
    ]

    operations = [
        migrations.RunPython(forward, backward)
    ]
Christian Long
  • 10,385
  • 6
  • 60
  • 58
mithuntnt
  • 507
  • 1
  • 5
  • 17
  • 6
    Using Django 1.11 update_contenttypes no longer exists, use create_contenttypes instead – oden May 15 '17 at 03:25
7

Having a similar issue when writing a data migration that spans several apps. Turns out Django only loads those models into the app registry that are affected by what the "dependencies" member of the migration states: https://code.djangoproject.com/ticket/24303

Had to basically add an entry to the migration dependencies that I use that is not directly related by e.g. a ForeignKey to the app that is currently being migrated.

Sakuraba
  • 2,631
  • 1
  • 19
  • 13
  • I don't understand. How did you determine which (unrelated) migrations needed to be added as dependency for the models to be available ? – haki Feb 15 '15 at 11:46
  • Basically I added all the apps whose models I wanted to use as part of the data migration. – Sakuraba Mar 13 '15 at 09:30
  • Besides running update_all_contenttypes (thanks for the tip), you should include ('contenttypes', '0001_initial') as migration dependency. – slamora May 11 '15 at 09:09
1
update_contenttypes(apps.app_configs['contenttypes'])

will update the contenttypes app's content types.

I believe you would want to do this...

update_contenttypes(apps.app_configs['app_label']) 

where app_label is the app label for the app where the Invitation model lives.This will update your single app's content types so it will be available to query as per your original code.

simondo92
  • 36
  • 6
1

I faced the same issue today (Django 3.2.12). ContentTypes were present while running the migrate command, however, they were missing on test, though. I managed to solve it by calling ContentType.objects.get_for_model (which is an alternative solution to those given above). E.g.:

ContentType = apps.get_model("contenttypes", "ContentType")
YourModel = apps.get_model("app_name", "model_name")
content_type = ContentType.objects.get_for_model(YourModel)

It works because get_for_model creates a ContentType if it doesn't exist. See the Django implementation below:

    def get_for_model(self, model, for_concrete_model=True):
        """
        Return the ContentType object for a given model, creating the
        ContentType if necessary. Lookups are cached so that subsequent lookups
        for the same model don't hit the database.
        """
        opts = self._get_opts(model, for_concrete_model)
        try:
            return self._get_from_cache(opts)
        except KeyError:
            pass

        # The ContentType entry was not found in the cache, therefore we
        # proceed to load or create it.
        try:
            # Start with get() and not get_or_create() in order to use
            # the db_for_read (see #20401).
            ct = self.get(app_label=opts.app_label, model=opts.model_name)
        except self.model.DoesNotExist:
            # Not found in the database; we proceed to create it. This time
            # use get_or_create to take care of any race conditions.
            ct, created = self.get_or_create(
                app_label=opts.app_label,
                model=opts.model_name,
            )
        self._add_to_cache(self.db, ct)
        return ct
pbajsarowicz
  • 542
  • 6
  • 12
0
from django.db import migrations
from django.db.migrations import RunPython
from django.apps.registry import Apps, apps as global_apps
from django.contrib.contenttypes.management import create_contenttypes


def add_content_type_records(apps: Apps, schema_editor):
    my_app_config = global_apps.get_app_config('my_1_app')
    my_app_config.models_module = True
    create_contenttypes(my_app_config)
    my_app_config.models_module = None
    my_app_config = global_apps.get_app_config('my_2_app')
    my_app_config.models_module = True
    create_contenttypes(my_app_config)
    my_app_config.models_module = None


def create_setup_data(apps, schema_editor):
    ...


def delete_setup_data(apps, schema_editor):
    ...


class Migration(migrations.Migration):

    dependencies = [
        ('my_1_app', '....'),
        ('my_2_app', '....'),
        ('contenttypes', '__latest__'),
    ]

    operations = [
        RunPython(add_content_type_records, RunPython.noop),
        RunPython(create_setup_data, delete_setup_data),
    ]
lescijus
  • 76
  • 6
0

I'm unwilling to depend on an undocumented private interface like create_contenttypes, so I chose a different solution.

Context: I have a migration that adds a field and then populates the field. The field is a GenericForeignKey, so I need access to ContentType data to populate it. That data is absent in the test database, which is created automatically during the execution of the test suite.

Therefore, resting on the assumption of "test DB = empty", I implemented the following check at the top of my forward function, which is passed to RunPython:

def forward(apps, schema_editor):
    MyModel = apps.get_model("myapp", "MyModel")
    if MyModel.objects.count() == 0:
        return
    # code that depends on ContentType here...

It still runs properly in a regular context, and no longer fails in a test context.

PS - The Django Project really ought to implement a proper solution to this problem in the core.

odigity
  • 7,568
  • 4
  • 37
  • 51