Created
April 28, 2022 09:16
-
-
Save pymen/3e222db46ddca9e4abcfefc5fb4903b6 to your computer and use it in GitHub Desktop.
remove_stale_contenttypes
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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( | |
'--remove', 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'] | |
remove = options['remove'] | |
handle(db=db, interactive=True, verbosity=3, remove=remove) | |
class NoFastDeleteCollector(Collector): | |
def can_fast_delete(self, *args, **kwargs): | |
""" | |
Always load related objects to display them when showing confirmation. | |
""" | |
return False | |
def handle(db='default', verbosity=3, interactive=True, remove=False): | |
if not router.allow_migrate_model(db, ContentType): | |
print('Router declines 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 app_label not in apps.app_configs: | |
# print('to delete ') | |
# continue | |
to_remove = [] | |
for ct in content_types: | |
try: | |
model_class = ct.model_class() | |
except (LookupError, KeyError): | |
if remove: | |
ct.delete() | |
print(f'Removed record {ct.app_label=} {ct.model=}') | |
else: | |
print(f'Need to remove record {ct.app_label=} {ct.model=}') | |
else: | |
if model_class is None: | |
print(f'Add to autoremove record {ct.app_label=} {ct.model=}') | |
to_remove.append(ct) | |
# 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) | |
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) | |
print("""Some content types in your database are stale and can be deleted. | |
Any objects that depend on these content types will also be deleted. | |
The content types and dependent objects that would be deleted are: | |
%s | |
This list doesn't include any cascade deletions to data outside of Django's | |
models (uncommon). | |
Are you sure you want to delete these content types? | |
If you're unsure, answer 'no'.""" % content_type_display) | |
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: | |
print("Deleting stale content type '%s | %s'" % (ct.app_label, ct.model)) | |
ct.delete() | |
else: | |
if verbosity >= 2: | |
print("Stale content types remain.") |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment