From 44f06762c1fdb96ef134a470f12d8a2269804a34 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Tue, 1 Sep 2026 09:47:03 +0330 Subject: [PATCH] feat(hadis, reference): filter empty editions & categories in client APIs and order admin transmitters descending --- apps/hadis/serializers/reference_v2.py | 16 +++- apps/hadis/views/category.py | 103 +++++++++++++++++-------- apps/hadis/views/hadis.py | 20 ++++- apps/hadis/views_admin.py | 2 +- 4 files changed, 101 insertions(+), 40 deletions(-) diff --git a/apps/hadis/serializers/reference_v2.py b/apps/hadis/serializers/reference_v2.py index 0ee6b45..01ec847 100644 --- a/apps/hadis/serializers/reference_v2.py +++ b/apps/hadis/serializers/reference_v2.py @@ -172,7 +172,8 @@ class BookReferenceV2DetailSerializer(serializers.ModelSerializer): def get_information(self, obj): request = self.context.get("request") summary = get_localized_text(obj.description, request) if obj.description else "" - editions = BookEditionV2Serializer(obj.editions.all(), many=True, context=self.context).data + valid_editions = [ed for ed in obj.editions.all() if ed.volumes.exists()] + editions = BookEditionV2Serializer(valid_editions, many=True, context=self.context).data return { "summary": summary, @@ -184,10 +185,13 @@ class BookReferenceV2DetailSerializer(serializers.ModelSerializer): editions = obj.editions.all() editions_data = [] for ed in editions: + vol_qs = ed.volumes.all() + if not vol_qs.exists(): + continue editions_data.append({ "edition_id": ed.id, "edition_statement": ed.edition_number or f"Edition {ed.id}", - "volumes": BookVolumeV2Serializer(ed.volumes.all(), many=True, context=self.context).data + "volumes": BookVolumeV2Serializer(vol_qs, many=True, context=self.context).data }) return { "has_editions": True, @@ -226,7 +230,8 @@ class BookReferenceV2SyncSerializer(serializers.ModelSerializer): def get_information(self, obj): request = self.context.get("request") summary = get_localized_text(obj.description, request) if obj.description else "" - editions = BookEditionV2Serializer(obj.editions.all(), many=True, context=self.context).data + valid_editions = [ed for ed in obj.editions.all() if ed.volumes.exists()] + editions = BookEditionV2Serializer(valid_editions, many=True, context=self.context).data return { "summary": summary, "editions": editions, @@ -244,10 +249,13 @@ class BookReferenceV2SyncSerializer(serializers.ModelSerializer): editions = obj.editions.all() editions_data = [] for ed in editions: + vol_qs = ed.volumes.all() + if not vol_qs.exists(): + continue editions_data.append({ "edition_id": ed.id, "edition_statement": ed.edition_number or f"Edition {ed.id}", - "volumes": BookVolumeV2Serializer(ed.volumes.all(), many=True, context=self.context).data + "volumes": BookVolumeV2Serializer(vol_qs, many=True, context=self.context).data }) return { "has_editions": True, diff --git a/apps/hadis/views/category.py b/apps/hadis/views/category.py index 32ba87b..2c38fb1 100644 --- a/apps/hadis/views/category.py +++ b/apps/hadis/views/category.py @@ -136,6 +136,18 @@ class HadisCategoryTreeView(ListAPIView): for root in roots: sect_type = root.sect.sect_type + # Build tree using mapping (returns None if the root branch is completely empty) + category_data = self._build_tree_recursive( + root, + request, + children_map, + recursive_counts, + hadis_ids_map, + quran_data_map, + ) + if category_data is None: + continue + if sect_type not in grouped_data: grouped_data[sect_type] = { 'sects': {}, @@ -158,15 +170,6 @@ class HadisCategoryTreeView(ListAPIView): 'source_types': list(source_types) } - # Build tree using mapping - category_data = self._build_tree_recursive( - root, - request, - children_map, - recursive_counts, - hadis_ids_map, - quran_data_map, # 👇 پاس دادن مپِ قرآن به تابع بازگشتی - ) grouped_data[sect_type]['categories'].append(category_data) total_count = len(queryset) @@ -180,10 +183,28 @@ class HadisCategoryTreeView(ListAPIView): def _build_tree_recursive(self, category, request, children_map, recursive_counts, hadis_ids_map, quran_data_map): """ - Build tree from flat mapping + Build tree from flat mapping, pruning any node that has no active children and no active hadiths. """ - children = children_map.get(category.id, []) + raw_children = children_map.get(category.id, []) + built_children = [] + for child in raw_children: + child_node = self._build_tree_recursive( + child, + request, + children_map, + recursive_counts, + hadis_ids_map, + quran_data_map, + ) + if child_node is not None: + built_children.append(child_node) + has_hadis = category.direct_hadis_count > 0 + has_quran = bool(quran_data_map.get(category.id, None)) if category.source_type == HadisCategory.SourceType.QURAN else False + + # If this category has no children and no hadiths (and no quran verse), prune it + if len(built_children) == 0 and not has_hadis and not has_quran: + return None return { 'id': category.id, @@ -198,23 +219,12 @@ class HadisCategoryTreeView(ListAPIView): 'has_hadis': has_hadis, 'has_parent': category.parent_id is not None, 'hadis_ids': hadis_ids_map.get(category.id, []) if has_hadis else [], - # 👇 کلید جدید: تزریق دیتای قرآن دقیقاً داخل نودِ کتگوری 'quran_data': quran_data_map.get(category.id, None) if category.source_type == HadisCategory.SourceType.QURAN else None, 'order': category.order, 'thumbnail': self._get_thumbnail_url(category, request), 'xmind_file': self._get_xmind_url(category, request), 'has_xmind_file': bool(getattr(category, 'xmind_file', None)), - 'children': [ - self._build_tree_recursive( - child, - request, - children_map, - recursive_counts, - hadis_ids_map, - quran_data_map, # 👇 - ) - for child in children - ] + 'children': built_children } def _get_thumbnail_url(self, category, request): @@ -244,9 +254,15 @@ class HadisCategoryTreeNormalView(ListAPIView): return self.list(request, *args, **kwargs) def get_queryset(self): + from django.db.models import Count, Q return HadisCategory.objects.filter( parent__isnull=True, sect__is_active=True + ).annotate( + active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), + children_total_count=Count('children') + ).filter( + Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) ).order_by('sect__order', 'order') @@ -293,8 +309,6 @@ class HadisCategorySelectBySectView(ListAPIView): def get_queryset(self): sect_type = self.kwargs.get('sect_type') slug = self.kwargs.get('slug') - print(slug) - print(sect_type) # Find the parent category by slug and sect_type try: @@ -304,14 +318,19 @@ class HadisCategorySelectBySectView(ListAPIView): sect__is_active=True ) except HadisCategory.DoesNotExist: - print('not ok') return HadisCategory.objects.none() - # Return children of this category, filtered as before + from django.db.models import Count, Q + # Return children of this category that have either active hadiths, children, or are quran return HadisCategory.objects.filter( parent=parent_category, sect__sect_type=sect_type, sect__is_active=True + ).annotate( + active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), + children_total_count=Count('children') + ).filter( + Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) ).order_by('order') @@ -369,39 +388,61 @@ class HadisCategorySelectBySectSourceView(ListAPIView): except HadisCategory.DoesNotExist: return HadisCategory.objects.none() - # Return children of this category, filtered by source_type + from django.db.models import Count, Q + # Return children of this category, filtered by source_type and excluding empty leaves return HadisCategory.objects.filter( parent=parent_category, sect__sect_type=sect_type, sect__is_active=True, source_type=source_type + ).annotate( + active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), + children_total_count=Count('children') + ).filter( + Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) ).order_by('order') class CategoriesView(ListAPIView): """ - API view to list all HadisCategories + API view to list all HadisCategories (excluding empty leaf categories) """ - queryset = HadisCategory.objects.all() serializer_class = CategorySerializer pagination_class = StandardResultsSetPagination @categories_list_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) + def get_queryset(self): + from django.db.models import Count, Q + return HadisCategory.objects.filter( + sect__is_active=True + ).annotate( + active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), + children_total_count=Count('children') + ).filter( + Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) + ).order_by('order', 'id') + class CategoriesBySectView(ListAPIView): """ - API view to list HadisCategories filtered by sect_type + API view to list HadisCategories filtered by sect_type (excluding empty leaf categories) """ serializer_class = CategorySerializer pagination_class = StandardResultsSetPagination def get_queryset(self): sect_type = self.kwargs.get('sect_type') + from django.db.models import Count, Q queryset = HadisCategory.objects.filter( sect__sect_type=sect_type, sect__is_active=True + ).annotate( + active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), + children_total_count=Count('children') + ).filter( + Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) ) is_root = self.request.query_params.get('is_root', 'true').lower() if is_root in ['true', '1']: diff --git a/apps/hadis/views/hadis.py b/apps/hadis/views/hadis.py index d12e86d..3d0121a 100644 --- a/apps/hadis/views/hadis.py +++ b/apps/hadis/views/hadis.py @@ -701,9 +701,15 @@ class HadisFiltersView(ListAPIView): 'slug': status.slug }) - # Get categories from HadisCategory model + # Get categories from HadisCategory model (excluding empty leaf categories) + from django.db.models import Count, Q categories = [] - for category in HadisCategory.objects.all().order_by('order'): + for category in HadisCategory.objects.annotate( + active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), + children_total_count=Count('children') + ).filter( + Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) + ).order_by('order'): title_text = get_localized_text(category.title, request) if title_text and category.slug: categories.append({ @@ -814,7 +820,8 @@ class HadisFiltersSyncAPIView(APIView): for status in HadisStatus.objects.all().order_by('order') ] - # ۲. دسته‌بندی‌ها (مرتب‌شده بر اساس فیلد order) + # ۲. دسته‌بندی‌ها (مرتب‌شده بر اساس فیلد order، به استثنای دسته‌بندی‌های خالی) + from django.db.models import Count, Q categories = [ { "id": cat.id, @@ -822,7 +829,12 @@ class HadisFiltersSyncAPIView(APIView): "title": get_localized_text(cat.title, request=request, language_code=lang) or "", "order": cat.order, } - for cat in HadisCategory.objects.all().order_by('order') + for cat in HadisCategory.objects.annotate( + active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), + children_total_count=Count('children') + ).filter( + Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) + ).order_by('order') ] # ۳. منابع / کتاب‌ها diff --git a/apps/hadis/views_admin.py b/apps/hadis/views_admin.py index 7e134b8..a9c6150 100644 --- a/apps/hadis/views_admin.py +++ b/apps/hadis/views_admin.py @@ -312,7 +312,7 @@ class AdminTransmitterViewSet(ModelViewSet): if generation_filter and generation_filter != "all": queryset = queryset.filter(generation=generation_filter) - return queryset.order_by("id") + return queryset.order_by("-created_at", "-id")