mirror of
https://github.com/django/django.git
synced 2024-12-23 01:25:58 +00:00
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.apps import apps
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.management import BaseCommand
|
from django.core.management import BaseCommand
|
||||||
from django.db import DEFAULT_DB_ALIAS, router
|
from django.db import DEFAULT_DB_ALIAS, router
|
||||||
from django.db.models.deletion import Collector
|
from django.db.models.deletion import Collector
|
||||||
|
|
||||||
from ...management import get_contenttypes_and_models
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|
||||||
@ -18,18 +18,32 @@ class Command(BaseCommand):
|
|||||||
'--database', default=DEFAULT_DB_ALIAS,
|
'--database', default=DEFAULT_DB_ALIAS,
|
||||||
help='Nominates the database to use. Defaults to the "default" database.',
|
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):
|
def handle(self, **options):
|
||||||
db = options['database']
|
db = options['database']
|
||||||
|
include_stale_apps = options['include_stale_apps']
|
||||||
interactive = options['interactive']
|
interactive = options['interactive']
|
||||||
verbosity = options['verbosity']
|
verbosity = options['verbosity']
|
||||||
|
|
||||||
for app_config in apps.get_app_configs():
|
if not router.allow_migrate_model(db, ContentType):
|
||||||
content_types, app_models = get_contenttypes_and_models(app_config, db, ContentType)
|
return
|
||||||
to_remove = [
|
ContentType.objects.clear_cache()
|
||||||
ct for (model_name, ct) in content_types.items()
|
|
||||||
if model_name not in app_models
|
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.
|
# Confirm that the content type is stale before deletion.
|
||||||
using = router.db_for_write(ContentType)
|
using = router.db_for_write(ContentType)
|
||||||
if to_remove:
|
if to_remove:
|
||||||
|
@ -1651,6 +1651,13 @@ the deletion.
|
|||||||
|
|
||||||
Specifies the database to use. Defaults to ``default``.
|
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``
|
``django.contrib.gis``
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
@ -99,7 +99,9 @@ Minor features
|
|||||||
:mod:`django.contrib.contenttypes`
|
: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`
|
:mod:`django.contrib.gis`
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -10,10 +10,15 @@ from django.test.utils import captured_stdout
|
|||||||
from .models import ModelWithNullFKToSite, Post
|
from .models import ModelWithNullFKToSite, Post
|
||||||
|
|
||||||
|
|
||||||
@modify_settings(INSTALLED_APPS={'append': ['no_models']})
|
@modify_settings(INSTALLED_APPS={'append': ['empty_models', 'no_models']})
|
||||||
class RemoveStaleContentTypesTests(TestCase):
|
class RemoveStaleContentTypesTests(TestCase):
|
||||||
# Speed up tests by avoiding retrieving ContentTypes for all test apps.
|
# 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):
|
def setUp(self):
|
||||||
self.before_count = ContentType.objects.count()
|
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)
|
contenttypes_management.create_contenttypes(self.app_config, interactive=False, verbosity=0, apps=apps)
|
||||||
self.assertEqual(ContentType.objects.count(), self.before_count + 1)
|
self.assertEqual(ContentType.objects.count(), self.before_count + 1)
|
||||||
|
|
||||||
def test_contenttypes_removed_in_apps_without_models(self):
|
@modify_settings(INSTALLED_APPS={'remove': ['empty_models']})
|
||||||
ContentType.objects.create(app_label='no_models', model='Fake')
|
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:
|
with mock.patch('builtins.input', return_value='yes'), captured_stdout() as stdout:
|
||||||
call_command('remove_stale_contenttypes', verbosity=2)
|
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)
|
self.assertEqual(ContentType.objects.count(), self.before_count)
|
||||||
|
Loading…
Reference in New Issue
Block a user