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') 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 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') 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 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 (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 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 (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 queryset.order_by('order', 'id') @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): if not json_field or not isinstance(json_field, list): return "" for item in json_field: if isinstance(item, dict) and item.get('language_code') == lang: return item.get('text', '') for item in json_field: if isinstance(item, dict) and item.get('language_code') == 'en': return item.get('text', '') if len(json_field) > 0 and isinstance(json_field[0], dict): return json_field[0].get('text', '') return "" 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', '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' ).order_by('chain_index', 'order') hadiths = Hadis.objects.filter( category=category, status=True ).order_by('number').prefetch_related( Prefetch('transmitters', queryset=transmitter_qs), '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 chain_index chain_groups = defaultdict(list) for ht in hadis.transmitters.all(): if ht.transmitter: chain_groups[ht.chain_index].append(ht) if not chain_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: for chain_idx, ht_list in chain_groups.items(): ht_list.sort(key=lambda x: x.order) curr = root_tree curr["leaf_hadiths"].add(hadis.id) for ht in ht_list: tr = ht.transmitter tr_key = ("narrator", tr.id) if tr_key not in curr["children_map"]: rel_obj = tr.reliability 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), } tr_node_data = { "id": f"narrator-{tr.id}", "transmitter_id": tr.id, "node_type": "narrator", "slug": tr.slug, "title": self.get_localized(tr.full_name, lang) or tr.slug, "full_name": self.get_localized(tr.full_name, lang), "arabic_full_name": self.get_arabic(tr.full_name), "kunya": self.get_localized(tr.kunya, lang), "arabic_kunya": self.get_arabic(tr.kunya), "known_as": self.get_localized(tr.known_as, lang), "arabic_known_as": self.get_arabic(tr.known_as), "nickname": self.get_localized(tr.nickname, lang), "arabic_nickname": self.get_arabic(tr.nickname), "birth_year_hijri": tr.birth_year_hijri, "death_year_hijri": tr.death_year_hijri, "age_at_death": tr.age_at_death, "generation": tr.generation, "tadlis": tr.tadlis, "ikhtilat": tr.ikhtilat, "companion_type": tr.companion_type, "madhhab": tr.madhhab, "in_sahih_muslim": tr.in_sahih_muslim, "in_sahih_bukhari": tr.in_sahih_bukhari, "reliability": rel_data, "thumbnail": self._get_thumbnail_url(tr.thumbnail, request), "share_link": tr.share_link, } 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) # Convert internal trie to the standard XMind JSON tree structure def format_tree_node(node_dict, path_prefix=""): node_data = dict(node_dict["data"]) if "data" in node_dict else {} children_map = node_dict.get("children_map", {}) # Form unique instance ID for tree canvas base_id = node_data.get("id", "node") unique_node_id = f"{path_prefix}_{base_id}" if path_prefix else base_id node_data["node_id"] = unique_node_id attached = [] for (child_type, child_id), child_subtree in 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 } } } return Response(data)