summaryrefslogtreecommitdiff
path: root/django
diff options
context:
space:
mode:
authorSimon Charette <charette.s@gmail.com>2015-12-17 20:12:05 -0500
committerSimon Charette <charette.s@gmail.com>2016-05-17 12:14:58 -0400
commitf179113e6cbc8ba0a8d4e87e1d4410fb61d63e75 (patch)
tree3d37340aee66ff6b3f492fb81efdb547ab24061f /django
parent354acd04af524ad82002b903df1189581c51cabe (diff)
Fixed #24067 -- Renamed content types upon model renaming.
Thanks to Tim for the extensive review.
Diffstat (limited to 'django')
-rw-r--r--django/contrib/contenttypes/apps.py7
-rw-r--r--django/contrib/contenttypes/management.py84
2 files changed, 88 insertions, 3 deletions
diff --git a/django/contrib/contenttypes/apps.py b/django/contrib/contenttypes/apps.py
index f6c3d430fd..e5708adc99 100644
--- a/django/contrib/contenttypes/apps.py
+++ b/django/contrib/contenttypes/apps.py
@@ -1,10 +1,12 @@
from django.apps import AppConfig
from django.contrib.contenttypes.checks import check_generic_foreign_keys
from django.core import checks
-from django.db.models.signals import post_migrate
+from django.db.models.signals import post_migrate, pre_migrate
from django.utils.translation import ugettext_lazy as _
-from .management import update_contenttypes
+from .management import (
+ inject_rename_contenttypes_operations, update_contenttypes,
+)
class ContentTypesConfig(AppConfig):
@@ -12,5 +14,6 @@ class ContentTypesConfig(AppConfig):
verbose_name = _("Content Types")
def ready(self):
+ pre_migrate.connect(inject_rename_contenttypes_operations, sender=self)
post_migrate.connect(update_contenttypes)
checks.register(check_generic_foreign_keys, checks.Tags.models)
diff --git a/django/contrib/contenttypes/management.py b/django/contrib/contenttypes/management.py
index 7af1a78e36..0bdf8ed9fa 100644
--- a/django/contrib/contenttypes/management.py
+++ b/django/contrib/contenttypes/management.py
@@ -1,9 +1,91 @@
from django.apps import apps as global_apps
-from django.db import DEFAULT_DB_ALIAS, router
+from django.db import DEFAULT_DB_ALIAS, migrations, router, transaction
+from django.db.utils import IntegrityError
from django.utils import six
from django.utils.six.moves import input
+class RenameContentType(migrations.RunPython):
+ def __init__(self, app_label, old_model, new_model):
+ self.app_label = app_label
+ self.old_model = old_model
+ self.new_model = new_model
+ super(RenameContentType, self).__init__(self.rename_forward, self.rename_backward)
+
+ def _rename(self, apps, schema_editor, old_model, new_model):
+ ContentType = apps.get_model('contenttypes', 'ContentType')
+ db = schema_editor.connection.alias
+ if not router.allow_migrate_model(db, ContentType):
+ return
+
+ try:
+ content_type = ContentType.objects.db_manager(db).get_by_natural_key(self.app_label, old_model)
+ except ContentType.DoesNotExist:
+ pass
+ else:
+ content_type.model = new_model
+ try:
+ with transaction.atomic(using=db):
+ content_type.save(update_fields={'model'})
+ except IntegrityError:
+ # Gracefully fallback if a stale content type causes a
+ # conflict as update_contenttypes will take care of asking the
+ # user what should be done next.
+ content_type.model = old_model
+ else:
+ # Clear the cache as the `get_by_natual_key()` call will cache
+ # the renamed ContentType instance by its old model name.
+ ContentType.objects.clear_cache()
+
+ def rename_forward(self, apps, schema_editor):
+ self._rename(apps, schema_editor, self.old_model, self.new_model)
+
+ def rename_backward(self, apps, schema_editor):
+ self._rename(apps, schema_editor, self.new_model, self.old_model)
+
+
+def inject_rename_contenttypes_operations(plan=None, apps=global_apps, using=DEFAULT_DB_ALIAS, **kwargs):
+ """
+ Insert a `RenameContentType` operation after every planned `RenameModel`
+ operation.
+ """
+ if plan is None:
+ return
+
+ # Determine whether or not the ContentType model is available.
+ try:
+ ContentType = apps.get_model('contenttypes', 'ContentType')
+ except LookupError:
+ available = False
+ else:
+ if not router.allow_migrate_model(using, ContentType):
+ return
+ available = True
+
+ for migration, backward in plan:
+ if ((migration.app_label, migration.name) == ('contenttypes', '0001_initial')):
+ # There's no point in going forward if the initial contenttypes
+ # migration is unapplied as the ContentType model will be
+ # unavailable from this point.
+ if backward:
+ break
+ else:
+ available = True
+ continue
+ # The ContentType model is not available yet.
+ if not available:
+ continue
+ inserts = []
+ for index, operation in enumerate(migration.operations):
+ if isinstance(operation, migrations.RenameModel):
+ operation = RenameContentType(
+ migration.app_label, operation.old_name_lower, operation.new_name_lower
+ )
+ inserts.append((index + 1, operation))
+ for inserted, (index, operation) in enumerate(inserts):
+ migration.operations.insert(inserted + index, operation)
+
+
def update_contenttypes(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, apps=global_apps, **kwargs):
"""
Creates content types for models in the given app, removing any model