Something really annoying is happening to me since using Django migrations (not south) and using loaddata for fixtures inside of them.
Here is a simple way to reproduce my problem:
create a new model Testmodel with 1 field field1 (CharField or whatever)
create an associated migration (let's say 0001) with makemigrations
run the migration
and add some data in the new table
dump the data in a fixture testmodel.json
create a migration with call_command('loaddata', 'testmodel.json'): migration 0002
add some a new field to the model: field2
create an associated migration (0003)
Now, commit that, and put your db in the state just before the changes: ./manage.py migrate myapp zero. So you are in the same state as your teammate that didn't get your changes yet.
If you try to run ./manage.py migrate again you will get a ProgrammingError at migration 0002 saying that "column field2 does not exist".
It seems it's because loaddata is looking into your model (which is already having field2), and not just applying the fixture to the db.
This can happen in multiple cases when working in a team, and also making the test runner fail.
Did I get something wrong? Is it a bug? What should be done is those cases?
--
I am using django 1.7
loaddata command will simply call serializers. Serializers will work on models state from your models.py file, not from current migration, but there is little trick to fool default serializer.
First, you don't want to use that serializer by call_command but rather directly:
from django.core import serializers
def load_fixture(apps, schema_editor):
fixture_file = '/full/path/to/testmodel.json'
fixture = open(fixture_file)
objects = serializers.deserialize('json', fixture, ignorenonexistent=True)
for obj in objects:
obj.save()
fixture.close()
Second, monkey-patch apps registry used by serializers:
from django.core import serializers
def load_fixture(apps, schema_editor):
original_apps = serializers.python.apps
serializers.python.apps = apps
fixture_file = '/full/path/to/testmodel.json'
fixture = open(fixture_file)
objects = serializers.deserialize('json', fixture, ignorenonexistent=True)
for obj in objects:
obj.save()
fixture.close()
serializers.python.apps = original_apps
Now serializer will use models state from apps instead of default one and whole migration process will succeed.
To expand on the answer from GwynBleidD and mix in this issue since Postgres won't reset the primary key sequences when loaded this way (https://stackoverflow.com/a/14589706/401636)
I think I now have a failsafe migration for loading fixture data.
utils.py:
import os
from io import StringIO
import django.apps
from django.conf import settings
from django.core import serializers
from django.core.management import call_command
from django.db import connection
os.environ['DJANGO_COLORS'] = 'nocolor'
def reset_sqlsequence(apps=None, schema_editor=None):
"""Suitable for use in migrations.RunPython"""
commands = StringIO()
cursor = connection.cursor()
patched = False
if apps:
# Monkey patch django.apps
original_apps = django.apps.apps
django.apps.apps = apps
patched = True
else:
# If not in a migration, use the normal apps registry
apps = django.apps.apps
for app in apps.get_app_configs():
# Generate the sequence reset queries
label = app.label
if patched and app.models_module is None:
# Defeat strange test in the mangement command
app.models_module = True
call_command('sqlsequencereset', label, stdout=commands)
if patched and app.models_module is True:
app.models_module = None
if patched:
# Cleanup monkey patch
django.apps.apps = original_apps
sql = commands.getvalue()
print(sql)
if sql:
# avoid DB error if sql is empty
cursor.execute(commands.getvalue())
class LoadFixtureData(object):
def __init__(self, *files):
self.files = files
def __call__(self, apps=None, schema_editor=None):
if apps:
# If in a migration Monkey patch the app registry
original_apps = serializers.python.apps
serializers.python.apps = apps
for fixture_file in self.files:
with open(fixture_file) as fixture:
objects = serializers.deserialize('json', fixture)
for obj in objects:
obj.save()
if apps:
# Cleanup monkey patch
serializers.python.apps = original_apps
And now my data migrations look like:
# -*- coding: utf-8 -*-
# Generated by Django 1.11.1 on foo
from __future__ import unicode_literals
import os
from django.conf import settings
from django.db import migrations
from .utils import LoadFixtureData, reset_sqlsequence
class Migration(migrations.Migration):
dependencies = [
('app_name', '0002_auto_foo'),
]
operations = [
migrations.RunPython(
code=LoadFixtureData(*[
os.path.join(settings.BASE_DIR, 'app_name', 'fixtures', fixture) + ".json"
for fixture in ('fixture_one', 'fixture_two',)
]),
# Reverse will NOT remove the fixture data
reverse_code=migrations.RunPython.noop,
),
migrations.RunPython(
code=reset_sqlsequence,
reverse_code=migrations.RunPython.noop,
),
]
When you run python manage.py migrate it's trying to load your testmodel.json in fixtures folder, but your model (after updated) does not match with data in testmodel.json. You could try this:
Change your directory from fixture to _fixture.
Run python manage.py migrate
Optional, you now can change _fixture by fixture and load your data as before with migrate command or load data with python manage.py loaddata app/_fixtures/testmodel.json
Related
I'm creating a data-migration for the new_app with the possibility to roll it back.
# This is `new_app` migration
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.RunPython(import_data, reverse_code=delete_data)
]
This migration adds some data to the model defined in other app: my_other_app. To import the model where I want to update or delete records I use apps.get_model() method.
# This is `new_app` migration
def import_data(apps, schema_editor):
model = apps.get_model('my_other_app', 'MyModel')
It works like charm when I apply migrations. But when I run try to roll back the migration with the :~> manage.py migrate new_app zero I get exception: LookupError: No installed app with label 'my_other_app'. Model import in roll back code:
# This is `new_app` migration
def delete_data(apps, schema_editor):
schema_model = apps.get_model('my_other_app', 'MyModel')
The code for model import is identical, but why it doesn't work during migration roll back? For now I have a workaround with straight model import during roll-back. Don't know if it may cause troubles in future.
Make sure that dependencies includes the latest migration from the other app that you're referencing. eg:
dependencies = [
'my_other_app.0001_initial',
]
Also, make sure 'my_other_app' is in your INSTALLED_APPS setting.
I've been using django-modeltranslation to translate models in django for a while. It is really straightforward and it works really well on apps I've been developing, where all model translated content gets inserted with forms by the final user.
eg: inputs: content, content_en, content_pt, ...
I have to build an application where I need to translate 'built-in' model strings that are generated by django, like 'auth.permission.name' or 'contenttypes.contenttype.name' and add them to translation django.po files.
I came up with a solution that works fine,
which uses post_migration signals that create a file with lists of ugettext_lazy elements, so new strings, like a new contenttype.name for example, are added to 'django.po' dynamically and loaded to the database.
Yet, is a bit weird having to create a file with ugettext calls
in order to register the strings, but I didn't find another way of registering and adding them dynamically to the django.po file, so I need your help
Here's what I have done:
1. I created an app named 'tools', that is the last one on INSTALLED_APPS, so its migrations are naturally the last ones to be called. This app does not have any models, it just runs migrations, has the django-modeltranslation translation.py file and an application config with a post_migration signal call.
# translations.py
from modeltranslation.translator import translator, TranslationOptions
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
class PermissionTranslationOptions(TranslationOptions):
fields = ('name',)
class ContentTypeTranslationOptions(TranslationOptions):
fields = ('name',)
translator.register(Permission, PermissionTranslationOptions)
translator.register(ContentType, ContentTypeTranslationOptions)
2. Running 'manage.py makemigrations' creates the migrations on the 'auth' and 'contenttypes' applications with the extra 'name_*' fields.
3. the app has an application config that has a post_migrate signal
# __init__.py
default_app_config = 'apps.tools.config.SystemConfig'
# config.py
from django.apps import AppConfig
from django.db.models.signals import post_migrate
from apps.tools.translations.exporter import make_translations
from apps.tools.translations.importer import load_translations
def run_translations(sender, **kwargs):
# This creates the translations
make_translations()
# This loads the the translations to the db
load_translations()
class SystemConfig(AppConfig):
name = 'apps.tools'
verbose_name = 'Tools'
def ready(self):
# Call post migration operations
post_migrate.connect(run_translations, sender=self)
4. make_translations() is called after migrations and generates a file with lists of uggettext_lazy calls.
This is the bit I would like to change. Do I really need to create a file?
# exporter
import os
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.utils import translation
from django.contrib.contenttypes.management import update_all_contenttypes
# TODO
# It has got to be another way
def make_translations():
# lets go default
translation.activate("en")
update_all_contenttypes()
try:
f = open(os.path.join(os.path.realpath(os.path.dirname(__file__)), 'translations.py'), 'w')
# Write file
f.write("from django.utils.translation import ugettext_lazy as _\n\n")
# All Permissions to lazy text
f.write('permissions = {\n')
for perm in Permission.objects.all().order_by('id'):
f.write(' "'+str(perm.id)+'": _("'+perm.name+'"),\n')
f.write('}\n\n')
# All Content types to lazy text
f.write('content_types = {\n')
for content in ContentType.objects.all().order_by('id'):
f.write(' "'+str(content.id)+'": _("'+content.name+'"),\n')
f.write('}\n\n')
# Closing file
f.close()
# Importing file to get it registered with ugettext_lazy
try:
from apps.tools.translations import translations
except:
print('Could not import file')
pass
except:
print('Could not create file')
pass
The above results in a file like this:
from django.utils.translation import ugettext_lazy as _
permissions = {
"1": _("Can add permission"),
"2": _("Can change permission"),
"3": _("Can delete permission"),
"4": _("Can add group"),
...
}
content_types = {
"1": _("group"),
"2": _("user"),
"3": _("permission"),
"4": _("content type"),
"5": _("session"),
...
}
5. Running 'makemessages' would add this strings to 'django.po' files, yet, the post_migration signal does not stop here, and loads the existing compiled strings in the database
# importer
from django.contrib.auth.models import Permission
from django.contrib.contenttypes.models import ContentType
from django.conf import settings
from django.utils import translation
def load_translations():
try:
from apps.tools.translations.translations import permissions, content_types
except:
# File does not exists
print('Translations could not be loaded')
return
# For each language
for lang in settings.LANGUAGES:
# Activate language
translation.activate(lang[0])
# Loading translated permissions
all_permissions = Permission.objects.all()
for permission in all_permissions:
permission.name = unicode(permissions[str(permission.id)])
permission.save()
# Loading translated content_types
all_contenttypes = ContentType.objects.all()
for contenttype in all_contenttypes:
contenttype.name = unicode(content_types[str(contenttype.id)])
contenttype.save()
How can I replace 'make_translations()' without creating a file and register those strings with ugettext_lazy?
Thanks for your help
I've read your post and also somehow I had the same problem with translation for permissions, I've found a very short way to solve the problem:
I wouldn't recommend to do this way neither regret that.
but the solution is:
edit the app_labeled_name function decorated as property of this path: .pyenv/Lib/site-packages/django/contrib/contenttypes/models.py of ContentType class, to become like this:
#property
def app_labeled_name(self):
model = self.model_class()
if not model:
return self.model
return '%s | %s' % (apps.get_app_config(model._meta.app_label).verbose_name,
model._meta.verbose_name)
the trick is to use apps.get_app_config(model._meta.app_label).verbose_name instead of model._meta.app_label, so it would use the same verbose_name as whatever use set for your app in the AppConfig subclass of your app.
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 ?
I've done what you are trying to do. The problems are:
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.
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.
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").
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),
]
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.
I'm trying to port my project to use Django 1.7. Everything is fine except 1 thing. Models inside tests folders.
Django 1.7 new migrations run migrate command internally. Before syncdb was ran. That means if a model is not included in migrations - it won't be populated to DB (and also to test DB). That's exactly what I'm experiencing right now.
What I do is:
In my /app/tests/models.py I have dummy model: class TestBaseImage(BaseImage): pass
All it does is to inherit from an abstract BaseImage model.
Then in tests I create instances of that dummy model to test it.
The problem is that it doesn't work any more. It's not included in migrations (that's obvious as I don't want to keep my test models in a production DB). Running my tests causes DB error saying that table does not exist. That makes sense as it's not included in migrations.
Is there any way to make it work with new migrations system? I can't find a way to "fix" that.
Code I use:
app/tests/models.py
from ..models import BaseImage
class TestBaseImage(BaseImage):
"""Dummy model just to test BaseImage abstract class"""
pass
app/models.py
class BaseImage(models.Model):
# ... fields ...
class Meta:
abstract = True
factories:
class BaseImageFactory(factory.django.DjangoModelFactory):
"""Factory class for Vessel model"""
FACTORY_FOR = BaseImage
ABSTRACT_FACTORY = True
class PortImageFactory(BaseImageFactory):
FACTORY_FOR = PortImage
example test:
def get_model_field(model, field_name):
"""Returns field instance"""
return model._meta.get_field_by_name(field_name)[0]
def test_owner_field(self):
"""Tests owner field"""
field = get_model_field(BaseImage, "owner")
self.assertIsInstance(field, models.ForeignKey)
self.assertEqual(field.rel.to, get_user_model())
There is a ticket requesting a way to do test-only models here
As a workaround, you can decouple your tests.py and make it an app.
tests
|--migrations
|--__init__.py
|--models.py
|--tests.py
You will end up with something like this:
myapp
|-migrations
|-tests
|--migrations
|--__init__.py
|--models.py
|--tests.py
|-__init__.py
|-models.py
|-views.py
Then you should add it to your INSTALLED_APPS
INSTALLED_APPS = (
# ...
'myapp',
'myapp.tests',
)
You probably don't want to install myapp.tests in production, so you can keep separate settings files. Something like this:
INSTALLED_APPS = (
# ...
'myapp',
)
try:
from local_settings import *
except ImportError:
pass
Or better yet, create a test runner and install your tests there.
Last but not least, remember to run python manage.py makemigrations
Here's a workaround that seems to work. Trick the migration framework into thinking that there are no migrations for your app. In settings.py:
if 'test' in sys.argv:
# Only during unittests...
# myapp uses a test-only model, which won't be loaded if we only load
# our real migration files, so point to a nonexistent one, which will make
# the test runner fall back to 'syncdb' behavior.
MIGRATION_MODULES = {
'myapp': 'myapp.migrations_not_used_in_tests'
}
I found the idea on the first post in ths Django dev mailing list thread, and it's also currently being used in Django itself, but it may not work in future versions of Django where migrations are required and the "syncdb fallback" is removed.
I have been trying to find the answer in the Django Auth docs, but can not seem to find what I am looking for.
The problem I am having is, when I define the code for adding Groups (same as Groups in the admin page):
#read_only
group, created = Group.objects.get_or_create(name='read_only')
if created:
group.permissions.add(can_read_campaign)
logger.info('read_only_user Group created')
#standard
group, created = Group.objects.get_or_create(name='standard_user')
if created:
group.permissions.add(can_edit_users)
logger.info('standard_user Group created')
#admin
group, created = Group.objects.get_or_create(name='admin_user')
if created:
group.permissions.add(can_edit_campaign, can_edit_users)
logger.info('admin_user Group created')
When I have run this code in models.py and init.py and they both give me this error:
django.core.exceptions.AppRegistryNotReady
I presume this is due to the Model/init trying to insert things into the django app/admin too early?
How can I add these Groups programmatically?
EDIT:
This is not a duplicate question, this was actually adding permission and groups within the models during setup of the project, rather than through the shell.
I have solved this issues, by using signals and receivers (django modules).
I added the code to create the permissions/groups into it's own function and decorated this with a receiver (post_migrate), which will run this function after migrations are complete, removing this error.
#receiver(post_migrate)
def init_groups(sender, **kwargs):
#permission and group code goes here
I was recommended this way to do it:
Create a fake migration in the appropriate module:
python manage.py makemigrations --empty yourappname
Open up the file that was created, which should look like this:
# -*- coding: utf-8 -*-
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('yourappname', '0001_initial'),
]
operations = [
]
And add your code:
# -*- coding: utf-8 -*-
from django.db import models, migrations
def add_group_permissions():
#read_only
group, created = Group.objects.get_or_create(name='read_only')
if created:
group.permissions.add(can_read_campaign)
logger.info('read_only_user Group created')
#standard
group, created = Group.objects.get_or_create(name='standard_user')
if created:
group.permissions.add(can_edit_users)
logger.info('standard_user Group created')
#admin
group, created = Group.objects.get_or_create(name='admin_user')
if created:
group.permissions.add(can_edit_campaign, can_edit_users)
logger.info('admin_user Group created')
class Migration(migrations.Migration):
dependencies = [
('yourappname', '0001_initial'),
]
operations = [
migrations.RunPython(add_group_permissions),
]
Finally, run the migration:
python manage.py migrate
This is nice because you can deploy to Heroku or wherever and be sure it'll be applied, as it's just another migration.
Combining #Robert Grant and this I was able to do it like:
python manage.py makemigrations --empty yourappname
And then:
from django.contrib.auth.models import Group, Permission
from django.db import models, migrations
import logging
logger = logging.getLogger(__name__)
campaign_group_permissions = {
"Campaign Manager": [
"add_campaign",
"change_campaign",
"delete_campaign",
"view_campaign",
"add_campaignsms",
"add_sending",
"change_sending",
"view_sending"
]
}
def add_group_permissions():
# See https://code.djangoproject.com/ticket/23422
db_alias = schema_editor.connection.alias
try:
emit_post_migrate_signal(2, False, 'default')
except TypeError: # Django < 1.8
emit_post_migrate_signal([], 2, False, 'default', db_alias)
for group in campaign_group_permissions:
role, created = Group.objects.get_or_create(name=group)
logger.info(f'{group} Group created')
for perm in campaign_group_permissions[group]:
role.permissions.add(Permission.objects.get(codename=perm))
logger.info(f'Permitting {group} to {perm}')
role.save()
class Migration(migrations.Migration):
dependencies = [
('yourappname', '0001_initial'),
]
operations = [
migrations.RunPython(add_group_permissions),
]
Note: this works on Django 3.x, but I'm pretty sure it will work for Django 1.7 as well.
#Ruloweb's fantastic response almost worked for me, but I had to make a couple tweaks to get it to work in Django 3.1 with multiple apps.
First, I needed to add arguments to the add_group_permissions() function. I also needed to import the emit_post_migration_signal:
from django.contrib.auth.models import Group, Permission
from django.core.management.sql import emit_post_migrate_signal # <-- Added this
from django.db import models, migrations
import logging
logger = logging.getLogger(__name__)
public_group_permissions = {
"Your permission group name here": ['your permissions here']
}
def add_group_permissions(apps, schema_editor): # <-- updated this
# See https://code.djangoproject.com/ticket/23422
db_alias = schema_editor.connection.alias
try:
emit_post_migrate_signal(2, False, 'default')
except TypeError: # Django < 1.8
emit_post_migrate_signal([], 2, False, 'default', db_alias)
for group in public_group_permissions:
role, created = Group.objects.get_or_create(name=group)
logger.info(f'{group} Group created')
for perm in public_group_permissions[group]:
role.permissions.add(Permission.objects.get(codename=perm))
logger.info(f'Permitting {group} to {perm}')
role.save()
class Migration(migrations.Migration):
dependencies = [
('your_app_name', '0001_initial'),
]
operations = [
migrations.RunPython(add_group_permissions),
]