111 lines
4.4 KiB
Python
111 lines
4.4 KiB
Python
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
|
|
|
|
|
|
class Command(BaseCommand):
|
|
def add_arguments(self, parser):
|
|
parser.add_argument(
|
|
"--noinput",
|
|
"--no-input",
|
|
action="store_false",
|
|
dest="interactive",
|
|
help="Tells Django to NOT prompt the user for input of any kind.",
|
|
)
|
|
parser.add_argument(
|
|
"--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"]
|
|
|
|
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:
|
|
if interactive:
|
|
ct_info = []
|
|
for ct in to_remove:
|
|
ct_info.append(
|
|
" - Content type for %s.%s" % (ct.app_label, ct.model)
|
|
)
|
|
collector = NoFastDeleteCollector(using=using, origin=ct)
|
|
collector.collect([ct])
|
|
|
|
for obj_type, objs in collector.data.items():
|
|
if objs != {ct}:
|
|
ct_info.append(
|
|
" - %s %s object(s)"
|
|
% (
|
|
len(objs),
|
|
obj_type._meta.label,
|
|
)
|
|
)
|
|
content_type_display = "\n".join(ct_info)
|
|
self.stdout.write(
|
|
"Some content types in your database are stale and can be "
|
|
"deleted.\n"
|
|
"Any objects that depend on these content types will also be "
|
|
"deleted.\n"
|
|
"The content types and dependent objects that would be deleted "
|
|
"are:\n\n"
|
|
f"{content_type_display}\n\n"
|
|
"This list doesn't include any cascade deletions to data "
|
|
"outside of Django's\n"
|
|
"models (uncommon).\n\n"
|
|
"Are you sure you want to delete these content types?\n"
|
|
"If you're unsure, answer 'no'."
|
|
)
|
|
ok_to_delete = input("Type 'yes' to continue, or 'no' to cancel: ")
|
|
else:
|
|
ok_to_delete = "yes"
|
|
|
|
if ok_to_delete == "yes":
|
|
for ct in to_remove:
|
|
if verbosity >= 2:
|
|
self.stdout.write(
|
|
"Deleting stale content type '%s | %s'"
|
|
% (ct.app_label, ct.model)
|
|
)
|
|
ct.delete()
|
|
else:
|
|
if verbosity >= 2:
|
|
self.stdout.write("Stale content types remain.")
|
|
|
|
|
|
class NoFastDeleteCollector(Collector):
|
|
def can_fast_delete(self, *args, **kwargs):
|
|
"""
|
|
Always load related objects to display them when showing confirmation.
|
|
"""
|
|
return False
|