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:
parent
13993e0f38
commit
142ab6846a
|
@ -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:
|
||||
|
|
|
@ -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``
|
||||
----------------------
|
||||
|
||||
|
|
|
@ -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`
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue