Loading django/contrib/contenttypes/management.py +37 −9 Original line number Diff line number Diff line 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 Loading Loading @@ -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 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) ) ok_to_delete = input("""The following content types are stale and need to be deleted: 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 Loading @@ -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 docs/releases/1.11.txt +4 −1 Original line number Diff line number Diff line Loading @@ -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` ~~~~~~~~~~~~~~~~~~~~~~~~~ Loading tests/contenttypes_tests/tests.py +18 −4 Original line number Diff line number Diff line Loading @@ -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, ) Loading Loading @@ -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: Loading Loading
django/contrib/contenttypes/management.py +37 −9 Original line number Diff line number Diff line 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 Loading Loading @@ -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 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) ) ok_to_delete = input("""The following content types are stale and need to be deleted: 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 Loading @@ -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
docs/releases/1.11.txt +4 −1 Original line number Diff line number Diff line Loading @@ -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` ~~~~~~~~~~~~~~~~~~~~~~~~~ Loading
tests/contenttypes_tests/tests.py +18 −4 Original line number Diff line number Diff line Loading @@ -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, ) Loading Loading @@ -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: Loading