Opened 9 years ago

Closed 9 years ago

Last modified 9 years ago

#24291 closed Bug (fixed)

Migrations fail with unused swappable model

Reported by: Marten Kenbeek Owned by: Marten Kenbeek
Component: Migrations Version: 1.8alpha1
Severity: Release blocker Keywords:
Cc: Markus Holtermann Triage Stage: Ready for checkin
Has patch: yes Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

An unused swappable model has no managers, but when trying to migrate the app containing that model, django.db.migrations.state.ModelState.from_model is called. That method expects the model to have at least a _default_manager. As this isn't the case, an AttributeError is raised:

File "manage.py", line 10, in <module>
    execute_from_command_line(sys.argv)
  File "/home/s120374/projects/dev/django/django/core/management/__init__.py", line 330, in execute_from_command_line
    utility.execute()
  File "/home/s120374/projects/dev/django/django/core/management/__init__.py", line 322, in execute
    self.fetch_command(subcommand).run_from_argv(self.argv)
  File "/home/s120374/projects/dev/django/django/core/management/base.py", line 350, in run_from_argv
    self.execute(*args, **cmd_options)
  File "/home/s120374/projects/dev/django/django/core/management/base.py", line 401, in execute
    output = self.handle(*args, **options)
  File "/home/s120374/projects/dev/django/django/core/management/commands/migrate.py", line 173, in handle
    ProjectState.from_apps(apps),
  File "/home/s120374/projects/dev/django/django/db/migrations/state.py", line 94, in from_apps
    model_state = ModelState.from_model(model)
  File "/home/s120374/projects/dev/django/django/db/migrations/state.py", line 348, in from_model
    default_manager_name = model._default_manager.name
AttributeError: type object 'Swappable' has no attribute '_default_manager'

The same happens when making a new migration using makemigrations, and when migrating an app that doesn't contain the swappable model.

Change History (10)

comment:1 by Marten Kenbeek, 9 years ago

Test that currently fails:

diff --git a/tests/migrations/test_state.py b/tests/migrations/test_state.py
index a7dbdbe..ca7a21e 100644
--- a/tests/migrations/test_state.py
+++ b/tests/migrations/test_state.py
@@ -2,7 +2,7 @@
 from django.db import models
 from django.db.migrations.operations import RemoveField
 from django.db.migrations.state import ProjectState, ModelState, InvalidBasesError
-from django.test import TestCase
+from django.test import TestCase, override_settings
 
 from .models import (FoodManager, FoodQuerySet, ModelWithCustomBase,
     NoMigrationFoodManager)
@@ -543,6 +543,37 @@ def test_manager_refer_correct_model_version(self):
         self.assertIsNot(old_model.food_mgr.model, new_model.food_mgr.model)
         self.assertIsNot(old_model.food_qs.model, new_model.food_qs.model)
 
+    @override_settings(TEST_SWAPPABLE_MODEL="migrations.SomeFakeModel")
+    def test_create_swappable(self):
+        """
+        Tests making a ProjectState from an Apps
+        """
+
+        new_apps = Apps(["migrations"])
+
+        class Author(models.Model):
+            name = models.CharField(max_length=255)
+            bio = models.TextField()
+            age = models.IntegerField(blank=True, null=True)
+
+            class Meta:
+                app_label = "migrations"
+                apps = new_apps
+                swappable = "TEST_SWAPPABLE_MODEL"
+
+
+        project_state = ProjectState.from_apps(new_apps)
+        author_state = project_state.models['migrations', 'author']
+
+        self.assertEqual(author_state.app_label, "migrations")
+        self.assertEqual(author_state.name, "Author")
+        self.assertEqual([x for x, y in author_state.fields], ["id", "name", "bio", "age"])
+        self.assertEqual(author_state.fields[1][1].max_length, 255)
+        self.assertEqual(author_state.fields[2][1].null, False)
+        self.assertEqual(author_state.fields[3][1].null, True)
+        self.assertEqual(author_state.options, {"swappable": "TEST_SWAPPABLE_MODEL"})
+        self.assertEqual(author_state.bases, (models.Model, ))
+
 
 class ModelStateTests(TestCase):
     def test_custom_model_base(self):

comment:2 by Marten Kenbeek, 9 years ago

Has patch: set
Owner: changed from nobody to Marten Kenbeek
Status: newassigned

comment:3 by Markus Holtermann, 9 years ago

Cc: Markus Holtermann added
Triage Stage: UnreviewedAccepted

PR: https://github.com/django/django/pull/4071

Can you confirm that this also happens on 1.8? In that case, can you adjust the version on the ticket, please.

comment:4 by Marten Kenbeek, 9 years ago

Version: master1.8alpha1

Yes, the same happens on 1.8.

comment:5 by Markus Holtermann, 9 years ago

Severity: NormalRelease blocker

comment:6 by Tim Graham, 9 years ago

Triage Stage: AcceptedReady for checkin

comment:7 by Markus Holtermann, 9 years ago

Patch needs improvement: set
Triage Stage: Ready for checkinAccepted

comment:8 by Tim Graham, 9 years ago

Patch needs improvement: unset
Triage Stage: AcceptedReady for checkin

comment:9 by Markus Holtermann <info@…>, 9 years ago

Resolution: fixed
Status: assignedclosed

In 15dc8d1c9d3697170a2c59ecaa7a2b4ba58f5990:

Fixed #24291 - Fixed migration ModelState generation with unused swappable models

Swapped out models don't have a _default_manager unless they have
explicitly defined managers. ModelState.from_model() now accounts for
this case and uses an empty list for managers if no explicit managers
are defined and a model is swapped out.

comment:10 by Markus Holtermann <info@…>, 9 years ago

In 84c9b24c5afba524c8dcb4bb5f7c40c43291d132:

[1.8.x] Fixed #24291 - Fixed migration ModelState generation with unused swappable models

Swapped out models don't have a _default_manager unless they have
explicitly defined managers. ModelState.from_model() now accounts for
this case and uses an empty list for managers if no explicit managers
are defined and a model is swapped out.

Backport of 15dc8d1c9d3697170a2c59ecaa7a2b4ba58f5990 from master

Note: See TracTickets for help on using tickets.
Back to Top