Browse Source

feat(bookmarks): add auto-delete signals on content deletion and cleanup command

master
Mohsen Taba 4 days ago
parent
commit
a0a67be5ff
  1. 4
      apps/bookmark/apps.py
  2. 84
      apps/bookmark/management/commands/cleanup_orphan_bookmarks.py
  3. 81
      apps/bookmark/signals.py
  4. 19
      apps/bookmark/views/bookmark.py

4
apps/bookmark/apps.py

@ -4,3 +4,7 @@ from django.apps import AppConfig
class BookmarkConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.bookmark'
def ready(self):
import apps.bookmark.signals # noqa: F401

84
apps/bookmark/management/commands/cleanup_orphan_bookmarks.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)!"))

81
apps/bookmark/signals.py

@ -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

19
apps/bookmark/views/bookmark.py

@ -287,12 +287,16 @@ def _get_valid_content_ids(service, content_ids):
elif service in ('article', 'blog'):
from apps.article.models import Article
return set(Article.objects.filter(id__in=content_ids, status=True).values_list('id', flat=True))
elif service in ('hadith', 'hadith_correction'):
from apps.hadith.models import Hadith
return set(Hadith.objects.filter(id__in=content_ids, status=True).values_list('id', flat=True))
except Exception:
pass
return set(content_ids)
elif service == 'hadith':
from apps.hadis.models import Hadis
return set(Hadis.objects.filter(id__in=content_ids, status=True).values_list('id', flat=True))
elif service == 'hadith_correction':
from apps.hadis.models import HadisCorrection
return set(HadisCorrection.objects.filter(id__in=content_ids).values_list('id', flat=True))
except Exception as exc:
import logging
logging.getLogger(__name__).warning("[Bookmark] Error validating content ids for service %s: %s", service, exc)
return set()
class BookmarkStatusView(APIView):
@ -386,8 +390,7 @@ class BookmarkListView(APIView):
if b.content_id in valid_ids:
valid_bookmarks.append(b)
else:
b.status = False
b.save(update_fields=['status'])
b.delete()
serializer = BookmarkSerializer(valid_bookmarks, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)

Loading…
Cancel
Save