Fixed #18682 -- Expanded explanation in stale content type deletion. (#6869)

This commit is contained in:
Erik Romijn 2016-07-03 15:55:14 +02:00 committed by Florian Apolloner
parent f7e00b40c5
commit 8db889eaf7
3 changed files with 59 additions and 14 deletions

View File

@ -1,5 +1,6 @@
from django.apps import apps as global_apps
from django.db import DEFAULT_DB_ALIAS, migrations, router, transaction
from django.db.models.deletion import Collector
from django.db.utils import IntegrityError
from django.utils import six
from django.utils.six.moves import input
@ -141,21 +142,39 @@ def update_contenttypes(app_config, verbosity=2, interactive=True, using=DEFAULT
print("Adding content type '%s | %s'" % (ct.app_label, ct.model))
# Confirm that the content type is stale before deletion.
using = router.db_for_write(ContentType)
if to_remove:
if interactive:
content_type_display = '\n'.join(
' %s | %s' % (ct.app_label, ct.model)
for ct in to_remove
)
ok_to_delete = input("""The following content types are stale and need to be deleted:
ct_info = []
for ct in to_remove:
ct_info.append(' - Content type for %s.%s' % (ct.app_label, ct.model))
collector = NoFastDeleteCollector(using=using)
collector.collect([ct])
for obj_type, objs in collector.data.items():
if objs == {ct}:
continue
ct_info.append(' - %s object%s of type %s.%s:' % (
len(objs),
's' if len(objs) != 1 else '',
obj_type._meta.app_label,
obj_type._meta.model_name)
)
content_type_display = '\n'.join(ct_info)
print("""Some content types in your database are stale and can be deleted.
Any objects that depend on these content types will then also be deleted.
The content types, and the dependent objects that would be deleted, are:
%s
Any objects related to these content types by a foreign key will also
be deleted. Are you sure you want to delete these content types?
If you're unsure, answer 'no'.
This list does not include data that might be in your database
outside of Django's models.
Type 'yes' to continue, or 'no' to cancel: """ % content_type_display)
Are you sure you want to delete these content types?
If you're unsure, answer 'no'.
""" % content_type_display)
ok_to_delete = input("Type 'yes' to continue, or 'no' to cancel: ")
else:
ok_to_delete = False
@ -167,3 +186,12 @@ If you're unsure, answer 'no'.
else:
if verbosity >= 2:
print("Stale content types remain.")
class NoFastDeleteCollector(Collector):
def can_fast_delete(self, *args, **kwargs):
"""
We always want to load the objects into memory so that we can display
them to the user when asking confirmation.
"""
return False

View File

@ -73,7 +73,10 @@ Minor features
:mod:`django.contrib.contenttypes`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* ...
* When stale content types are detected during a management command, there is
now an expansive list of objects that will be deleted. Previously, only
the content type objects themselves were listed, even if there were objects
with foreign keys towards the content types that would be deleted also.
:mod:`django.contrib.gis`
~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -20,7 +20,7 @@ from django.test.utils import captured_stdout, isolate_apps
from django.utils.encoding import force_str, force_text
from .models import (
Article, Author, ModelWithNullFKToSite, SchemeIncludedURL,
Article, Author, ModelWithNullFKToSite, Post, SchemeIncludedURL,
Site as MockSite,
)
@ -383,13 +383,27 @@ class GenericRelationshipTests(SimpleTestCase):
class UpdateContentTypesTests(TestCase):
def setUp(self):
self.before_count = ContentType.objects.count()
ContentType.objects.create(app_label='contenttypes_tests', model='Fake')
self.content_type = ContentType.objects.create(app_label='contenttypes_tests', model='Fake')
self.app_config = apps.get_app_config('contenttypes_tests')
def test_interactive_true(self):
def test_interactive_true_with_dependent_objects(self):
"""
interactive mode of update_contenttypes() (the default) should delete
stale contenttypes.
stale contenttypes and warn of dependent objects
"""
Post.objects.create(title='post', content_type=self.content_type)
contenttypes_management.input = lambda x: force_str("yes")
with captured_stdout() as stdout:
contenttypes_management.update_contenttypes(self.app_config)
self.assertEqual(Post.objects.count(), 0)
self.assertIn("1 object of type contenttypes_tests.post:", stdout.getvalue())
self.assertIn("Deleting stale content type", stdout.getvalue())
self.assertEqual(ContentType.objects.count(), self.before_count)
def test_interactive_true_without_dependent_objects(self):
"""
interactive mode of update_contenttypes() (the default) should delete
stale contenttypes and inform there are no dependent objects
"""
contenttypes_management.input = lambda x: force_str("yes")
with captured_stdout() as stdout: