From 737d24923ac69bb8b89af1bb2f3f4c4c744349e8 Mon Sep 17 00:00:00 2001 From: Markus Holtermann Date: Mon, 12 Jan 2015 02:52:22 +0100 Subject: [PATCH] Fixed #24075 -- Prevented running post_migrate signals when unapplying initial migrations of contenttypes and auth Thanks Florian Apolloner for the report and Claude Paroz and Tim Graham for the review and help on the patch. --- django/contrib/auth/management/__init__.py | 5 +++++ django/contrib/auth/tests/test_management.py | 20 ++++++++++++++++++- django/contrib/contenttypes/management.py | 5 +++++ django/contrib/contenttypes/tests/tests.py | 21 +++++++++++++++++++- django/db/migrations/loader.py | 12 +++++++++++ docs/releases/1.7.4.txt | 3 ++- 6 files changed, 63 insertions(+), 3 deletions(-) diff --git a/django/contrib/auth/management/__init__.py b/django/contrib/auth/management/__init__.py index 5440e597c1..cf00908cab 100644 --- a/django/contrib/auth/management/__init__.py +++ b/django/contrib/auth/management/__init__.py @@ -11,6 +11,7 @@ from django.contrib.auth import get_permission_codename from django.core import exceptions from django.core.management.base import CommandError from django.db import DEFAULT_DB_ALIAS, router +from django.db.migrations.loader import is_latest_migration_applied from django.utils.encoding import DEFAULT_LOCALE_ENCODING from django.utils import six @@ -58,6 +59,10 @@ def _check_permission_clashing(custom, builtin, ctype): def create_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs): + # TODO: Remove when migration plan / state is passed (#24100). + if not is_latest_migration_applied('auth'): + return + if not app_config.models_module: return diff --git a/django/contrib/auth/tests/test_management.py b/django/contrib/auth/tests/test_management.py index 0c039bd0ec..10d7cb07f1 100644 --- a/django/contrib/auth/tests/test_management.py +++ b/django/contrib/auth/tests/test_management.py @@ -17,7 +17,7 @@ from django.core import checks from django.core import exceptions from django.core.management import call_command from django.core.management.base import CommandError -from django.test import TestCase, override_settings, override_system_checks +from django.test import TestCase, override_settings, override_system_checks, skipUnlessDBFeature from django.utils import six from django.utils.encoding import force_str @@ -570,3 +570,21 @@ class PermissionTestCase(TestCase): six.assertRaisesRegex(self, exceptions.ValidationError, "The verbose_name of permission is longer than 244 characters", create_permissions, auth_app_config, verbosity=0) + + +class MigrateTests(TestCase): + + @skipUnlessDBFeature('can_rollback_ddl') + def test_unmigrating_first_migration_post_migrate_signal(self): + """ + #24075 - When unmigrating an app before its first migration, + post_migrate signal handler must be aware of the missing tables. + """ + try: + with override_settings( + INSTALLED_APPS=["django.contrib.auth", "django.contrib.contenttypes"], + MIGRATION_MODULES={'auth': 'django.contrib.auth.migrations'}, + ): + call_command("migrate", "auth", "zero", stdout=six.StringIO()) + finally: + call_command("migrate", stdout=six.StringIO()) diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py index abcc5b836f..3617dbbcf6 100644 --- a/django/contrib/contenttypes/management.py +++ b/django/contrib/contenttypes/management.py @@ -1,5 +1,6 @@ from django.apps import apps from django.db import DEFAULT_DB_ALIAS, router +from django.db.migrations.loader import is_latest_migration_applied from django.utils.encoding import smart_text from django.utils import six from django.utils.six.moves import input @@ -10,6 +11,10 @@ def update_contenttypes(app_config, verbosity=2, interactive=True, using=DEFAULT Creates content types for models in the given app, removing any model entries that no longer have a matching model class. """ + # TODO: Remove when migration plan / state is passed (#24100). + if not is_latest_migration_applied('contenttypes'): + return + if not app_config.models_module: return diff --git a/django/contrib/contenttypes/tests/tests.py b/django/contrib/contenttypes/tests/tests.py index 924ffdca46..5360b2a59a 100644 --- a/django/contrib/contenttypes/tests/tests.py +++ b/django/contrib/contenttypes/tests/tests.py @@ -3,8 +3,9 @@ from __future__ import unicode_literals from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.views import shortcut from django.contrib.sites.shortcuts import get_current_site +from django.core.management import call_command from django.http import HttpRequest, Http404 -from django.test import TestCase, override_settings +from django.test import TestCase, override_settings, skipUnlessDBFeature from django.utils import six from .models import ConcreteModel, ProxyModel, FooWithoutUrl, FooWithUrl, FooWithBrokenAbsoluteUrl @@ -241,3 +242,21 @@ class ContentTypesTests(TestCase): # Instead, just return the ContentType object and let the app detect stale states. ct_fetched = ContentType.objects.get_for_id(ct.pk) self.assertIsNone(ct_fetched.model_class()) + + +class MigrateTests(TestCase): + + @skipUnlessDBFeature('can_rollback_ddl') + def test_unmigrating_first_migration_post_migrate_signal(self): + """ + #24075 - When unmigrating an app before its first migration, + post_migrate signal handler must be aware of the missing tables. + """ + try: + with override_settings( + INSTALLED_APPS=["django.contrib.contenttypes"], + MIGRATION_MODULES={'contenttypes': 'django.contrib.contenttypes.migrations'}, + ): + call_command("migrate", "contenttypes", "zero", stdout=six.StringIO()) + finally: + call_command("migrate", stdout=six.StringIO()) diff --git a/django/db/migrations/loader.py b/django/db/migrations/loader.py index 9cc428e162..51fb2783eb 100644 --- a/django/db/migrations/loader.py +++ b/django/db/migrations/loader.py @@ -5,6 +5,7 @@ import os import sys from django.apps import apps +from django.db import connection from django.db.migrations.recorder import MigrationRecorder from django.db.migrations.graph import MigrationGraph, NodeNotFoundError from django.utils import six @@ -339,3 +340,14 @@ class AmbiguityError(Exception): Raised when more than one migration matches a name prefix """ pass + + +def is_latest_migration_applied(app_label): + # TODO: Remove when migration plan / state is passed (#24100). + loader = MigrationLoader(connection) + loader.load_disk() + leaf_nodes = loader.graph.leaf_nodes(app=app_label) + return ( + leaf_nodes and leaf_nodes[0] in loader.applied_migrations or + app_label in loader.unmigrated_apps + ) diff --git a/docs/releases/1.7.4.txt b/docs/releases/1.7.4.txt index 379b148488..486baa6e07 100644 --- a/docs/releases/1.7.4.txt +++ b/docs/releases/1.7.4.txt @@ -9,4 +9,5 @@ Django 1.7.4 fixes several bugs in 1.7.3. Bugfixes ======== -* ... +* Fixed a migration crash when unapplying ``contrib.contenttypes``’s or + ``contrib.auth``’s first migration (:ticket:`24075`).