Coder Social home page Coder Social logo

openwisp / django-swappable-models Goto Github PK

View Code? Open in Web Editor NEW
226.0 226.0 30.0 90 KB

Swapper - The unofficial Django swappable models API. Maintained by the OpenWISP project.

Home Page: http://openwisp.org

License: MIT License

Python 99.36% Shell 0.64%
django django-models foreign-keys migrations reusable-app swappable-models swapper

django-swappable-models's People

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

django-swappable-models's Issues

Problem when app's name is namespaced

Hello,

I have a bunch of apps namespaced in a django_omop module, meaning each app_label contains a dot ., which causes problems with the swap setting definition because it also includes the dot ..

For instance, when using swapper.get_model_name('django_omop.vocabulary', 'Concept'), it tries to fetch the setting DJANGO_OMOP.VOCABULARY_CONCEPT.

While such a setting could be defined by modifying globals(), this is really not a good solution.

This could easily be fixed by modifying swapper.swappable_setting and replacing dot . with something like double underscore __.

I can propose a fix in a PR if you want.

[docs] Add change log

Add CHANGES.rst as we have in the rest of OpenWISP modules. For the previous versions we can include a brief summary while we'll be more detailed with the new versions.

Incompatible with Django 1.7

Traceback (most recent call last):
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/src/manage.py", line 14, in <module>
    execute_from_command_line(sys.argv)
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/core/management/__init__.py", line 385, in execute_from_command_line
    utility.execute()
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/core/management/__init__.py", line 354, in execute
    django.setup()
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/__init__.py", line 21, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/apps/registry.py", line 108, in populate
    app_config.import_models(all_models)
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/apps/config.py", line 202, in import_models
    self.models_module = import_module(models_module_name)
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/importlib/__init__.py", line 109, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 2231, in _gcd_import
  File "<frozen importlib._bootstrap>", line 2214, in _find_and_load
  File "<frozen importlib._bootstrap>", line 2203, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 1200, in _load_unlocked
  File "<frozen importlib._bootstrap>", line 1129, in _exec
  File "<frozen importlib._bootstrap>", line 1448, in exec_module
  File "<frozen importlib._bootstrap>", line 321, in _call_with_frames_removed
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/src/boss/apps/base_partners/models.py", line 37, in <module>
    class AbstractPartner(models.Model):
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/src/boss/apps/base_partners/models.py", line 41, in AbstractPartner
    Partner = load_model("boss_base_partners", "PartnerTag")
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/src/boss/swapper.py", line 81, in load_model
    cls = get_model(app_label, model)
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/db/models/__init__.py", line 54, in alias
    return getattr(loading, function_name)(*args, **kwargs)
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/apps/registry.py", line 199, in get_model
    self.check_models_ready()
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/apps/registry.py", line 131, in check_models_ready
    raise AppRegistryNotReady("Models aren't loaded yet.")
django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet.

Rise this error!!!

ImproperlyConfigured exception running tests for reusable app

After the changes for my reusable app which already had its test cases working before using django-swappable-models, the test cases have started to fail.

Here is the stacktrace when running the tests for the reusableapp (in this case it is django-blog-zinnia)

Traceback (most recent call last):
  File "./bin/test", line 30, in <module>
    sys.exit(nose.main(argv=['nose', '--with-sfd', '--with-progressive', '--nologcapture', '-s']+sys.argv[1:]))
  File "/home/ubuntu/django-blog-zinnia/eggs/nose-1.3.7-py2.7.egg/nose/core.py", line 121, in __init__
    **extra_args)
  File "/usr/lib/python2.7/unittest/main.py", line 94, in __init__
    self.parseArgs(argv)
  File "/home/ubuntu/django-blog-zinnia/eggs/nose-1.3.7-py2.7.egg/nose/core.py", line 145, in parseArgs
    self.config.configure(argv, doc=self.usage())
  File "/home/ubuntu/django-blog-zinnia/eggs/nose-1.3.7-py2.7.egg/nose/config.py", line 347, in configure
    self.plugins.begin()
  File "/home/ubuntu/django-blog-zinnia/eggs/nose-1.3.7-py2.7.egg/nose/plugins/manager.py", line 99, in __call__
    return self.call(*arg, **kw)
  File "/home/ubuntu/django-blog-zinnia/eggs/nose-1.3.7-py2.7.egg/nose/plugins/manager.py", line 167, in simple
    result = meth(*arg, **kw)
  File "/home/ubuntu/django-blog-zinnia/eggs/nose_sfd-0.4-py2.7.egg/sfd/sfd.py", line 41, in begin
    django.setup()  # Django >= 1.7
  File "/home/ubuntu/django-blog-zinnia/eggs/Django-1.10.5-py2.7.egg/django/__init__.py", line 27, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/home/ubuntu/django-blog-zinnia/eggs/Django-1.10.5-py2.7.egg/django/apps/registry.py", line 115, in populate
    app_config.ready()
  File "/home/ubuntu/django-blog-zinnia/eggs/Django-1.10.5-py2.7.egg/django/contrib/admin/apps.py", line 23, in ready
    self.module.autodiscover()
  File "/home/ubuntu/django-blog-zinnia/eggs/Django-1.10.5-py2.7.egg/django/contrib/admin/__init__.py", line 26, in autodiscover
    autodiscover_modules('admin', register_to=site)
  File "/home/ubuntu/django-blog-zinnia/eggs/Django-1.10.5-py2.7.egg/django/utils/module_loading.py", line 50, in autodiscover_modules
    import_module('%s.%s' % (app_config.name, module_to_search))
  File "/usr/lib/python2.7/importlib/__init__.py", line 37, in import_module
    __import__(name)
  File "/home/ubuntu/django-blog-zinnia/zinnia/admin/__init__.py", line 6, in <module>
    from zinnia.admin.category import CategoryAdmin
  File "/home/ubuntu/django-blog-zinnia/zinnia/admin/category.py", line 7, in <module>
    from zinnia.admin.forms import CategoryAdminForm
  File "/home/ubuntu/django-blog-zinnia/zinnia/admin/forms.py", line 16, in <module>
    Entry = swapper.load_model("zinnia", "Entry")
  File "/home/ubuntu/Env/zinnia/lib/python2.7/site-packages/swapper/__init__.py", line 94, in load_model
    "Could not find {name}!".format(name=join(app_label, model))
django.core.exceptions.ImproperlyConfigured: Could not find zinnia.Entry!
make: *** [test] Error 1

The code for admin/forms.py looks somewhat like this:

Entry = swapper.load_model("zinnia", "Entry")

class EntryAdminForm(forms.ModelForm):
    """
    Form for Entry's Admin.
    """
   ...
   ...
   ...
    def __init__(self, *args, **kwargs):
        super(EntryAdminForm, self).__init__(*args, **kwargs)

        Entry = swapper.load_model("zinnia", "Entry")

        self.fields['categories'].widget = RelatedFieldWidgetWrapper(
            self.fields['categories'].widget,
            Entry.categories.field.remote_field,
            self.admin_site)

    class Meta:
        """
        EntryAdminForm's Meta.
        """
        model = Entry
        fields = forms.ALL_FIELDS
        ...

The documentation states that swapper.load_model works only when the django has loaded all the models so probably defining the model = Entry in the class Meta seems incorrect to me.

In that case will I need to import the default Entry model in the admin/forms.py and in the init method of EntryAdmin, reload the Entry model via swapper.load_model (as already done) ?

If so it would be great if we could have the documentation updated about this.

How to register an model in Django Admin?

When I try to register an model in Django Admin, rise the error

from django.contrib import admin

from boss.swapper import load_model

Country = load_model("boss_base_addresses", "Country")
State = load_model("boss_base_addresses", "State")
Locality = load_model("boss_base_addresses", "Locality")
Geolocation = load_model("boss_base_addresses", "Geolocation")
Address = load_model("boss_base_addresses", "Address")

class CountryAdmin(admin.ModelAdmin):
    search_fields = ('name', 'code')

class StateAdmin(admin.ModelAdmin):
    search_fields = ('name', 'code')

class LocalityAdmin(admin.ModelAdmin):
    search_fields = ('name', 'postal_code')

class AddressAdmin(admin.ModelAdmin):
    search_fields = ('name',)

class GeolocationAdmin(admin.ModelAdmin):
    search_fields = ('latitude', 'longitude',)

admin.site.register(Country, CountryAdmin)
admin.site.register(State, StateAdmin)
admin.site.register(Locality, LocalityAdmin)
admin.site.register(Geolocation, GeolocationAdmin)
admin.site.register(Address, AddressAdmin)

Traceback (most recent call last):
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/src/manage.py", line 14, in <module>
    execute_from_command_line(sys.argv)
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/core/management/__init__.py", line 385, in execute_from_command_line
    utility.execute()
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/core/management/__init__.py", line 354, in execute
    django.setup()
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/__init__.py", line 21, in setup
    apps.populate(settings.INSTALLED_APPS)
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/apps/registry.py", line 115, in populate
    app_config.ready()
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/contrib/admin/apps.py", line 22, in ready
    self.module.autodiscover()
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/contrib/admin/__init__.py", line 23, in autodiscover
    autodiscover_modules('admin', register_to=site)
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/utils/module_loading.py", line 74, in autodiscover_modules
    import_module('%s.%s' % (app_config.name, module_to_search))
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/importlib/__init__.py", line 109, in import_module
    return _bootstrap._gcd_import(name[level:], package, level)
  File "<frozen importlib._bootstrap>", line 2231, in _gcd_import
  File "<frozen importlib._bootstrap>", line 2214, in _find_and_load
  File "<frozen importlib._bootstrap>", line 2203, in _find_and_load_unlocked
  File "<frozen importlib._bootstrap>", line 1200, in _load_unlocked
  File "<frozen importlib._bootstrap>", line 1129, in _exec
  File "<frozen importlib._bootstrap>", line 1448, in exec_module
  File "<frozen importlib._bootstrap>", line 321, in _call_with_frames_removed
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/src/boss/apps/base_addresses/admin.py", line 34, in <module>
    admin.site.register(Country, CountryAdmin)
  File "/home/cfpp/workspace/cerebromix/src/dev-backend/lib/python3.4/site-packages/django/contrib/admin/sites.py", line 78, in register
    for model in model_or_iterable:
TypeError: 'NoneType' object is not iterable

[QA] Add run-qa-checks script

Add run-qa-checks script like we have in other modules, ensure flake8, isort and black formatting issues are fixed.

need clarification

EDIT totally re-written for reasons of brevity and clarity

This is a possible duplicate of issue #10, but it wasn't written in a way which was concrete enough for me to be sure. I'll try and make this as concrete and minimal as possible. I have 3 questions (below).

Consider reusableapp which consists of

reusableapp/models.py

from django.db import models
import swapper

class FooBase(models.Model):
    some_field = models.CharField(max_length=32)

    class Meta:
        abstract = True


class Foo(FooBase):
    # default (swappable) implementation of FooBase
    class Meta:
        swappable = swapper.swappable_setting("reusableapp", "Foo")

reusableapp/migrations/0001_initial.py

from __future__ import unicode_literals

from django.db import migrations, models
import swapper


class Migration(migrations.Migration):

    dependencies = [
        swapper.dependency('reusableapp', 'Foo')
    ]

    operations = [
        migrations.CreateModel(
            name='Foo',
            fields=[
                ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
                ('some_field', models.CharField(max_length=32)),
            ],
            options={
                'swappable': swapper.swappable_setting("reusableapp", "Foo"),
            },
        ),
    ]

and 'myapp' which only consists of:

myapp/models.py

from django.db import models
from reusableapp.models import FooBase

class Bar(models.Model):
    name = models.CharField(max_length=32)

class Foo(FooBase):
         bar = models.ForeignKey('Bar', related_name='foo', null=True)

question 1 should myapp/model.py:Foo also have:

       class Meta:
           swappable = swapper.swappable_setting("reusableapp", "Foo")

I think the answer is yes, as I don't see why this implementation is any different than the default implementation in reusableapp. Also, I seem to get field lookup errors, as mentioned in issue #10 if this isn't declared.

question 2 does the migration for myapp which introduces it's own implementation of FooBase need to be updated to use swapper instead of settings as in reusableapp/migrations/0001_initial.py?

question 3 if the answer to question 2 is yes, should it declare:

   dependencies = [
      swapper.dependency("reusableapp", "Foo")
   ]

[feature] Add swappable dependency pointing to __latest__

This can be useful in scenarios like openwisp/openwisp-radius#328 (review).

I think we should add an optional latest=False keyword argument to this function:

def dependency(app_label, model):
"""
Returns a Django 1.7+ style dependency tuple for inclusion in
migration.dependencies[]
"""
from django.db.migrations import swappable_dependency
return swappable_dependency(get_model_name(app_label, model))

If latest is True, we should call a new function which works identically to django's swappable_dependency with the difference that it points to __latest__ instead.

We should document this feature and warn against it's possible drawbacks, recommending users to use it with caution and only if the default usage doesn't work.

[change] Allow passing a specific migration number

In some cases, apps we depend on have data migrations we must depend on which are not included in the initial migration but may be included in the successive migrations.

Depending on latest doesn't fix that and may introduce more harm than good.
In these cases we simply have to depend on the specific migration and recommend anyone wanting to swap that particular app to create a migration with the same name in their swapped models.

I think we should change dependency as follows:

def dependency(app_label, model, latest=False):
"""
Returns a Django 1.7+ style dependency tuple for inclusion in
migration.dependencies[]
"""
dependencies = swappable_dependency(get_model_name(app_label, model))
if not latest:
return dependencies
return dependencies[0], '__latest__'

  • drop latest argument which may do more harm than good
  • add a version attribute which defaults to None and if present is returned similary to how we return __latest__ now

We should then update the docs about latest and replace those with this concept.

@codesankalp what do you think?

PS: see also openwisp/openwisp-users#300.

Failure with makemigrations

I always get a failed field lookup during makemigrations if the base app ('reusableapp') already has a migration in place, and the derived app ('myapp') did not previously provide a swapped version of a swappable model, but now does provide one, and there is another (swappable?) model which has a foreign key to this newly added model. It does not matter whether the other model was original or swapped.

The error is a lookup failure for the swappable model in the init function of the StateApps class in Django (django/db/migrations/state.py). In that location the exception is ignored if the 'ignore_swappable' parameter is True but ONLY if also app_label == settings.AUTH_USER_MODEL.

Removing the limitation on app_label from Django seems to result in makemigrations completing successfully and correctly.

Running makemigrations only for 'myapp' does not matter.

Changing the reusableapp migration file as in the instructions does not make a difference. Django version also does not matter, anything from 1.7 up to 1.9 does this.

Swapping Child rather than Parent does not result in the same problems (note that there is no foreign key pointing to it).

Making additional changes to either model after successfully creating the initial ones does not pose any problems either (perhaps unless another foreign key is added but I haven't tested).

So it seems that using swapper with migrations already provided with the 'reusableapp' forces you to manually make an initial migration for the app/project which uses it. Unless I am missing something, in which case the README is incomplete.

Reusable apps can not be shipped with migrations

Hello!

First of all, many thanks for this repo! I hope Django will ship this feature in its core in not such a distant future.

I think that migrations don't really work with this system (except for simple cases).

Consider 3 models with ForeignKey relationships, A -> B -> C (C depends on B which depends on A). If you swap model B for your own, you will run into a problem:

  • the reusable app needs your model B to migrate, because A uses B, which you redefined
  • your website needs model C to migrate, because B has a ForeignKey to C, but C is in the reusable app

If you generated the migrations file in the reusable app, you basically end up with a circular import.

django.db.migrations.exceptions.CircularDependencyError: other_app.0001_initial, reusable_app.0001_initial

You can actually see the problem with the following example:

models.py in the reusable app:

import swapper

from django.db import models

class BaseA(models.Model):
    class Meta:
        abstract = True

class A(BaseA):
    class Meta:
        swappable = swapper.swappable_setting('reusable_app', 'A')

class BaseB(models.Model):
    a = models.ForeignKey(swapper.get_model_name('reusable_app', 'A'), on_delete=models.CASCADE)

    class Meta:
        abstract = True

class B(BaseB):
    class Meta:
        swappable = swapper.swappable_setting('reusable_app', 'B')

class BaseC(models.Model):
    b = models.ForeignKey(swapper.get_model_name('reusable_app', 'B'), on_delete=models.CASCADE)

    class Meta:
        abstract = True

class C(BaseC):
    class Meta:
        swappable = swapper.swappable_setting('reusable_app', 'C')

models.py in the other app:

from django.db import models
from reusable_app.models import BaseB

class B(BaseB, models.Model):
    name = models.CharField("extra field for B", max_length=255)

First attempt: we generate migrations for the reusable app first

In this first case, your other app is not in INSTALLED_APPS, and you only make migrations for your reusable app, so that it can create a proper database schema when installed.

So you run:

$ ./manage.py makemigrations reusable_app
Migrations for 'reusable_app':
  reusable_app/migrations/0001_initial.py:
    - Create model A
    - Create model B
    - Create model C

You get a nice migrations file:

# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-10-19 08:21
from __future__ import unicode_literals

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='A',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
            ],
            options={
                'swappable': 'REUSABLE_APP_A_MODEL',
            },
        ),
        migrations.CreateModel(
            name='B',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('a', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.REUSABLE_APP_A_MODEL)),
            ],
            options={
                'swappable': 'REUSABLE_APP_B_MODEL',
            },
        ),
        migrations.CreateModel(
            name='C',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('b', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.REUSABLE_APP_B_MODEL)),
            ],
            options={
                'swappable': 'REUSABLE_APP_C_MODEL',
            },
        ),
    ]

You can, of course, modify it according to your documentation in order to avoid blindly reading configuration values from django.conf.settings which may not be defined:

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

from django.db import migrations, models
import django.db.models.deletion
import swapper


class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='A',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
            ],
            options={
                'swappable': swapper.swappable_setting('reusable_app', 'A'),
            },
        ),
        migrations.CreateModel(
            name='B',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('a', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=swapper.get_model_name('reusable_app', 'A'))),
            ],
            options={
                'swappable': swapper.swappable_setting('reusable_app', 'B'),
            },
        ),
        migrations.CreateModel(
            name='C',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('b', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=swapper.get_model_name('reusable_app', 'B'))),
            ],
            options={
                'swappable': swapper.swappable_setting('reusable_app', 'C'),
            },
        ),
    ]

And this works fine:

$ ./manage.py migrate
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, reusable_app, sessions
Running migrations:
  Applying reusable_app.0001_initial... OK

But now, lets say I want to use my other app that ships with its own version of the "B" model.

I add other_app to INSTALLED_APPS and specify REUSABLE_APP_B_MODEL = 'other_app.B'. Now it doesn't work anymore…

$ ./manage.py makemigrations other_app
Traceback (most recent call last):
  File "./manage.py", line 22, in <module>
    execute_from_command_line(sys.argv)
  File ".../django/core/management/__init__.py", line 367, in execute_from_command_line
    utility.execute()
  File ".../django/core/management/__init__.py", line 359, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File ".../django/core/management/base.py", line 294, in run_from_argv
    self.execute(*args, **cmd_options)
  File ".../django/core/management/base.py", line 345, in execute
    output = self.handle(*args, **options)
  File ".../django/core/management/commands/makemigrations.py", line 173, in handle
    migration_name=self.migration_name,
  File ".../django/db/migrations/autodetector.py", line 47, in changes
    changes = self._detect_changes(convert_apps, graph)
  File ".../django/db/migrations/autodetector.py", line 132, in _detect_changes
    self.old_apps = self.from_state.concrete_apps
  File ".../django/db/migrations/state.py", line 180, in concrete_apps
    self.apps = StateApps(self.real_apps, self.models, ignore_swappable=True)
  File ".../django/db/migrations/state.py", line 249, in __init__
    raise ValueError("\n".join(error.msg for error in errors))
ValueError: The field reusable_app.C.b was declared with a lazy reference to 'other_app.b', but app 'other_app' isn't installed.

Well, Django is lying in a way. Because other_app IS installed:

$ ./manage.py check
System check identified no issues (0 silenced).
$ ./manage.py shell
Python 3.5.1 (default, Jan 22 2016, 08:54:32) 
[GCC 4.2.1 Compatible Apple LLVM 7.0.2 (clang-700.1.81)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import swapper
>>> swapper.load_model('reusable_app', 'B')
<class 'other_app.models.B'>

I guess that Django doesn't see the other_app being installed because it reads the reusable_app.0001_initial migration file, which doesn't depend on anything, so it loads it before loading the other_app, and see references to other_app that it cannot resolve.

I tried to add fill the dependencies parameter in reusable_app.0001_initial, I was hoping that it would trigger an import of the other_app, but it didn't change anything:

    dependencies = [
        swapper.dependency('reusable_app', 'A'),
        swapper.dependency('reusable_app', 'B'),
        swapper.dependency('reusable_app', 'C'),
    ]

Second attempt: generate both migration files at the same time

So since generating migrations for the reusable app first doesn't work, there is a workaround. To clean things up, I just deleted the migrations file from the reusable app.

Now the only way to make the whole thing work is to type the following:

$ ./manage.py makemigrations
Migrations for 'other_app':
  other_app/migrations/0001_initial.py:
    - Create model B
  other_app/migrations/0002_b_a.py:
    - Add field a to b
Migrations for 'reusable_app':
  reusable_app/migrations/0001_initial.py:
    - Create model A
    - Create model B
    - Create model C

When I apply these migrations, they are executed in the following order:

  • other_app.0001_initial
  • reusable_app.0001_initial
  • other_app.0002_b_a

I guess that Django splits migrations in order to resolve the circular dependencies issue.

In my real-life project, I have a much more complicated setup with 12 inter-dependent swappable models. I guess it can lead to even more complicated inter-depndencies…

Preventing circular dependency

I have the following models for my reusableapp:

class BaseParent(models.Model):
    name = models.CharField(max_length=32)
    authors = models.ManyToManyField('reusableapp.Author')

    class Meta:
        abstract = True

class Parent(BaseParent):
    class Meta:
        swappable = swapper.swappable_setting('reusableapp', 'Parent')

class Author(models.Model):
    name = models.CharField(max_length=32)

Here are the migrations for the reusableapp:

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

class Migration(migrations.Migration):

    initial = True

    dependencies = [
    ]

    operations = [
        migrations.CreateModel(
            name='Author',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=32)),
            ],
        ),
    ]
from __future__ import unicode_literals
from django.db import migrations, models
import swapper

class Migration(migrations.Migration):

    dependencies = [
        ('reusableapp', '0001_initial'),
        swapper.dependency('reusableapp', 'Parent')
    ]

    operations = [
        migrations.CreateModel(
            name='Parent',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=32)),
            ],
            options={
                'swappable': swapper.swappable_setting('reusableapp', 'Parent')
            },
        ),
        migrations.AddField(
            model_name='parent',
            name='authors',
            field=models.ManyToManyField(to='reusableapp.Author'),
        ),
    ]

Now in myapp I have:

from django.db import models
from reusableapp.models import BaseParent

class Parent(BaseParent):
    extra_field = models.CharField(max_length=32)

And in myapp settings I have:

REUSABLEAPP_PARENT_MODEL = 'myapp.Parent'

On running makemigrations it generates the following migration for myapp:

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

class Migration(migrations.Migration):
    initial = True

    dependencies = [
        ('reusableapp', '0002_auto_20170414_1447'),
    ]

    operations = [
        migrations.CreateModel(
            name='Parent',
            fields=[
                ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                ('name', models.CharField(max_length=32)),
                ('extra_field', models.CharField(max_length=32)),
                ('authors', models.ManyToManyField(to='reusableapp.Author')),
            ],
            options={
                'abstract': False,
            },
        ),
    ]

But it leads to circular dependency as can be seen when running migrate on myapp:

django.db.migrations.exceptions.CircularDependencyError: myapp.0001_initial, reusableapp.0002_auto_20170414_1447

As per the documentation I have added the dependency on Parent in my 0002_auto... migration:

swapper.dependency('reusableapp', 'Parent')

If I remove this dependency, then my migrations run fine. Either I am missing something or the documentation requires some changes?

Support for Python 3.7

Python 3.7 is nowadays the most commonly used python3 version. But the tests fail:

[ 8s] File "/home/abuild/rpmbuild/BUILD/django-swappable-models-1.1.0/tests/default_app/models.py", line 18, in Item
[ 8s] type = models.ForeignKey(swapper.get_model_name('default_app', "Type"))
[ 8s] TypeError: init() missing 1 required positional argument: 'on_delete'

Could you please make the software compatible with Python 3.7? Thank you.

Abandoned?

Hi,

This API seems like the perfect fit for a new project of mine, but I'm wondering if it has been abandoned? No official 2.x support yet nor any commits in the main branch for 2 years. Should I assume that if we decide to use it we'll have to maintain it ourselves?

Thanks!

How to allow inheritance of swapped model?

Hi, this is from a question of mine that i posted at StackOverflow. Since nobody has answered in a while, i thought that i should asked it here also.

Excerpt from StackOverflow

Django - How to allow inheritance of swappable model?

I am using swappable to make a reusable app (named Meat) that provides models that Developers can swap for their own. That model is a super class of other models.

from django.db.models import Model, CharField
from swapper import swappable_setting

class AbstractMeat(Model):
    class Meta:
        abstract = True
    name = CharField(max_length=16)

class Meat(AbstractMeat):
    class Meta:
        swappable = swappable_setting("cyber", "Meat")

class Pork(Meat):
    pass

class Fish(Meat):
    pass

To test this, i created the real app and set MEAT_MEAT_MODEL.

# settings.py
MEAT_MEAT_MODEL = "real.RealMeat"

# real/models.py
from django.forms import IntegerField
from cyber.models import AbstractMeat

class RealMeat(AbstractMeat):
    price = IntegerField()

Running runserver i get this error:

meat.Fish.meat_ptr: (fields.E301) Field defines a relation with the model 'meat.Meat', which has been swapped out.
	HINT: Update the relation to point at 'settings.MEAT_MEAT_MODEL'.
meat.Pork.meat_ptr: (fields.E301) Field defines a relation with the model 'meat.Meat', which has been swapped out.
	HINT: Update the relation to point at 'settings.MEAT_MEAT_MODEL'.

This error arises on Django 1.9 to 1.11, but for my purpose only 1.11 is critical.

I tried overriding the meat_ptr as instructed in Multi-table inheritance like so:

from swapper import get_model_name
from django.db.models import OneToOneField, CASCADE

class Pork(Meat):
    meat_ptr = OneToOneField(
        get_model_name("meat", "Meat"), CASCADE,
        parent_link=True)

But it gives me this error on 1.11 and 1.10 (but not 1.9):

django.core.exceptions.FieldError: Auto-generated field 'meat_ptr' in class 'Pork' for parent_link to base class 'Meat' clashes with declared field of the same name.

In conclusion, how do i make this happen?

Inherited swapped model ?

What if I want to write a model that inherit a swapped model?
I can't use

class Child(swapper.get_model_name('reusableapp', 'Parent')):
    ...

because it won't accept a string as model base.
I can't use swapper.load_model(...) neither (as explained in the README).

Do you have any idea on how to do this?
I want people that use my app to be able to add custom fields in a base model that is inherited by other models of my app. I can't ask them to swap all of the models 🐹

Allow loading models with require_ready=False

In a specific case, we have a series of django apps all based on swappable, all models are loaded using swappable except this case:

https://github.com/openwisp/openwisp-monitoring/blob/8ff5b28f2d5db587c50f0680c0994fbce173b2d0/openwisp_monitoring/device/models.py#L4-L9

DeviceData inherits BaseDevice which is a concrete model imported with the classic from .. import.

If we try to load BaseDevice with swappable, the following error is raised:

django.core.exceptions.AppRegistryNotReady: Models aren't loaded yet.

However, I was able to load the model with swappable anyway with a small patch:

BaseDevice = load_model('config', 'Device', require_ready=False)

require_ready would be a new argument that is just passed to django's get_model(), which already has this argument.

I think it's safe to add this argument to load_model, defaulting to False.

PS: require_ready is only available since django 2.0

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.