@ -55,35 +55,43 @@ class HadisCategoryTreeView(ListAPIView):
def get_queryset ( self ) :
"""
Fetch ALL active categories in a single query with annotations .
Fetch ALL active categories in a single query .
"""
from django.db.models import Count , Q
return (
HadisCategory . objects
. filter ( sect__is_active = True )
. select_related ( ' sect ' )
. annotate (
# Count of hadiths directly in this category
direct_hadis_count = Count ( ' hadis ' , filter = Q ( hadis__status = True ) )
)
. order_by ( ' sect__order ' , ' tree_id ' , ' lft ' , ' order ' )
)
def list ( self , request , * args , * * kwargs ) :
queryset = self . get_queryset ( )
queryset = list ( self . get_queryset ( ) )
cat_ids = [ cat . id for cat in queryset ]
# 1. Map Hadis IDs
hadis_ids_map = defaultdict ( li st)
hadis _pairs = (
# 1. Map Hadis IDs from BOTH legacy FK (`category`) and M2M (`categories`)
hadis_ids_sets = defaultdict ( se t )
fk _pairs = (
Hadis . objects
. filter ( status = True , category_id__in = [ cat . id for cat in queryset ] )
. filter ( status = True , category_id__in = cat_ids )
. values_list ( ' category_id ' , ' id ' )
. order_by ( ' category_id ' , ' id ' )
)
for category_id , hadis_id in hadis_pairs :
hadis_ids_map [ category_id ] . append ( hadis_id )
for category_id , hadis_id in fk_pairs :
hadis_ids_sets [ category_id ] . add ( hadis_id )
m2m_pairs = (
Hadis . categories . through . objects
. filter ( hadis__status = True , hadiscategory_id__in = cat_ids )
. values_list ( ' hadiscategory_id ' , ' hadis_id ' )
)
for category_id , hadis_id in m2m_pairs :
hadis_ids_sets [ category_id ] . add ( hadis_id )
hadis_ids_map = {
cid : sorted ( h_ids ) for cid , h_ids in hadis_ids_sets . items ( )
}
for cat in queryset :
cat . direct_hadis_count = len ( hadis_ids_map . get ( cat . id , ( ) ) )
# 👇 2. Map Quran Data (Bulk fetch to prevent N+1 Queries)
quran_data_map = { }
@ -112,24 +120,24 @@ class HadisCategoryTreeView(ListAPIView):
children_map [ cat . parent_id ] = [ ]
children_map [ cat . parent_id ] . append ( cat )
# 4. Pre-calculate recursive hadis counts
recursive_coun ts = { }
# 4. Pre-calculate recursive hadis counts (distinct active hadith IDs in subtree)
recursive_se ts = { }
def get_recursive_count ( cat_id ) :
if cat_id in recursive_counts :
return recursive_counts [ cat_id ]
cat = category_map [ cat_id ]
count = cat . direct_hadis_count
def get_recursive_hadis_set ( cat_id ) :
if cat_id in recursive_sets :
return recursive_sets [ cat_id ]
combined = set ( hadis_ids_sets . get ( cat_id , ( ) ) )
for child in children_map . get ( cat_id , [ ] ) :
count + = get_recursive_coun t ( child . id )
combined . update ( get_recursive_hadis_se t ( child . id ) )
recursive_coun ts [ cat_id ] = count
return count
recursive_se ts [ cat_id ] = combined
return combined
for cat in queryset :
get_recursive_count ( cat . id )
recursive_counts = {
cat . id : len ( get_recursive_hadis_set ( cat . id ) )
for cat in queryset
}
# 5. Build grouped structure
grouped_data = { }
@ -265,6 +273,56 @@ class HadisCategoryTreeView(ListAPIView):
return None
def annotate_and_prune_empty_categories ( queryset ) :
"""
Annotates categories with :
- `active_hadis_count` : count of active Hadis directly in this category ( via FK `category` or M2M `categories` )
- `total_content_count` : count of active Hadis anywhere in this category ' s MPTT subtree (via FK or M2M)
- `has_subtree_quran` : whether any category in this MPTT subtree has a Quran verse or is a Quran category with content
And filters out categories whose entire subtree has no active hadiths and no Quran verses .
"""
from django.db.models import Exists
from django.db.models.expressions import RawSQL
from ..models.category import CategoryQuranVerse
queryset = queryset . annotate (
active_hadis_count = RawSQL ( """
(
SELECT COUNT ( DISTINCT h . id )
FROM hadis_hadis h
LEFT JOIN hadis_hadis_categories hhc ON hhc . hadis_id = h . id
WHERE h . status = TRUE
AND ( h . category_id = hadis_hadiscategory . id OR hhc . hadiscategory_id = hadis_hadiscategory . id )
)
""" , []),
total_content_count = RawSQL ( """
(
SELECT COUNT ( DISTINCT h . id )
FROM hadis_hadis h
LEFT JOIN hadis_hadiscategory c1 ON c1 . id = h . category_id
LEFT JOIN hadis_hadis_categories hhc ON hhc . hadis_id = h . id
LEFT JOIN hadis_hadiscategory c2 ON c2 . id = hhc . hadiscategory_id
WHERE h . status = TRUE
AND (
( c1 . tree_id = hadis_hadiscategory . tree_id AND c1 . lft > = hadis_hadiscategory . lft AND c1 . rght < = hadis_hadiscategory . rght )
OR
( c2 . tree_id = hadis_hadiscategory . tree_id AND c2 . lft > = hadis_hadiscategory . lft AND c2 . rght < = hadis_hadiscategory . rght )
)
)
""" , []),
has_subtree_quran = Exists (
CategoryQuranVerse . objects . filter (
category__tree_id = RawSQL ( " hadis_hadiscategory.tree_id " , [ ] ) ,
category__lft__gte = RawSQL ( " hadis_hadiscategory.lft " , [ ] ) ,
category__rght__lte = RawSQL ( " hadis_hadiscategory.rght " , [ ] ) ,
)
) ,
) . filter (
Q ( total_content_count__gt = 0 ) | Q ( has_subtree_quran = True )
)
return queryset
class HadisCategoryTreeNormalView ( ListAPIView ) :
"""
Normal ( paginated ) tree view for HadisCategory .
@ -279,33 +337,18 @@ class HadisCategoryTreeNormalView(ListAPIView):
return self . list ( request , * args , * * kwargs )
def get_queryset ( self ) :
from django.db.models import Count , Q
return HadisCategory . objects . filter (
qs = 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 ' )
)
return annotate_and_prune_empty_categories ( qs ) . 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 ) )
if ' total_content_count ' not in queryset . query . annotations :
queryset = annotate_and_prune_empty_categories ( queryset )
ordering_param = (
request . query_params . get ( ' ordering ' ) or
@ -395,18 +438,13 @@ class HadisCategorySelectBySectView(ListAPIView):
except HadisCategory . DoesNotExist :
return HadisCategory . objects . none ( )
from django.db.models import Count , Q
# Return children of this category that have either active hadiths, children, or are quran
# Return children of this category that have active content in their subtree or Quran verse
queryset = 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 )
)
queryset = annotate_and_prune_empty_categories ( queryset )
return apply_category_ordering_and_counts ( queryset , self . request )
@ -464,19 +502,14 @@ class HadisCategorySelectBySectSourceView(ListAPIView):
except HadisCategory . DoesNotExist :
return HadisCategory . objects . none ( )
from django.db.models import Count , Q
# Return children of this category, filtered by source_type and excluding empty leaves
# Return children of this category, filtered by source_type and excluding empty subtrees
queryset = 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 )
)
queryset = annotate_and_prune_empty_categories ( queryset )
return apply_category_ordering_and_counts ( queryset , self . request )
class CategoriesView ( ListAPIView ) :
@ -490,15 +523,10 @@ class CategoriesView(ListAPIView):
return self . list ( request , * args , * * kwargs )
def get_queryset ( self ) :
from django.db.models import Count , Q
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 )
)
queryset = annotate_and_prune_empty_categories ( queryset )
search_query = ( self . request . query_params . get ( ' search ' ) or ' ' ) . strip ( )
if search_query :
from utils.search_parser import build_advanced_search_q
@ -525,16 +553,11 @@ class CategoriesBySectView(ListAPIView):
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 )
)
queryset = annotate_and_prune_empty_categories ( queryset )
search_query = ( self . request . query_params . get ( ' search ' ) or ' ' ) . strip ( )
if search_query :
from utils.search_parser import build_advanced_search_q
@ -653,9 +676,9 @@ class HadisCategoryXMindView(APIView):
) . order_by ( ' order ' )
hadiths = Hadis . objects . filter (
category = category ,
Q ( category = category ) | Q ( categories = category ) ,
status = True
) . order_by ( ' number ' ) . prefetch_related (
) . distinct ( ) . order_by ( ' number ' ) . prefetch_related (
Prefetch ( ' transmitters ' , queryset = transmitter_qs ) ,
' narrator_layers ' ,
' hadiscorrection_set '