Created
December 19, 2023 04:55
-
-
Save powderflask/f0ae8b9ae6c7bb471f591de5175204cd to your computer and use it in GitHub Desktop.
Django management command to wipe all traces of a django app with models from DB
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
""" | |
Completely remove all traces of a django app from the DB. | |
Suggested workflow: | |
Step 1: carefully remove dependencies on app from code base | |
Step 2: run this command to remove all traces of app model instances and content types from DB | |
Step 3: deploy code changes and this command everywhere (e.g., on stage and production codebase and DB) | |
At some future date... | |
Step 4: remove app from settings.INSTALLED_APPS | |
remove app code from codebase | |
Step 5: deploy new codebase everywhere | |
""" | |
from django.contrib.contenttypes.models import ContentType | |
from django.contrib.auth.models import Permission | |
from django.contrib.admin.models import LogEntry | |
from django.core.management import call_command | |
from django.core.management.base import BaseCommand | |
def confirm(question, default=None): | |
result = input("%s (y/n): " % question) | |
if not result and default is not None: | |
return default | |
while len(result) < 1 or result[0].lower() not in "yn": | |
result = input("Please answer yes or no: ") | |
return result[0].lower() == "y" | |
class Command(BaseCommand ): | |
help = "Completely wipe app from the DB." | |
def add_arguments(self, parser): | |
parser.add_argument('--quiet', action='store_true', help='suppress verbose output') | |
parser.add_argument('app_label', default=None, help='app label of app to remove.') | |
def instructions(self): | |
self.stdout.write("CAUTION: this command will completely remove all traces of the app from the DB.") | |
self.stdout.write(" Before starting...") | |
self.stdout.write(" - ensure all dependencies on the app have been removed from code base;") | |
self.stdout.write(" - carefully check for any remaining DB relations to the app models;") | |
self.stdout.write(" - check for and eliminate any migration dependencies on the app;") | |
self.stdout.write(" - otherwise, leave app installed and code intact (at least apps, models, and migrations);") | |
def handle(self, *args, **options): | |
app_label = options['app_label'] | |
quiet = options.get('quiet', False) | |
if not quiet: | |
self.instructions() | |
if confirm(f'Proceed with removing {app_label}? ', default='n'): | |
self.remove_app(app_label, quiet) | |
elif not quiet: | |
self.stdout.write(" Operation aborted. No changes made.") | |
def remove_app(self, app_label, quiet=False): | |
""" Completely wipe all traces of app_label app from DB """ | |
# pre-fetch all objects related to the app's content types | |
# perms will cascade, but delete them first so whole op. can be in DB | |
content_types = ContentType.objects.filter(app_label=app_label).all() | |
permissions = Permission.objects.filter(content_type_id__in=content_types) | |
log_entries = LogEntry.objects.filter(content_type_id__in=content_types) | |
if not quiet: | |
self.stdout.write(f'Removing all traces of {app_label}, including...') | |
self.stdout.write(f' {content_types.count()} ContentType :') | |
self.stdout.write(f' {[c.model for c in content_types]}') | |
self.stdout.write(f' {permissions.count()} Permission :') | |
self.stdout.write(f' {[p.codename for p in permissions]}') | |
self.stdout.write(f' {log_entries.count()} LogEntry :') | |
self.stdout.write(f' {[l.change_message for l in log_entries]}') | |
go = confirm(f'run `> migrate {app_label} zero`; and delete related content type data?', default='n') | |
if go: | |
# reverse migrations to remove model DB tables and migrations entries | |
call_command('migrate', app_label, 'zero') | |
# delete the content type related data | |
log_entries.delete() | |
permissions.delete() | |
content_types.delete() | |
if not quiet: | |
self.stdout.write(f'{app_label} successfully removed.') | |
self.stdout.write(f'Next steps: deploy this change everywhere; then remove app code at some future date.') | |
elif not quiet: | |
self.stdout.write(" Operation aborted. No changes made.") | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment