Browse Source
feat(bookmarks): add auto-delete signals on content deletion and cleanup command
master
feat(bookmarks): add auto-delete signals on content deletion and cleanup command
master
4 changed files with 180 additions and 8 deletions
-
4apps/bookmark/apps.py
-
84apps/bookmark/management/commands/cleanup_orphan_bookmarks.py
-
81apps/bookmark/signals.py
-
19apps/bookmark/views/bookmark.py
@ -0,0 +1,84 @@ |
|||
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)!")) |
|||
@ -0,0 +1,81 @@ |
|||
import logging |
|||
from django.db.models.signals import post_delete |
|||
from django.dispatch import receiver |
|||
|
|||
from apps.bookmark.models import Bookmark |
|||
|
|||
logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
def _delete_bookmarks_for(service: str, content_id: int): |
|||
if not content_id: |
|||
return |
|||
try: |
|||
deleted_count, _ = Bookmark.objects.filter(service=service, content_id=content_id).delete() |
|||
if deleted_count > 0: |
|||
logger.info("[Bookmark] Auto-deleted %d bookmark(s) for %s ID %s upon content deletion.", deleted_count, service, content_id) |
|||
except Exception as exc: |
|||
logger.error("[Bookmark] Error deleting bookmarks for %s ID %s: %s", service, content_id, exc) |
|||
|
|||
|
|||
# 1. Library Books |
|||
try: |
|||
from apps.library.models import Book |
|||
|
|||
@receiver(post_delete, sender=Book) |
|||
def on_book_deleted(sender, instance, **kwargs): |
|||
_delete_bookmarks_for(Bookmark.ServiceChoices.LIBRARY, instance.id) |
|||
except ImportError: |
|||
pass |
|||
|
|||
# 2. Hadith & Hadith Corrections |
|||
try: |
|||
from apps.hadis.models import Hadis, HadisCorrection |
|||
|
|||
@receiver(post_delete, sender=Hadis) |
|||
def on_hadis_deleted(sender, instance, **kwargs): |
|||
_delete_bookmarks_for(Bookmark.ServiceChoices.HADITH, instance.id) |
|||
|
|||
@receiver(post_delete, sender=HadisCorrection) |
|||
def on_hadis_correction_deleted(sender, instance, **kwargs): |
|||
_delete_bookmarks_for(Bookmark.ServiceChoices.HADITH_CORRECTION, instance.id) |
|||
except ImportError: |
|||
pass |
|||
|
|||
# 3. Videos & Video Playlists |
|||
try: |
|||
from apps.video.models import Video, VideoPlaylist |
|||
|
|||
@receiver(post_delete, sender=Video) |
|||
def on_video_deleted(sender, instance, **kwargs): |
|||
_delete_bookmarks_for(Bookmark.ServiceChoices.VIDEO, instance.id) |
|||
|
|||
@receiver(post_delete, sender=VideoPlaylist) |
|||
def on_video_playlist_deleted(sender, instance, **kwargs): |
|||
_delete_bookmarks_for(Bookmark.ServiceChoices.VIDEO_PLAYLIST, instance.id) |
|||
except ImportError: |
|||
pass |
|||
|
|||
# 4. Podcasts & Podcast Playlists |
|||
try: |
|||
from apps.podcast.models import Podcast, PodcastPlaylist |
|||
|
|||
@receiver(post_delete, sender=Podcast) |
|||
def on_podcast_deleted(sender, instance, **kwargs): |
|||
_delete_bookmarks_for(Bookmark.ServiceChoices.PODCAST, instance.id) |
|||
|
|||
@receiver(post_delete, sender=PodcastPlaylist) |
|||
def on_podcast_playlist_deleted(sender, instance, **kwargs): |
|||
_delete_bookmarks_for(Bookmark.ServiceChoices.PODCAST_PLAYLIST, instance.id) |
|||
except ImportError: |
|||
pass |
|||
|
|||
# 5. Articles |
|||
try: |
|||
from apps.article.models import Article |
|||
|
|||
@receiver(post_delete, sender=Article) |
|||
def on_article_deleted(sender, instance, **kwargs): |
|||
_delete_bookmarks_for(Bookmark.ServiceChoices.ARTICLE, instance.id) |
|||
except ImportError: |
|||
pass |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue