10

Let's imagine a following simplified Django project:

<root>/lib/python2.7/site-packages/externalapp/shop
<root>/myapp

myapp also extends externalapp.shop.models models by adding a few fields. manage.py makemigrations did generated following schema migration file called 0004_auto_20150410_2001.py:

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


class Migration(migrations.Migration):

    # __init__ is added by me as an attempt how to tell django's
    # migration loader operations are for the different application
    def __init__(self, name, app_label):
        super(Migration, self).__init__(name, 'shop')

    dependencies = [
        ('myapp', '__first__'),
        ('shop', '0003_auto_20150408_0958'),
    ]

    operations = [
        migrations.AddField(
            model_name='product',
            name='vat',
            field=models.ForeignKey(to='myapp.VAT', null=True),
        ),
    ]

If the above migration schema is placed in <root>/lib/python2.7/site-packages/externalapp/shop/migrations/ path by default, manage.py migrate succeeds and table fields are correctly added.

However if I do move the above migration file into myapp/migrations/, following manage.py migrate fails with

django.core.management.base.CommandError: Conflicting migrations detected (0001_initial, 0004_auto_20150410_2001 in myapp). To fix them run 'python manage.py makemigrations --merge'

error message I can't quite understand and suggested makemigrations --merge fails with expected:

ValueError: Could not find common ancestor of set([u'0001_initial', u'0004_auto_20150410_2001'])

I've tried to override migrations.Migration.__init__ to alter derived app_label but seems migration loader ignores it.

How adjust migration file so it can work from other application ? The reason is in production externalapp sources can't be directly touched, are read only.

David Unric
  • 7,421
  • 1
  • 37
  • 65
  • 1
    How do you add fields to the external app from your own app? That sounds like a potential disaster. As for the migrations, all migrations within a single app must form a single, unbranched path from the first to the last migration. You have two endpoints for your migrations. You should change the `('myapp', '__first__')` dependency to depend on the last migration in `myapp`. – knbk Apr 11 '15 at 11:31
  • @knbk Thanks for the hint. It fixes conflicting migrations error, but new with *state.models[app_label, self.model_name_lower].fields.append((self.name, field)) KeyError: (u'myapp', u'product')* arises. How explicitly tell django/south it should operate on `externalapp.shop` instead of `myapp` for this specific migration ? *adding a few fields from other application, if you know what you are doing, need not to be a disaster, even a simpler and faster solution (no extra table & sql joins). [Related blog](http://blog.jupo.org/2011/11/10/django-model-field-injection/) about.* – David Unric Apr 11 '15 at 15:08
  • Update: I did dropped the database, uncommented Migration initializer and rerun all migrations and it started to work ! Thank you. – David Unric Apr 11 '15 at 15:19

2 Answers2

20

To move a migration file around a Django project, like in case of injecting models of other applications, you need to ensure in your django.db.migrations.Migration descendant:

  • explicitly set application name, as migrations loader derives it automatically by app where migration file resides and would attempt to perform operations on different models otherwise
  • notify migrations recorder it provides migration for other application or it would still consider migration as unapplied (records about applied migrations are stored in a table, currently named django_migrations)

I've solved the issue in migration initializer which may look like:

from django.db import migrations

TARGET_APP = 'shop'    # application label migration is for

class Migration(migrations.Migration):

    def __init__(self, name, app_label):
        # overriding application operated upon
        super(Migration, self).__init__(name, TARGET_APP)

    # specify what original migration file it replaces
    # or leave migration loader confused about unapplied migration
    replaces = ((TARGET_APP, __module__.rsplit('.', 1)[-1]),)

It does work for me and find it enough generic way.

Eager to hear about a better/simpler solution if possible.

David Unric
  • 7,421
  • 1
  • 37
  • 65
  • Can you please explain this line: replaces = ((TARGET_APP, __module__..rsplit('.', 1)[-1]),) – Mikael Jun 10 '15 at 09:38
  • @Mikael `replaces` is a class variable which specifies tuple pairs in format `(, )`, ie. which migration substitutes the original one. `__module__` specifies (dot separated) module path where the migration class descendant is being defined. `rsplit('.', 1)[-1]` extracts the tail of the path. – David Unric Jun 10 '15 at 10:56
  • Ah, the confusing part was __module__ , now I understand. Thanks! – Mikael Jun 10 '15 at 14:12
3

Since Django 1.9 there’s the MIGRATION_MODULES setting that you can use to pull the migrations of "foreign" models into your app.

As outlined in the FeinCMS docs, you create a new package (folder with __init__.py) in your app and list the foreign apps in settings like so:

MIGRATION_MODULES = {
    'one': 'yourapp.foreigners.one',
    'other': 'yourapp.foreigners.other',
}

Afterwards you can just manage.py makemigrations one other etc.

Hraban
  • 545
  • 5
  • 10