from rest_framework.generics import ListAPIView from rest_framework.response import Response from django.shortcuts import get_object_or_404 from utils.pagination import NoPagination from django.db.models import Q, Prefetch from collections import defaultdict from utils.pagination import StandardResultsSetPagination from utils import absolute_https_url from ..models import HadisSect, HadisCategory, Hadis, HadisTransmitter from apps.bookmark.serializers.bookmark import BookmarkStatusSerializer from ..serializers import ( HadisCategorySectListSerializer, HadisCategoryTreeSerializer, CategorySerializer , HadisCategorySelectSerializer , HadisCategorySelectSourceSerializer, get_localized_text, get_arabic_localized_text, SimpleCategory ) from ..docs import ( hadis_sect_list_swagger, hadis_category_tree_swagger, categories_list_swagger, categories_by_sect_swagger, categories_tree_by_sect_swagger, categories_tree_by_sect_source_swagger, hadis_category_xmind_swagger ) class HadisCategorySectListView(ListAPIView): """ API view to list all HadisSects grouped by sect_type (shia/sunni) """ queryset = HadisSect.objects.filter(is_active=True).order_by('order') serializer_class = HadisCategorySectListSerializer pagination_class = NoPagination @hadis_sect_list_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) class HadisCategoryTreeView(ListAPIView): """ API view to get all HadisCategory tree structure grouped by sect """ serializer_class = HadisCategoryTreeSerializer pagination_class = NoPagination @hadis_category_tree_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def get_queryset(self): """ Fetch ALL active categories in a single query with annotations. """ 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() # 1. Map Hadis IDs hadis_ids_map = defaultdict(list) hadis_pairs = ( Hadis.objects .filter(status=True, category_id__in=[cat.id for cat in queryset]) .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) # 👇 2. Map Quran Data (Bulk fetch to prevent N+1 Queries) quran_data_map = {} from ..models.category import CategoryQuranVerse from ..serializers.category import CategoryQuranVerseSerializer quran_verses = CategoryQuranVerse.objects.filter( category__in=queryset ).prefetch_related('secondary_translations') for verse in quran_verses: quran_data_map[verse.category_id] = CategoryQuranVerseSerializer( verse, context={'request': request} ).data # 3. 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) # 4. Pre-calculate recursive hadis counts 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) # 5. Build grouped structure grouped_data = {} 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': {}, 'categories': [] } # Add sect info sect_id = str(root.sect.id) if sect_id not in grouped_data[sect_type]['sects']: source_types = HadisCategory.objects.filter( sect=root.sect ).values_list('source_type', flat=True).order_by().distinct() grouped_data[sect_type]['sects'][sect_id] = { '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) } grouped_data[sect_type]['categories'].append(category_data) total_count = len(queryset) response_data = { 'count': total_count, 'results': grouped_data } return Response(response_data) def _build_tree_recursive(self, category, request, children_map, recursive_counts, hadis_ids_map, quran_data_map): """ Build tree from flat mapping, pruning any node that has no active children and no active hadiths. """ 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, 'title': get_localized_text(category.title, request), 'description': get_localized_text(category.description, request), 'slug': category.slug, 'source_type': category.source_type, 'sect_id': category.sect_id, 'sect_type': category.sect.sect_type, 'hadis_count': category.direct_hadis_count, 'children_count': recursive_counts.get(category.id, 0), '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': built_children } def _get_thumbnail_url(self, category, request): """Get absolute thumbnail URL""" if hasattr(category, 'thumbnail') and category.thumbnail: return absolute_https_url(category.thumbnail.url, request) return None def _get_xmind_url(self, category, request): """Get absolute xmind URL""" if getattr(category, 'xmind_file', None): return request.build_absolute_uri(category.xmind_file.url) if request else category.xmind_file.url return None class HadisCategoryTreeNormalView(ListAPIView): """ Normal (paginated) tree view for HadisCategory. Unlike the sync view, this simply returns the root categories (filtered to active sects) with their nested children, and uses the project's default pagination. """ serializer_class = HadisCategoryTreeSerializer pagination_class = StandardResultsSetPagination @hadis_category_tree_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( 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') 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): """ Tree view for HadisCategory filtered by sect_type and category slug. Returns the children categories of the specified category (by slug) within the sect_type. """ serializer_class = HadisCategorySelectSerializer pagination_class = StandardResultsSetPagination @categories_tree_by_sect_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def list(self, request, *args, **kwargs): # 1. Run the standard list logic (get pagination, filter, results) response = super().list(request, *args, **kwargs) # 2. Find the "Parent" Category based on the URL slug category_slug = self.kwargs.get('slug') category_obj = get_object_or_404(HadisCategory, slug=category_slug) # 3. Serialize this single category for the Hero section # You might need a simple serializer just for titles/descriptions category_data = SimpleCategory(category_obj).data # 4. Inject it into the response data # Note: We access response.data because we are using DRF's Response object if isinstance(response.data, dict): # Reorder the response to place current_category before results ordered_data = {} for key in ['count', 'next', 'previous']: if key in response.data: ordered_data[key] = response.data[key] ordered_data['current_category'] = category_data if 'results' in response.data: ordered_data['results'] = response.data['results'] response.data = ordered_data return response def get_queryset(self): sect_type = self.kwargs.get('sect_type') slug = self.kwargs.get('slug') # Find the parent category by slug and sect_type try: parent_category = HadisCategory.objects.get( slug=slug, sect__sect_type=sect_type, sect__is_active=True ) 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 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) ) return apply_category_ordering_and_counts(queryset, self.request) class HadisCategorySelectBySectSourceView(ListAPIView): """ Tree view for HadisCategory filtered by sect_type, category slug and source_type. Returns the children categories of the specified category (by slug) within the sect_type, filtered by source_type. """ serializer_class = HadisCategorySelectSourceSerializer pagination_class = StandardResultsSetPagination @categories_tree_by_sect_source_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def list(self, request, *args, **kwargs): # 1. Run the standard list logic (get pagination, filter, results) response = super().list(request, *args, **kwargs) # 2. Find the "Parent" Category based on the URL slug category_slug = self.kwargs.get('slug') category_obj = get_object_or_404(HadisCategory, slug=category_slug) # 3. Serialize this single category for the Hero section # You might need a simple serializer just for titles/descriptions category_data = SimpleCategory(category_obj).data # 4. Inject it into the response data # Note: We access response.data because we are using DRF's Response object if isinstance(response.data, dict): # Reorder the response to place current_category before results ordered_data = {} for key in ['count', 'next', 'previous']: if key in response.data: ordered_data[key] = response.data[key] ordered_data['current_category'] = category_data if 'results' in response.data: ordered_data['results'] = response.data['results'] response.data = ordered_data return response def get_queryset(self): sect_type = self.kwargs.get('sect_type') slug = self.kwargs.get('slug') source_type = self.kwargs.get('source_type') # Find the parent category by slug and sect_type try: parent_category = HadisCategory.objects.get( slug=slug, sect__sect_type=sect_type, sect__is_active=True ) 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 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) ) return apply_category_ordering_and_counts(queryset, self.request) class CategoriesView(ListAPIView): """ API view to list all HadisCategories (excluding empty leaf categories) """ 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 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) ) 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) class CategoriesBySectView(ListAPIView): """ 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']: 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) @categories_by_sect_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) from rest_framework.decorators import api_view, permission_classes, authentication_classes from rest_framework.permissions import AllowAny from rest_framework.response import Response @api_view(['GET', 'POST']) @permission_classes([AllowAny]) # Let anyone access this @authentication_classes([]) # Disable auth so we don't get 403 def test_deploy(request): # This filters all headers Django receives and returns them as JSON headers = { k: v for k, v in request.META.items() if k.startswith('HTTP_') or k == 'CONTENT_TYPE' } # Also check if Authentication settings are actually active from django.conf import settings auth_settings = settings.REST_FRAMEWORK.get('DEFAULT_AUTHENTICATION_CLASSES', 'NOT SET') return Response({ "received_headers": headers, "active_auth_settings": auth_settings }) from django.http import JsonResponse from django.conf import settings def debug_headers(request): # # Security: strictly limitation to prevent leaking sensitive info to public # # Only allow if a specific secret key is passed in the URL # if request.GET.get('secret_debug_key') != 'super_secret_123': # return JsonResponse({'error': 'Unauthorized'}, status=403) # Return all HTTP headers Django received from Nginx headers = { k: v for k, v in request.META.items() if k.startswith('HTTP_') or k in ['CONTENT_TYPE', 'CONTENT_LENGTH'] } # Also return the scheme Django thinks it is using scheme_debug = { 'scheme': request.scheme, 'is_secure': request.is_secure(), 'SECURE_PROXY_SSL_HEADER_SETTING': getattr(settings, 'SECURE_PROXY_SSL_HEADER', None), } return JsonResponse({'headers': headers, 'debug': scheme_debug}) from rest_framework.views import APIView class HadisCategoryXMindView(APIView): """ Returns a mind-map JSON structure for a specific category and its transmission chains ending in hadiths. Root -> Category Level 1+ -> Narrators in transmission chains (merged prefixes) Leaves -> Hadiths """ def get_localized(self, json_field, lang='en'): return get_localized_text(json_field, language_code=lang) or "" def get_arabic(self, json_field): return get_arabic_localized_text(json_field) or "" def _get_thumbnail_url(self, thumbnail, request): if not thumbnail: return None try: if request: return request.build_absolute_uri(thumbnail.url) except Exception: pass return thumbnail.url @hadis_category_xmind_swagger def get(self, request, category_slug): lang = request.query_params.get('lang') or request.query_params.get('language_code') or 'en' category = get_object_or_404(HadisCategory, slug=category_slug) root_title = self.get_localized(category.title, lang) or category.slug user = request.user if request and hasattr(request, 'user') else None if user and user.is_anonymous: user = None # Fetch active hadiths with their transmitters and corrections transmitter_qs = HadisTransmitter.objects.select_related( 'transmitter__reliability', 'status', 'narrator_layer' ).prefetch_related( 'uncertain_transmitters__reliability' ).order_by('order') hadiths = Hadis.objects.filter( category=category, status=True ).order_by('number').prefetch_related( Prefetch('transmitters', queryset=transmitter_qs), 'narrator_layers', 'hadiscorrection_set' ) # Build prefix tree (Trie-like structure) # Root: Category # Branches: Narrator nodes along transmission chains # Leaves: Hadiths root_tree = { "id": f"cat-{category.id}", "slug": category.slug, "node_type": "category", "title": root_title, "children_map": {}, # key: (node_type, id) -> subtree dict "leaf_hadiths": set() } for hadis in hadiths: hadis_title = self.get_localized(hadis.title, lang) hadis_title_narrator = self.get_localized(hadis.title_narrator, lang) hadis_translation = self.get_localized(hadis.translation, lang) bookmark_info = BookmarkStatusSerializer.get_bookmark_info( obj=hadis, user=user, service='hadith' ) is_bookmarked = bookmark_info.get('is_bookmarked', False) corrections_data = [ { "id": c.id, "slug": c.slug, "title": self.get_localized(c.title, lang), "narrator": c.narrator, "text": c.text, "translation": self.get_localized(c.translation, lang), "share_link": c.share_link, } for c in hadis.hadiscorrection_set.all() ] hadis_node_data = { "id": f"hadis-{hadis.id}", "hadis_id": hadis.id, "node_type": "hadith", "slug": hadis.slug, "number": hadis.number, "title": hadis_title, "title_narrator": hadis_title_narrator, "text": hadis.text, "translation": hadis_translation, "share_link": hadis.share_link, "is_bookmarked": is_bookmarked, "corrections": corrections_data, } # Group transmitters by narrative layer (NarratorLayer) # Each narrative layer forms its own transmission branch layer_groups = defaultdict(list) for ht in hadis.transmitters.all(): if ht.transmitter: layer_key = ht.narrator_layer_id if ht.narrator_layer_id is not None else f"chain-{ht.chain_index}" layer_groups[layer_key].append(ht) if not layer_groups: # No transmitters: directly attach hadith to category root hadis_key = ("hadith", hadis.id) if hadis_key not in root_tree["children_map"]: root_tree["children_map"][hadis_key] = { "data": hadis_node_data, "children_map": {}, "leaf_hadiths": set() } root_tree["children_map"][hadis_key]["leaf_hadiths"].add(hadis.id) root_tree["leaf_hadiths"].add(hadis.id) else: # Sort layers by layer number if available sorted_layer_items = sorted( layer_groups.items(), key=lambda item: (item[1][0].narrator_layer.number if item[1] and item[1][0].narrator_layer else 0) ) for layer_key, ht_list in sorted_layer_items: # Sort transmitters in reverse order so the chain goes from first narrator to last narrator reaching the hadith ht_list.sort(key=lambda x: x.order, reverse=True) curr = root_tree curr["leaf_hadiths"].add(hadis.id) for ht in ht_list: tr = ht.transmitter is_uncertain = getattr(ht, 'is_uncertain', False) cand_objs = list(ht.uncertain_transmitters.all()) if is_uncertain else [] is_node_uncertain = bool(is_uncertain and len(cand_objs) > 0) if is_node_uncertain: cand_ids = tuple(sorted([ut.id for ut in cand_objs])) tr_key = ("uncertain", cand_ids) else: cand_ids = () tr_key = ("narrator", tr.id if tr else ht.id) if tr_key not in curr["children_map"]: rel_obj = (tr.reliability if tr else None) or ht.status rel_data = None if rel_obj: rel_data = { "id": rel_obj.id, "title": self.get_localized(rel_obj.title, lang), "color": getattr(rel_obj, 'color', None), "main_color_code": getattr(rel_obj, 'main_color_code', None), "slug": getattr(rel_obj, 'slug', None), } layer_obj = ht.narrator_layer layer_data = None if layer_obj: layer_data = { "id": layer_obj.id, "number": layer_obj.number, "slug": layer_obj.slug, "name": self.get_localized(layer_obj.name, lang), "description": self.get_localized(layer_obj.description, lang), } uncertain_candidates = [] if is_node_uncertain: for ut in cand_objs: ut_rel = ut.reliability ut_rel_data = None if ut_rel: ut_rel_data = { "id": ut_rel.id, "title": self.get_localized(ut_rel.title, lang), "color": getattr(ut_rel, 'color', None), "main_color_code": getattr(ut_rel, 'main_color_code', None), "slug": getattr(ut_rel, 'slug', None), } uncertain_candidates.append({ "id": ut.id, "transmitter_id": ut.id, "slug": ut.slug, "name": self.get_localized(ut.full_name, lang) or ut.slug, "title": self.get_localized(ut.full_name, lang) or ut.slug, "full_name": self.get_localized(ut.full_name, lang), "arabic_full_name": self.get_arabic(ut.full_name), "kunya": self.get_localized(ut.kunya, lang), "arabic_kunya": self.get_arabic(ut.kunya), "known_as": self.get_localized(ut.known_as, lang), "arabic_known_as": self.get_arabic(ut.known_as), "nickname": self.get_localized(ut.nickname, lang), "arabic_nickname": self.get_arabic(ut.nickname), "birth_year_hijri": ut.birth_year_hijri, "death_year_hijri": ut.death_year_hijri, "reliability": ut_rel_data, "thumbnail": self._get_thumbnail_url(ut.thumbnail, request), "share_link": ut.share_link, }) node_type_str = "uncertain" if is_node_uncertain else "narrator" node_id_str = f"uncertain-{'-'.join(map(str, cand_ids))}" if is_node_uncertain else f"narrator-{tr.id if tr else ht.id}" tr_node_data = { "id": node_id_str, "transmitter_id": tr.id if tr else (uncertain_candidates[0]["id"] if uncertain_candidates else None), "node_type": node_type_str, "tag": node_type_str, "slug": tr.slug if tr else (uncertain_candidates[0]["slug"] if uncertain_candidates else None), "title": "Uncertain narrator" if is_node_uncertain else (self.get_localized(tr.full_name, lang) or tr.slug if tr else "Narrator"), "full_name": "Uncertain narrator" if is_node_uncertain else (self.get_localized(tr.full_name, lang) if tr else "Narrator"), "arabic_full_name": self.get_arabic(tr.full_name) if tr else None, "kunya": self.get_localized(tr.kunya, lang) if tr else None, "arabic_kunya": self.get_arabic(tr.kunya) if tr else None, "known_as": self.get_localized(tr.known_as, lang) if tr else None, "arabic_known_as": self.get_arabic(tr.known_as) if tr else None, "nickname": self.get_localized(tr.nickname, lang) if tr else None, "arabic_nickname": self.get_arabic(tr.nickname) if tr else None, "birth_year_hijri": tr.birth_year_hijri if tr else None, "death_year_hijri": tr.death_year_hijri if tr else None, "age_at_death": tr.age_at_death if tr else None, "generation": tr.generation if tr else None, "tadlis": tr.tadlis if tr else None, "ikhtilat": tr.ikhtilat if tr else None, "companion_type": tr.companion_type if tr else None, "madhhab": tr.madhhab if tr else None, "in_sahih_muslim": tr.in_sahih_muslim if tr else None, "in_sahih_bukhari": tr.in_sahih_bukhari if tr else None, "reliability": rel_data, "is_uncertain": is_node_uncertain, "uncertain_transmitters": uncertain_candidates, "transmitter": { "id": tr.id, "name": self.get_localized(tr.full_name, lang) or tr.slug, "full_name": self.get_localized(tr.full_name, lang), "slug": tr.slug, "reliability": rel_data, } if tr else None, "layer": layer_data, "layer_number": layer_obj.number if layer_obj else None, "layer_name": self.get_localized(layer_obj.name, lang) if layer_obj else None, "thumbnail": self._get_thumbnail_url(tr.thumbnail, request) if tr else None, "share_link": tr.share_link if tr else None, } curr["children_map"][tr_key] = { "data": tr_node_data, "children_map": {}, "leaf_hadiths": set() } curr = curr["children_map"][tr_key] curr["leaf_hadiths"].add(hadis.id) # Attach hadith leaf under the last narrator of this chain hadis_key = ("hadith", hadis.id) if hadis_key not in curr["children_map"]: curr["children_map"][hadis_key] = { "data": hadis_node_data, "children_map": {}, "leaf_hadiths": set() } curr["children_map"][hadis_key]["leaf_hadiths"].add(hadis.id) # Format prefix tree into recursive XMind JSON structure def format_tree_node(node_dict, path_prefix=""): node_data = dict(node_dict["data"]) node_type = node_data.get("node_type", "unknown") raw_id = node_data.get("id", "0") unique_node_id = f"{path_prefix}_{node_type}_{raw_id}" node_data["node_id"] = unique_node_id attached = [] for (child_type, child_id), child_subtree in node_dict["children_map"].items(): formatted_child = format_tree_node(child_subtree, path_prefix=unique_node_id) attached.append(formatted_child) node_data["count"] = len(attached) node_data["leaf_count"] = len(node_dict.get("leaf_hadiths", set())) node_data["children"] = { "attached": attached } return node_data root_attached = [] for (child_type, child_id), child_subtree in root_tree["children_map"].items(): root_attached.append(format_tree_node(child_subtree, path_prefix="root")) data = { "rootTopic": { "id": root_tree["id"], "node_type": "category", "slug": category.slug, "title": root_title, "count": len(root_attached), "leaf_count": len(root_tree["leaf_hadiths"]), "structureClass": "org.xmind.ui.map.unbalanced", "children": { "attached": root_attached } } } response = Response(data) response['Cache-Control'] = 'no-cache, no-store, must-revalidate' response['Pragma'] = 'no-cache' response['Expires'] = '0' return response