From 790e5492ce1bd7ec3232d54e2f698a821da66796 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Wed, 9 Sep 2026 11:50:55 +0330 Subject: [PATCH] feat(hadis): support category ordering by most content and alphabetical title --- apps/hadis/serializers/category.py | 68 +++++++++++---------------- apps/hadis/views/category.py | 75 ++++++++++++++++++++++++++---- 2 files changed, 94 insertions(+), 49 deletions(-) diff --git a/apps/hadis/serializers/category.py b/apps/hadis/serializers/category.py index 78a49f2..b1b09c3 100644 --- a/apps/hadis/serializers/category.py +++ b/apps/hadis/serializers/category.py @@ -225,20 +225,15 @@ class HadisCategorySelectSerializer(serializers.ModelSerializer): return Hadis.objects.filter(category=obj, status=True).exists() def get_children_count(self, obj): - # """Get count of active children categories that have children or hadis""" - # children = obj.get_children().filter(sect=obj.sect) - # return len(children) - """ - Calculates the total number of Hadiths in this category - and all its descendants (sub-categories). - """ - # 1. Get all descendants of this category (including itself) + if hasattr(obj, 'total_content_count'): + return obj.total_content_count family_tree = obj.get_descendants(include_self=True) - - # 2. Count all Hadiths that belong to any category in this tree - return Hadis.objects.filter(category__in=family_tree).count() - def get_hadis_count(self,obj): - return len(Hadis.objects.filter(category=obj)) + return Hadis.objects.filter(category__in=family_tree, status=True).count() + + def get_hadis_count(self, obj): + if hasattr(obj, 'active_hadis_count'): + return obj.active_hadis_count + return Hadis.objects.filter(category=obj, status=True).count() @@ -274,20 +269,15 @@ class HadisCategorySelectSourceSerializer(serializers.ModelSerializer): return Hadis.objects.filter(category=obj, status=True).exists() def get_children_count(self, obj): - # """Get count of active children categories that have children or hadis""" - # children = obj.get_children().filter(sect=obj.sect) - # return len(children) - """ - Calculates the total number of Hadiths in this category - and all its descendants (sub-categories). - """ - # 1. Get all descendants of this category (including itself) + if hasattr(obj, 'total_content_count'): + return obj.total_content_count family_tree = obj.get_descendants(include_self=True) - - # 2. Count all Hadiths that belong to any category in this tree - return Hadis.objects.filter(category__in=family_tree).count() - def get_hadis_count(self,obj): - return len(Hadis.objects.filter(category=obj)) + return Hadis.objects.filter(category__in=family_tree, status=True).count() + + def get_hadis_count(self, obj): + if hasattr(obj, 'active_hadis_count'): + return obj.active_hadis_count + return Hadis.objects.filter(category=obj, status=True).count() class CategorySerializer(serializers.ModelSerializer): sect_id = serializers.IntegerField(source='sect.id', read_only=True) @@ -310,22 +300,18 @@ class CategorySerializer(serializers.ModelSerializer): return obj.parent_id is not None def get_children_count(self, obj): - # """Get count of active children categories that have children or hadis""" - # children = obj.get_children().filter(sect=obj.sect) - # return len(children) - """ - Calculates the total number of Hadiths in this category - and all its descendants (sub-categories). - """ - # 1. Get all descendants of this category (including itself) + if hasattr(obj, 'total_content_count'): + return obj.total_content_count family_tree = obj.get_descendants(include_self=True) - - # 2. Count all Hadiths that belong to any category in this tree - return Hadis.objects.filter(category__in=family_tree).count() - def get_has_hadis(self,obj): - return Hadis.objects.filter(category=obj).exists() - def get_hadis_count(self,obj): - return len(Hadis.objects.filter(category=obj)) + return Hadis.objects.filter(category__in=family_tree, status=True).count() + + def get_has_hadis(self, obj): + return Hadis.objects.filter(category=obj, status=True).exists() + + def get_hadis_count(self, obj): + if hasattr(obj, 'active_hadis_count'): + return obj.active_hadis_count + return Hadis.objects.filter(category=obj, status=True).count() # def get_title(self,obj): # # ✅ Get language from request diff --git a/apps/hadis/views/category.py b/apps/hadis/views/category.py index 93fa06e..37d9220 100644 --- a/apps/hadis/views/category.py +++ b/apps/hadis/views/category.py @@ -266,6 +266,57 @@ class HadisCategoryTreeNormalView(ListAPIView): Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) ).order_by('sect__order', 'order') +def apply_category_ordering_and_counts(queryset, request): + from django.db.models import Subquery, OuterRef, Count, IntegerField + from django.db.models.functions import Coalesce + from django.db.models.expressions import RawSQL + from django.utils.translation import get_language + + descendant_hadis_subquery = Subquery( + Hadis.objects.filter( + category__tree_id=OuterRef('tree_id'), + category__lft__gte=OuterRef('lft'), + category__rght__lte=OuterRef('rght'), + status=True + ).values('category__tree_id').annotate(cnt=Count('id')).values('cnt')[:1], + output_field=IntegerField() + ) + queryset = queryset.annotate(total_content_count=Coalesce(descendant_hadis_subquery, 0)) + + ordering_param = ( + request.query_params.get('ordering') or + request.query_params.get('sort') or + '' + ).strip().lower() + + if ordering_param in ['content', 'most_content', '-total_content_count', '-content']: + return queryset.order_by('-total_content_count', 'order', 'id') + elif ordering_param in ['alphabetical', 'title', 'slug', 'a-z']: + lang = ( + request.query_params.get('language_code') or + request.query_params.get('lang') or + getattr(request, 'LANGUAGE_CODE', None) + ) + if not lang and hasattr(request, 'headers'): + lang = request.headers.get('Accept-Language') + if lang: + lang = lang.split(',')[0].split(';')[0].split('-')[0].strip().lower() + if not lang: + lang = get_language() or 'en' + + queryset = queryset.annotate( + loc_title=RawSQL(""" + COALESCE( + NULLIF((SELECT COALESCE(elem->>'title', elem->>'text') FROM jsonb_array_elements(CASE WHEN jsonb_typeof(hadis_hadiscategory.title::jsonb) = 'array' THEN hadis_hadiscategory.title::jsonb ELSE '[]'::jsonb END) elem WHERE elem->>'language_code' = %s LIMIT 1), ''), + NULLIF((SELECT COALESCE(elem->>'title', elem->>'text') FROM jsonb_array_elements(CASE WHEN jsonb_typeof(hadis_hadiscategory.title::jsonb) = 'array' THEN hadis_hadiscategory.title::jsonb ELSE '[]'::jsonb END) elem WHERE elem->>'language_code' = 'en' LIMIT 1), ''), + hadis_hadiscategory.slug + ) + """, [lang]) + ) + return queryset.order_by('loc_title', 'slug', 'id') + + return queryset.order_by('order', 'id') + class HadisCategorySelectBySectView(ListAPIView): """ @@ -274,7 +325,6 @@ class HadisCategorySelectBySectView(ListAPIView): """ serializer_class = HadisCategorySelectSerializer pagination_class = StandardResultsSetPagination - @categories_tree_by_sect_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) @@ -323,7 +373,7 @@ class HadisCategorySelectBySectView(ListAPIView): 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( + queryset = HadisCategory.objects.filter( parent=parent_category, sect__sect_type=sect_type, sect__is_active=True @@ -332,7 +382,8 @@ class HadisCategorySelectBySectView(ListAPIView): 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') + ) + return apply_category_ordering_and_counts(queryset, self.request) class HadisCategorySelectBySectSourceView(ListAPIView): @@ -391,7 +442,7 @@ class HadisCategorySelectBySectSourceView(ListAPIView): from django.db.models import Count, Q # Return children of this category, filtered by source_type and excluding empty leaves - return HadisCategory.objects.filter( + queryset = HadisCategory.objects.filter( parent=parent_category, sect__sect_type=sect_type, sect__is_active=True, @@ -401,7 +452,8 @@ class HadisCategorySelectBySectSourceView(ListAPIView): 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') + ) + return apply_category_ordering_and_counts(queryset, self.request) class CategoriesView(ListAPIView): """ @@ -415,14 +467,21 @@ class CategoriesView(ListAPIView): def get_queryset(self): from django.db.models import Count, Q - return HadisCategory.objects.filter( + queryset = 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') + ) + is_root = self.request.query_params.get('is_root', '').lower() + if is_root in ['true', '1']: + queryset = queryset.filter(parent__isnull=True) + elif is_root in ['false', '0']: + queryset = queryset.filter(parent__isnull=False) + + return apply_category_ordering_and_counts(queryset, self.request) @@ -451,7 +510,7 @@ class CategoriesBySectView(ListAPIView): elif is_root in ['false', '0']: queryset = queryset.filter(parent__isnull=False) - return queryset.order_by('order', 'id') + return apply_category_ordering_and_counts(queryset, self.request) @categories_by_sect_swagger def get(self, request, *args, **kwargs):