From 85b11c31a76c94462cb25a26d53fdf8ed03c2c89 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Sun, 18 Oct 2026 15:57:47 +0330 Subject: [PATCH] feat(hadis): add bulk assign topics action to admin viewset - Add bulk-assign-topics endpoint to AdminHadisViewSet supporting add/replace modes - Validate leaf topic constraints and handle both M2M categories and primary category_id - Invalidate hadis cache on bulk updates and return updated count with category details - Expand AdminHadisBriefSerializer with slug, title_narrator, and status fields --- apps/hadis/serializers/serializers_admin.py | 3 +- apps/hadis/views_admin.py | 101 ++++++++++++++++++++ 2 files changed, 103 insertions(+), 1 deletion(-) diff --git a/apps/hadis/serializers/serializers_admin.py b/apps/hadis/serializers/serializers_admin.py index 9ec9611..3a02e76 100644 --- a/apps/hadis/serializers/serializers_admin.py +++ b/apps/hadis/serializers/serializers_admin.py @@ -782,7 +782,8 @@ class AdminHadisDetailSerializer(serializers.ModelSerializer): class AdminHadisBriefSerializer(serializers.ModelSerializer): class Meta: model = Hadis - fields = ["id", "number", "title"] + fields = ["id", "number", "slug", "title", "title_narrator", "status"] + class AdminHadisCategoryBriefSerializer(serializers.ModelSerializer): diff --git a/apps/hadis/views_admin.py b/apps/hadis/views_admin.py index eff2fb4..11e80cd 100644 --- a/apps/hadis/views_admin.py +++ b/apps/hadis/views_admin.py @@ -443,6 +443,107 @@ class AdminHadisViewSet(ModelViewSet): return queryset.order_by("-id") + @action(detail=False, methods=["post"], url_path="bulk-assign-topics") + def bulk_assign_topics(self, request): + from django.db import transaction + from rest_framework import status as drf_status + from .signals import invalidate_hadis_cache + + hadis_ids = request.data.get("hadis_ids") or [] + category_ids = request.data.get("category_ids") or [] + mode = request.data.get("mode", "add") + + if not isinstance(hadis_ids, list) or not hadis_ids: + return Response( + {"detail": "hadis_ids must be a non-empty list."}, + status=drf_status.HTTP_400_BAD_REQUEST, + ) + if not isinstance(category_ids, list) or not category_ids: + return Response( + {"detail": "category_ids must be a non-empty list."}, + status=drf_status.HTTP_400_BAD_REQUEST, + ) + + try: + hadis_ids = [int(hid) for hid in hadis_ids] + category_ids = list(dict.fromkeys(int(cid) for cid in category_ids)) + except (ValueError, TypeError): + return Response( + {"detail": "Invalid ID format."}, + status=drf_status.HTTP_400_BAD_REQUEST, + ) + + categories = list(HadisCategory.objects.filter(id__in=category_ids)) + if len(categories) != len(category_ids): + return Response( + {"detail": "One or more selected topics do not exist."}, + status=drf_status.HTTP_400_BAD_REQUEST, + ) + + for cat in categories: + if cat.children.exists(): + return Response( + {"detail": "Cannot assign items to a parent category that has subcategories. Please select leaf topics only."}, + status=drf_status.HTTP_400_BAD_REQUEST, + ) + + hadises_qs = Hadis.objects.filter(id__in=hadis_ids) + existing_hadis_ids = list(hadises_qs.values_list("id", flat=True)) + if not existing_hadis_ids: + return Response( + {"detail": "No matching items found."}, + status=drf_status.HTTP_404_NOT_FOUND, + ) + + through_model = Hadis.categories.through + first_category_id = category_ids[0] + + with transaction.atomic(): + if mode == "replace": + through_model.objects.filter(hadis_id__in=existing_hadis_ids).delete() + relations = [ + through_model(hadis_id=hid, hadiscategory_id=cid) + for hid in existing_hadis_ids + for cid in category_ids + ] + through_model.objects.bulk_create(relations, ignore_conflicts=True) + Hadis.objects.filter(id__in=existing_hadis_ids).update(category_id=first_category_id) + else: + relations = [ + through_model(hadis_id=hid, hadiscategory_id=cid) + for hid in existing_hadis_ids + for cid in category_ids + ] + through_model.objects.bulk_create(relations, ignore_conflicts=True) + Hadis.objects.filter(id__in=existing_hadis_ids, category__isnull=True).update( + category_id=first_category_id + ) + + invalidate_hadis_cache() + + cat_map = {c.id: c for c in categories} + ordered_cats = [cat_map[cid] for cid in category_ids if cid in cat_map] + categories_detail = [ + { + "id": c.id, + "title": c.title, + "slug": c.slug, + "source_type": c.source_type, + "is_topic": True, + } + for c in ordered_cats + ] + + return Response( + { + "updated_count": len(existing_hadis_ids), + "mode": mode, + "categories_detail": categories_detail, + }, + status=drf_status.HTTP_200_OK, + ) + + class AdminHadisCorrectionViewSet(ModelViewSet): serializer_class = AdminHadisCorrectionSerializer