Fixed #31123 -- Added --include-stale-apps option to the remove_stale_contenttypes management command.

Co-Authored-By: Javier Buzzi <buzzi.javier@gmail.com>
This commit is contained in:
gowthamk63 2020-03-11 15:14:50 -04:00 committed by Mariusz Felisiak
parent 13993e0f38
commit 142ab6846a
4 changed files with 67 additions and 14 deletions

View File

@ -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:

View File

@ -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``
----------------------

View File

@ -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`
~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -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)