summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgowthamk63 <gowthamk63@gmail.com>2020-03-11 15:14:50 -0400
committerMariusz Felisiak <felisiak.mariusz@gmail.com>2020-03-19 11:48:10 +0100
commit142ab6846ac09d6d401e26fc8b6b988a583ac0f5 (patch)
treed12a79a5db7e68efc7449ce85e3339c20f60f2af
parent13993e0f38d5f272236887ef22b491eb1b20dce9 (diff)
Fixed #31123 -- Added --include-stale-apps option to the remove_stale_contenttypes management command.
Co-Authored-By: Javier Buzzi <buzzi.javier@gmail.com>
-rw-r--r--django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py30
-rw-r--r--docs/ref/django-admin.txt7
-rw-r--r--docs/releases/3.1.txt4
-rw-r--r--tests/contenttypes_tests/test_management.py40
4 files changed, 67 insertions, 14 deletions
diff --git a/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py b/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
index 4d282d524e..b1f966b032 100644
--- a/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
+++ b/django/contrib/contenttypes/management/commands/remove_stale_contenttypes.py
@@ -1,11 +1,11 @@
+import itertools
+
from django.apps import apps
from django.contrib.contenttypes.models import ContentType
from django.core.management import BaseCommand
from django.db import DEFAULT_DB_ALIAS, router
from django.db.models.deletion import Collector
-from ...management import get_contenttypes_and_models
-
class Command(BaseCommand):
@@ -18,18 +18,32 @@ class Command(BaseCommand):
'--database', default=DEFAULT_DB_ALIAS,
help='Nominates the database to use. Defaults to the "default" database.',
)
+ parser.add_argument(
+ '--include-stale-apps', action='store_true', default=False,
+ help=(
+ "Deletes stale content types including ones from previously "
+ "installed apps that have been removed from INSTALLED_APPS."
+ ),
+ )
def handle(self, **options):
db = options['database']
+ include_stale_apps = options['include_stale_apps']
interactive = options['interactive']
verbosity = options['verbosity']
- for app_config in apps.get_app_configs():
- content_types, app_models = get_contenttypes_and_models(app_config, db, ContentType)
- to_remove = [
- ct for (model_name, ct) in content_types.items()
- if model_name not in app_models
- ]
+ if not router.allow_migrate_model(db, ContentType):
+ return
+ ContentType.objects.clear_cache()
+
+ apps_content_types = itertools.groupby(
+ ContentType.objects.using(db).order_by('app_label', 'model'),
+ lambda obj: obj.app_label,
+ )
+ for app_label, content_types in apps_content_types:
+ if not include_stale_apps and app_label not in apps.app_configs:
+ continue
+ to_remove = [ct for ct in content_types if ct.model_class() is None]
# Confirm that the content type is stale before deletion.
using = router.db_for_write(ContentType)
if to_remove:
diff --git a/docs/ref/django-admin.txt b/docs/ref/django-admin.txt
index a5ef671e26..56358b4ef8 100644
--- a/docs/ref/django-admin.txt
+++ b/docs/ref/django-admin.txt
@@ -1651,6 +1651,13 @@ the deletion.
Specifies the database to use. Defaults to ``default``.
+.. django-admin-option:: --include-stale-apps
+
+.. versionadded:: 3.1
+
+Deletes stale content types including ones from previously installed apps that
+have been removed from :setting:`INSTALLED_APPS`. Defaults to ``False``.
+
``django.contrib.gis``
----------------------
diff --git a/docs/releases/3.1.txt b/docs/releases/3.1.txt
index efc57e8d01..77a71bbc43 100644
--- a/docs/releases/3.1.txt
+++ b/docs/releases/3.1.txt
@@ -99,7 +99,9 @@ Minor features
:mod:`django.contrib.contenttypes`
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* ...
+* The new :option:`remove_stale_contenttypes --include-stale-apps` option
+ allows removing stale content types from previously installed apps that have
+ been removed from :setting:`INSTALLED_APPS`.
:mod:`django.contrib.gis`
~~~~~~~~~~~~~~~~~~~~~~~~~
diff --git a/tests/contenttypes_tests/test_management.py b/tests/contenttypes_tests/test_management.py
index 3e375518c9..65842aa09e 100644
--- a/tests/contenttypes_tests/test_management.py
+++ b/tests/contenttypes_tests/test_management.py
@@ -10,10 +10,15 @@ from django.test.utils import captured_stdout
from .models import ModelWithNullFKToSite, Post
-@modify_settings(INSTALLED_APPS={'append': ['no_models']})
+@modify_settings(INSTALLED_APPS={'append': ['empty_models', 'no_models']})
class RemoveStaleContentTypesTests(TestCase):
# Speed up tests by avoiding retrieving ContentTypes for all test apps.
- available_apps = ['contenttypes_tests', 'no_models', 'django.contrib.contenttypes']
+ available_apps = [
+ 'contenttypes_tests',
+ 'empty_models',
+ 'no_models',
+ 'django.contrib.contenttypes',
+ ]
def setUp(self):
self.before_count = ContentType.objects.count()
@@ -65,9 +70,34 @@ class RemoveStaleContentTypesTests(TestCase):
contenttypes_management.create_contenttypes(self.app_config, interactive=False, verbosity=0, apps=apps)
self.assertEqual(ContentType.objects.count(), self.before_count + 1)
- def test_contenttypes_removed_in_apps_without_models(self):
- ContentType.objects.create(app_label='no_models', model='Fake')
+ @modify_settings(INSTALLED_APPS={'remove': ['empty_models']})
+ def test_contenttypes_removed_in_installed_apps_without_models(self):
+ ContentType.objects.create(app_label='empty_models', model='Fake 1')
+ ContentType.objects.create(app_label='no_models', model='Fake 2')
with mock.patch('builtins.input', return_value='yes'), captured_stdout() as stdout:
call_command('remove_stale_contenttypes', verbosity=2)
- self.assertIn("Deleting stale content type 'no_models | Fake'", stdout.getvalue())
+ self.assertNotIn(
+ "Deleting stale content type 'empty_models | Fake 1'",
+ stdout.getvalue(),
+ )
+ self.assertIn(
+ "Deleting stale content type 'no_models | Fake 2'",
+ stdout.getvalue(),
+ )
+ self.assertEqual(ContentType.objects.count(), self.before_count + 1)
+
+ @modify_settings(INSTALLED_APPS={'remove': ['empty_models']})
+ def test_contenttypes_removed_for_apps_not_in_installed_apps(self):
+ ContentType.objects.create(app_label='empty_models', model='Fake 1')
+ ContentType.objects.create(app_label='no_models', model='Fake 2')
+ with mock.patch('builtins.input', return_value='yes'), captured_stdout() as stdout:
+ call_command('remove_stale_contenttypes', include_stale_apps=True, verbosity=2)
+ self.assertIn(
+ "Deleting stale content type 'empty_models | Fake 1'",
+ stdout.getvalue(),
+ )
+ self.assertIn(
+ "Deleting stale content type 'no_models | Fake 2'",
+ stdout.getvalue(),
+ )
self.assertEqual(ContentType.objects.count(), self.before_count)