Reputation: 9779
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
Upvotes: 51
Views: 15170
Reputation: 622
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
Upvotes: 6
Reputation: 8216
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.
Upvotes: 0
Reputation: 76
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),
]
Upvotes: 0
Reputation: 1822
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)
...
Upvotes: 20
Reputation: 811
The answer is:
apps.get_model('contenttypes', 'ContentType')
:) Needed it myself today.
Upvotes: 18
Reputation: 517
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)
]
Upvotes: 8
Reputation: 36
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.
Upvotes: 1
Reputation: 2641
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.
Upvotes: 7