import logging from django.core.management.base import BaseCommand from apps.bookmark.models import Bookmark logger = logging.getLogger(__name__) class Command(BaseCommand): help = "Find and delete orphan bookmarks whose target content was deleted from the database." def add_arguments(self, parser): parser.add_argument( '--dry-run', action='store_true', help='Scan and report orphan bookmarks without actually deleting them.', ) def handle(self, *args, **options): dry_run = options.get('dry_run', False) from apps.library.models import Book from apps.podcast.models import Podcast, PodcastPlaylist from apps.video.models import Video, VideoPlaylist from apps.hadis.models import Hadis, HadisCorrection from apps.article.models import Article service_model_map = { Bookmark.ServiceChoices.LIBRARY: (Book, 'Library Book'), Bookmark.ServiceChoices.PODCAST: (Podcast, 'Podcast'), Bookmark.ServiceChoices.PODCAST_PLAYLIST: (PodcastPlaylist, 'Podcast Playlist'), Bookmark.ServiceChoices.HADITH: (Hadis, 'Hadith'), Bookmark.ServiceChoices.HADITH_CORRECTION: (HadisCorrection, 'Hadith Correction'), Bookmark.ServiceChoices.VIDEO: (Video, 'Video'), Bookmark.ServiceChoices.VIDEO_PLAYLIST: (VideoPlaylist, 'Video Playlist'), Bookmark.ServiceChoices.ARTICLE: (Article, 'Article'), } total_checked = Bookmark.objects.count() self.stdout.write(f"Scanning {total_checked} total bookmark(s)...") total_orphans = 0 orphan_ids_to_delete = [] for service_choice, (model_cls, label) in service_model_map.items(): bms = Bookmark.objects.filter(service=service_choice) content_ids = set(bms.values_list('content_id', flat=True)) if not content_ids: continue existing_ids = set(model_cls.objects.filter(id__in=content_ids).values_list('id', flat=True)) missing_ids = content_ids - existing_ids if missing_ids: orphan_bms = bms.filter(content_id__in=missing_ids) count = orphan_bms.count() total_orphans += count ids = list(orphan_bms.values_list('id', flat=True)) orphan_ids_to_delete.extend(ids) self.stdout.write( self.style.WARNING(f" - [{label} ({service_choice})]: found {count} orphan bookmark(s) (missing IDs: {sorted(list(missing_ids))[:10]}{'...' if len(missing_ids) > 10 else ''})") ) # Check any bookmarks with unknown service choices known_services = set(service_model_map.keys()) unknown_bms = Bookmark.objects.exclude(service__in=known_services) unknown_count = unknown_bms.count() if unknown_count > 0: total_orphans += unknown_count orphan_ids_to_delete.extend(list(unknown_bms.values_list('id', flat=True))) self.stdout.write( self.style.WARNING(f" - [Unknown Service]: found {unknown_count} bookmark(s) with invalid service name.") ) if total_orphans == 0: self.stdout.write(self.style.SUCCESS("No orphan bookmarks found! Everything is clean.")) return self.stdout.write(f"\nTotal orphan bookmark(s) found: {total_orphans}") if dry_run: self.stdout.write(self.style.NOTICE("Dry run enabled. No bookmarks were deleted.")) else: deleted_count, _ = Bookmark.objects.filter(id__in=orphan_ids_to_delete).delete() self.stdout.write(self.style.SUCCESS(f"Successfully deleted {deleted_count} orphan bookmark(s)!"))