|
|
|
@ -52,47 +52,63 @@ class HadisCategoryTreeView(ListAPIView): |
|
|
|
|
|
|
|
def get_queryset(self): |
|
|
|
""" |
|
|
|
Prefetch ALL data at once to avoid N+1 |
|
|
|
Fetch ALL active categories in a single query with annotations. |
|
|
|
""" |
|
|
|
from django.db.models import Prefetch, Count, Q |
|
|
|
|
|
|
|
# Create a queryset for children that also has annotations |
|
|
|
children_queryset = ( |
|
|
|
HadisCategory.objects |
|
|
|
.select_related('sect') |
|
|
|
.prefetch_related('children') |
|
|
|
.annotate( |
|
|
|
hadis_count=Count('hadis', filter=Q(hadis__status=True)), |
|
|
|
children_count=Count('children', distinct=True) |
|
|
|
) |
|
|
|
.order_by('order') |
|
|
|
) |
|
|
|
from django.db.models import Count, Q |
|
|
|
|
|
|
|
return ( |
|
|
|
HadisCategory.objects |
|
|
|
.filter(parent__isnull=True, sect__is_active=True) |
|
|
|
.filter(sect__is_active=True) |
|
|
|
.select_related('sect') |
|
|
|
.prefetch_related( |
|
|
|
Prefetch( |
|
|
|
'children', |
|
|
|
queryset=children_queryset |
|
|
|
), |
|
|
|
) |
|
|
|
.annotate( |
|
|
|
hadis_count=Count('hadis', filter=Q(hadis__status=True)), |
|
|
|
children_count=Count('children', distinct=True) |
|
|
|
# Count of hadiths directly in this category |
|
|
|
direct_hadis_count=Count('hadis', filter=Q(hadis__status=True)) |
|
|
|
) |
|
|
|
.order_by('sect__order', 'order') |
|
|
|
.order_by('sect__order', 'tree_id', 'lft', 'order') |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
def list(self, request, *args, **kwargs): |
|
|
|
queryset = self.get_queryset() |
|
|
|
|
|
|
|
# 1. Build a mapping of all categories and their children |
|
|
|
category_map = {cat.id: cat for cat in queryset} |
|
|
|
children_map = {} |
|
|
|
roots = [] |
|
|
|
|
|
|
|
for cat in queryset: |
|
|
|
if cat.parent_id is None: |
|
|
|
roots.append(cat) |
|
|
|
else: |
|
|
|
if cat.parent_id not in children_map: |
|
|
|
children_map[cat.parent_id] = [] |
|
|
|
children_map[cat.parent_id].append(cat) |
|
|
|
|
|
|
|
# 2. Pre-calculate recursive hadis counts (total hadiths in sub-tree) |
|
|
|
# This matches CategorySerializer.children_count logic |
|
|
|
recursive_counts = {} |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
for child in children_map.get(cat_id, []): |
|
|
|
count += get_recursive_count(child.id) |
|
|
|
|
|
|
|
recursive_counts[cat_id] = count |
|
|
|
return count |
|
|
|
|
|
|
|
for cat in queryset: |
|
|
|
get_recursive_count(cat.id) |
|
|
|
|
|
|
|
# 3. Build grouped structure |
|
|
|
grouped_data = {} |
|
|
|
|
|
|
|
# Single pass through prefetched data (NO QUERIES) |
|
|
|
for category in queryset: |
|
|
|
sect_type = category.sect.sect_type |
|
|
|
for root in roots: |
|
|
|
sect_type = root.sect.sect_type |
|
|
|
|
|
|
|
if sect_type not in grouped_data: |
|
|
|
grouped_data[sect_type] = { |
|
|
|
@ -101,26 +117,29 @@ class HadisCategoryTreeView(ListAPIView): |
|
|
|
} |
|
|
|
|
|
|
|
# Add sect info |
|
|
|
sect_id = str(category.sect.id) |
|
|
|
sect_id = str(root.sect.id) |
|
|
|
if sect_id not in grouped_data[sect_type]['sects']: |
|
|
|
# Optimistic source_types fetch - though still a query per sect |
|
|
|
# but better than before |
|
|
|
source_types = HadisCategory.objects.filter( |
|
|
|
sect=category.sect |
|
|
|
).values_list('source_type', flat=True) |
|
|
|
sect=root.sect |
|
|
|
).values_list('source_type', flat=True).order_by().distinct() |
|
|
|
|
|
|
|
grouped_data[sect_type]['sects'][sect_id] = { |
|
|
|
'id': category.sect.id, |
|
|
|
'sect_type': category.sect.sect_type, |
|
|
|
'title': get_localized_text(category.sect.title, request), |
|
|
|
'description': get_localized_text(category.sect.description,request), |
|
|
|
'order': category.sect.order, |
|
|
|
'source_types':list(set(source_types)) |
|
|
|
'id': root.sect.id, |
|
|
|
'sect_type': root.sect.sect_type, |
|
|
|
'title': get_localized_text(root.sect.title, request), |
|
|
|
'description': get_localized_text(root.sect.description, request), |
|
|
|
'order': root.sect.order, |
|
|
|
'source_types': list(source_types) |
|
|
|
} |
|
|
|
|
|
|
|
# Build tree using prefetched data |
|
|
|
category_data = self._build_tree(category, request) |
|
|
|
# Build tree using mapping |
|
|
|
category_data = self._build_tree_recursive(root, request, children_map, recursive_counts) |
|
|
|
grouped_data[sect_type]['categories'].append(category_data) |
|
|
|
|
|
|
|
# Count total categories (simple loop, no queries) |
|
|
|
total_count = self._count_tree_items(grouped_data) |
|
|
|
# Count total categories |
|
|
|
total_count = len(queryset) |
|
|
|
|
|
|
|
response_data = { |
|
|
|
'count': total_count, |
|
|
|
@ -129,48 +148,34 @@ class HadisCategoryTreeView(ListAPIView): |
|
|
|
|
|
|
|
return Response(response_data) |
|
|
|
|
|
|
|
def _build_tree(self, category, request): |
|
|
|
def _build_tree_recursive(self, category, request, children_map, recursive_counts): |
|
|
|
""" |
|
|
|
Build tree from ALREADY PREFETCHED data |
|
|
|
No database queries here! |
|
|
|
Build tree from flat mapping |
|
|
|
""" |
|
|
|
children = children_map.get(category.id, []) |
|
|
|
|
|
|
|
# Align fields with CategorySerializer |
|
|
|
return { |
|
|
|
'id': category.id, |
|
|
|
'title': get_localized_text(category.title, request), |
|
|
|
'description': get_localized_text(category.description, request), |
|
|
|
'slug': category.slug, |
|
|
|
'source_type': category.source_type, |
|
|
|
'hadis_count': category.hadis_count, # ← Use annotated value |
|
|
|
'children_count': category.children_count, # ← Use annotated value |
|
|
|
'has_hadis': category.hadis_count > 0, # ← Simple calculation |
|
|
|
'sect_id': category.sect_id, |
|
|
|
'sect_type': category.sect.sect_type, |
|
|
|
'hadis_count': category.direct_hadis_count, # Direct count |
|
|
|
'children_count': recursive_counts.get(category.id, 0), # Total in sub-tree (matches normal endpoint) |
|
|
|
'has_hadis': category.direct_hadis_count > 0, |
|
|
|
'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(child, request) |
|
|
|
for child in category.children.all() # Already prefetched! |
|
|
|
self._build_tree_recursive(child, request, children_map, recursive_counts) |
|
|
|
for child in children |
|
|
|
] |
|
|
|
} |
|
|
|
|
|
|
|
def _count_tree_items(self, grouped_data): |
|
|
|
"""Count total items in tree""" |
|
|
|
total = 0 |
|
|
|
for sect_type_data in grouped_data.values(): |
|
|
|
for item in sect_type_data['categories']: |
|
|
|
total += 1 |
|
|
|
total += self._count_children(item.get('children', [])) |
|
|
|
return total |
|
|
|
|
|
|
|
def _count_children(self, children_list): |
|
|
|
"""Recursively count children""" |
|
|
|
count = 0 |
|
|
|
for item in children_list: |
|
|
|
count += 1 |
|
|
|
if item.get('children'): |
|
|
|
count += self._count_children(item['children']) |
|
|
|
return count |
|
|
|
|
|
|
|
def _get_thumbnail_url(self, category, request): |
|
|
|
"""Get absolute thumbnail URL""" |
|
|
|
if hasattr(category, 'thumbnail') and category.thumbnail: |
|
|
|
|