from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated from rest_framework.generics import ListAPIView, RetrieveAPIView from django.shortcuts import get_object_or_404 from utils.pagination import NoPagination, StandardResultsSetPagination from rest_framework.pagination import PageNumberPagination from rest_framework.response import Response from django.db.models import Count from django.db.models import Prefetch from ..serializers.category import get_localized_text from ..models import HadisCategory, Hadis, HadisCollection,HadisTransmitter , HadisCorrection ,HadisReference, HadisStatus ,ReferenceImage from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer from ..docs import arguments_filters_swagger ,hadis_list_swagger, hadis_detail_swagger, hadis_collections_swagger, hadis_sync_swagger, hadis_transmitters_swagger, hadis_corrections_swagger, hadis_basic_swagger, hadis_main_list_swagger, hadis_layers_swagger class HadisCollectionListView(ListAPIView): """ API view to list all hadis collections """ queryset = HadisCollection.objects.filter(status=True).order_by('order') serializer_class = HadisCollectionListSerializer pagination_class = NoPagination @hadis_collections_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) class HadisSyncView(ListAPIView): """ API view to sync all hadis data for offline mode """ serializer_class = HadisSyncSerializer pagination_class = NoPagination def get_queryset(self): return ( Hadis.objects .filter(status=True) .select_related('category', 'hadis_status') .prefetch_related( 'tags', # 1. OPTIMIZED TRANSMITTERS Prefetch( 'transmitters', queryset=HadisTransmitter.objects.select_related( 'transmitter', 'transmitter__reliability', # <--- Fixes N+1 for reliability text 'narrator_layer' ).order_by('order') ), # 2. OPTIMIZED REFERENCES Prefetch( 'references', queryset=HadisReference.objects .select_related('book_reference') .prefetch_related( 'book_reference__authors', # <--- Fixes N+1 for Image ordering Prefetch( 'images', queryset=ReferenceImage.objects.order_by('priority') ) ) ), 'hadiscorrection_set', ) .order_by('id') ) @hadis_sync_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def list(self, request, *args, **kwargs): from django.utils.translation import get_language # 1. PRE-FETCH DATA ONCE queryset = self.get_queryset() # Get language once for the entire bulk operation lang = getattr(request, "LANGUAGE_CODE", None) or get_language() or "en" # Pre-calculate base URL for images media_url = request.build_absolute_uri('/')[:-1] # Remove trailing slash results = [] for obj in queryset: # --- Detail Block --- status_block = None if obj.hadis_status: status_block = { 'id': obj.hadis_status.id, 'title': get_localized_text(obj.hadis_status.title, language_code=lang), 'color': obj.hadis_status.color, 'main_color_code': obj.hadis_status.main_color_code, } tags_block = [ {'id': tag.id, 'title': get_localized_text(tag.title, language_code=lang)} for tag in obj.tags.all() ] references_block = [] reference_images_block = [] for ref in obj.references.all(): book = ref.book_reference references_block.append({ 'id': ref.id, 'title': get_localized_text(book.title, language_code=lang) if book else None, 'authors': [ {'id': a.id, 'name': get_localized_text(a.name, language_code=lang)} for a in (book.authors.all() if book else []) ], 'description': book.description if book else None, }) for img in ref.images.all(): reference_images_block.append({ 'id': img.id, 'thumbnail': f"{media_url}{img.thumbnail.url}" if img.thumbnail else None, 'priority': img.priority, }) address_details_list = [] if hasattr(obj, 'address_details') and obj.address_details and isinstance(obj.address_details, list): address_details_list = sorted(obj.address_details, key=lambda x: x.get('priority', 0)) detail_block = { 'address': get_localized_text(obj.address, language_code=lang), 'address_details': address_details_list, 'hadis_status': status_block, 'status_text': get_localized_text(obj.hadis_status_text, language_code=lang), 'share_link': obj.share_link, 'links': obj.links, 'tags': tags_block, 'references': references_block, 'reference_images': reference_images_block, } # --- Narrators Block --- transmitters_data = [] for tr_rel in obj.transmitters.all(): t = tr_rel.transmitter layer = tr_rel.narrator_layer rel_data = None if t.reliability: rel_data = { 'id': t.reliability.id, 'title': get_localized_text(t.reliability.title, language_code=lang), 'color': t.reliability.color, 'main_color_code': t.reliability.main_color_code, } transmitters_data.append({ 'id': t.id, 'name': get_localized_text(t.full_name, language_code=lang), 'slug': t.slug, 'known_as': get_localized_text(t.known_as, language_code=lang), 'nickname': get_localized_text(t.nickname, language_code=lang), 'reliability': rel_data, 'layer_level': layer.number if layer else None, 'layer_name': get_localized_text(layer.name, language_code=lang) if layer else None, 'is_gap': tr_rel.is_gap, 'birth_year_hijri': t.birth_year_hijri, 'death_year_hijri': t.death_year_hijri, 'order': tr_rel.order, }) narrators_block = { 'description': get_localized_text(obj.description, language_code=lang), 'transmitters': transmitters_data, } # --- Explanations (Complex Logic) --- explanation_data = [] if hasattr(obj, 'explanations') and obj.explanations and isinstance(obj.explanations, list): for item in obj.explanations: if isinstance(item, dict) and item.get('language_code') == lang: explanation_data.append({ 'title': item.get('title', ''), 'description': item.get('description', '') }) if not explanation_data and obj.explanation: explanation_data = get_localized_text(obj.explanation, language_code=lang) else: explanation_data = explanation_data if explanation_data else None # --- Corrections --- corrections_block = [] for c in obj.hadiscorrection_set.all(): corrections_block.append({ 'id': c.id, 'title': get_localized_text(c.title, language_code=lang), 'description': get_localized_text(c.description, language_code=lang), 'translation': get_localized_text(c.translation, language_code=lang), 'share_link': c.share_link, }) # --- Assemble Hadis Item --- results.append({ 'id': obj.id, 'number': obj.number, 'slug': obj.slug, 'category_id': obj.category_id, 'title': get_localized_text(obj.title, language_code=lang), 'title_narrator': get_localized_text(obj.title_narrator, language_code=lang), 'text': obj.text, # Usually JSON structure in our app 'translation': get_localized_text(obj.translation, language_code=lang), 'detail': detail_block, 'narrators': narrators_block, 'explanations': explanation_data, 'corrections': corrections_block, }) return Response({ 'count': len(results), 'results': results }) class HadisListView(ListAPIView): """ API view to list Hadis by category_id """ serializer_class = HadisListSerializer pagination_class = StandardResultsSetPagination authentication_classes = [TokenAuthentication] @hadis_list_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('category_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): category_slug = self.kwargs.get('category_slug') if not HadisCategory.objects.filter(slug=category_slug).exists(): return Hadis.objects.none() queryset = Hadis.objects.filter( category__slug=category_slug, status=True ).annotate( # distinct=True is CRITICAL here. # Without it, if 3 narrators are from "Layer 1", it counts as 3. # With it, it counts as 1 (unique layer). layer_count=Count('transmitters__narrator_layer', distinct=True) ).select_related('category') # Filter by bookmarks if provided is_bookmark = self.request.query_params.get('is_bookmark', '').lower() if is_bookmark == 'true' and self.request.user.is_authenticated: from apps.bookmark.models.bookmark import Bookmark bookmarked_ids = Bookmark.objects.filter( user=self.request.user, service=Bookmark.ServiceChoices.HADITH, status=True ).values_list('content_id', flat=True) queryset = queryset.filter(id__in=bookmarked_ids) return queryset def get_serializer_context(self): """Add user bookmarks to serializer context to avoid caching issues""" context = super().get_serializer_context() # Add user's bookmarked hadis IDs to context user = self.request.user if user.is_authenticated: from apps.bookmark.models.bookmark import Bookmark user_bookmarks = Bookmark.objects.filter( user=user, service=Bookmark.ServiceChoices.HADITH, status=True ).values_list('content_id', flat=True) context['user_bookmarked_hadis_ids'] = set(user_bookmarks) else: context['user_bookmarked_hadis_ids'] = set() return context class HadisMainListView(ListAPIView): """ API view to list Hadis by category_id """ serializer_class = HadisListSerializer authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] pagination_class = StandardResultsSetPagination @hadis_main_list_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def get_queryset(self): # queryset = Hadis.objects.select_related('category', 'hadis_status') queryset = Hadis.objects.select_related('category__sect', 'hadis_status') # Get search parameters search_query = self.request.query_params.get('search', None) status_filter = self.request.query_params.get('status', None) category_filter = self.request.query_params.get('category', None) # Apply search filter if search_query: queryset = self.apply_search_filter(queryset, search_query) # Apply status filter if status_filter: queryset = queryset.filter(hadis_status__title__icontains=status_filter) # Apply category filter if category_filter: queryset = queryset.filter(category__title__icontains=category_filter) # Filter by bookmarks if provided is_bookmark = self.request.query_params.get('is_bookmark', '').lower() if is_bookmark == 'true' and self.request.user.is_authenticated: from apps.bookmark.models.bookmark import Bookmark bookmarked_ids = Bookmark.objects.filter( user=self.request.user, service=Bookmark.ServiceChoices.HADITH, status=True ).values_list('content_id', flat=True) queryset = queryset.filter(id__in=bookmarked_ids) return queryset def list(self, request, *args, **kwargs): queryset = self.get_queryset() # Apply pagination page = self.paginate_queryset(queryset) if page is not None: serializer = self.get_serializer(page, many=True) paginated_response = self.get_paginated_response(serializer.data) # Get category titles category_titles = self.get_category_titles(request) # Get status titles status_titles = self.get_status_titles(request) # Modify the paginated response to include our custom data response_data = paginated_response.data # response_data['category_titles'] = self.get_cached_category_titles(request) # response_data['status_titles'] = self.get_cached_status_titles(request) return Response(response_data) # Fallback for when pagination is disabled serializer = self.get_serializer(queryset, many=True) return Response({ 'count': queryset.count(), 'results': serializer.data }) return Response(response_data) def get_category_titles(self,request): """Get list of category titles based on language""" from ..models import HadisCategory categories = HadisCategory.objects.all() category_titles = [] for category in categories: title = get_localized_text(category.title,request) category_titles.append(title) return category_titles def get_status_titles(self, request): """Get list of status titles based on language""" from ..models import HadisStatus statuses = HadisStatus.objects.all().order_by('order') status_titles = [] for status in statuses: title = get_localized_text(status.title,request) status_titles.append(title) return status_titles def apply_search_filter(self, queryset, search_query): """ Apply search filter across multiple fields including JSONFields. Searches in: title, title_narrator, text, translation """ from django.db.models import Q # Basic search conditions search_conditions = Q(text__icontains=search_query) # For JSONFields, search in the JSON string representation # This will find matches in the "text" values within the JSON arrays search_conditions |= Q(title__icontains=search_query) search_conditions |= Q(title_narrator__icontains=search_query) search_conditions |= Q(translation__icontains=search_query) return queryset.filter(search_conditions) #we add this later # def get_cached_category_titles(self, request): # """Fetches categories, cached for 1 hour to reduce DB load""" # lang = getattr(request, "LANGUAGE_CODE", "en") # cache_key = f"hadis_meta_categories_{lang}" # data = cache.get(cache_key) # if not data: # # If not in cache, fetch from DB # from ..models import HadisCategory # categories = HadisCategory.objects.all().only('id', 'title') # Fetch only needed fields # # Build list # data = [get_localized_text(c.title, request) for c in categories] # # Save to cache # cache.set(cache_key, data, timeout=60 * 60) # 1 Hour # return data # def get_cached_status_titles(self, request): # """Fetches statuses, cached for 1 hour""" # lang = getattr(request, "LANGUAGE_CODE", "en") # cache_key = f"hadis_meta_statuses_{lang}" # data = cache.get(cache_key) # if not data: # from ..models import HadisStatus # statuses = HadisStatus.objects.all().order_by('order') # data = [get_localized_text(s.title, request) for s in statuses] # cache.set(cache_key, data, timeout=60 * 60) # return data class HadisBasicView(RetrieveAPIView): """ API view to retrieve basic Hadis information by hadis_slug """ serializer_class = HadisBasicSerializer lookup_field = 'slug' lookup_url_kwarg = 'hadis_slug' authentication_classes = [TokenAuthentication] @hadis_basic_swagger def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) def get_queryset(self): return Hadis.objects.filter(status=True) def get_serializer_context(self): """Add user bookmarks to serializer context to avoid caching issues""" context = super().get_serializer_context() # Add user's bookmarked hadis IDs to context user = self.request.user if user.is_authenticated: from apps.bookmark.models.bookmark import Bookmark user_bookmarks = Bookmark.objects.filter( user=user, service=Bookmark.ServiceChoices.HADITH, status=True ).values_list('content_id', flat=True) context['user_bookmarked_hadis_ids'] = set(user_bookmarks) else: context['user_bookmarked_hadis_ids'] = set() return context class HadisDetailView(RetrieveAPIView): """ API view to retrieve detailed Hadis information by hadis_slug (excluding transmitters and corrections) """ serializer_class = HadisDetailSerializer lookup_field = 'slug' lookup_url_kwarg = 'hadis_slug' @hadis_detail_swagger def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) def get_queryset(self): return Hadis.objects.filter(status=True).select_related( 'category', 'hadis_status' ).prefetch_related( 'tags', 'references__book_reference', 'references__book_reference__authors', 'references__images', ) class HadisTransmittersView(RetrieveAPIView): """ Fetches a single Hadis but filters the nested Transmitters list if a ?layer=slug param is provided. """ serializer_class = HadisTransmitterListSerializer lookup_field = 'slug' lookup_url_kwarg = 'hadis_slug' pagination_class = NoPagination @hadis_transmitters_swagger def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) def get_queryset(self): # 1. Get the filter param layer_slug = self.request.query_params.get('layer') # 2. Build the query for the "Child" (Transmitters) # We start with the base optimization (select_related) transmitter_qs = HadisTransmitter.objects.select_related( 'transmitter', 'narrator_layer' ).order_by('order') # 3. Apply the filter to the Child Query (if param exists) if layer_slug: # Assumes 'NarratorLayer' has a 'slug' field. # If not, use 'narrator_layer__name' or 'narrator_layer__id'. transmitter_qs = transmitter_qs.filter(narrator_layer__slug=layer_slug) # 4. Use the Prefetch object to inject this filtered list into the Parent return Hadis.objects.filter(status=True).prefetch_related( Prefetch('transmitters', queryset=transmitter_qs) ) class HadisCorrectionsView(ListAPIView): """ API view to retrieve corrections for a specific hadis """ serializer_class = HadisCorrectionSerializer authentication_classes = [TokenAuthentication] permission_classes = [IsAuthenticated] pagination_class = StandardResultsSetPagination lookup_field = 'slug' lookup_url_kwarg = 'hadis_slug' @hadis_corrections_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) # hadis = self.get_object() # corrections_data = [] # for correction in hadis.hadiscorrection_set.all(): # correction_info = { # 'id': correction.id, # 'title': correction.title, # 'description': correction.description, # 'translation': correction.translation # } # corrections_data.append(correction_info) # return Response({ # 'hadis_id': hadis.id, # 'corrections_count': len(corrections_data), # 'corrections': corrections_data # }) def get_queryset(self): hadis_slug = self.kwargs.get('hadis_slug') try: hadis = Hadis.objects.get(slug=hadis_slug, status=True) if not HadisCorrection.objects.filter(hadis=hadis).exists(): return Hadis.objects.none() queryset = HadisCorrection.objects.filter(hadis=hadis) # Filter by bookmarks if provided is_bookmark = self.request.query_params.get('is_bookmark', '').lower() if is_bookmark == 'true' and self.request.user.is_authenticated: from apps.bookmark.models.bookmark import Bookmark bookmarked_ids = Bookmark.objects.filter( user=self.request.user, service=Bookmark.ServiceChoices.HADITH_CORRECTION, status=True ).values_list('content_id', flat=True) queryset = queryset.filter(id__in=bookmarked_ids) return queryset except Hadis.DoesNotExist: return HadisCorrection.objects.none() class HadisLayersView(ListAPIView): """ API view to retrieve all narrator layers for a specific hadis """ serializer_class = NarratorLayerSerializer pagination_class = NoPagination lookup_field = 'slug' lookup_url_kwarg = 'hadis_slug' @hadis_layers_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def get_queryset(self): from ..models import NarratorLayer hadis_slug = self.kwargs.get('hadis_slug') # Get the hadis object to ensure it exists hadis = get_object_or_404(Hadis, slug=hadis_slug, status=True) # Get all distinct narrator layer IDs for this hadis layer_ids = HadisTransmitter.objects.filter( hadis=hadis ).values_list('narrator_layer', flat=True).distinct() # Filter out None values (transmitters without layers) layer_ids = [lid for lid in layer_ids if lid is not None] # Return the layer objects ordered by number return NarratorLayer.objects.filter(id__in=layer_ids).order_by('number') class HadisFiltersView(ListAPIView): """ API view to return filter data for hadis Returns statuses and categories for filtering """ pagination_class = NoPagination @arguments_filters_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def get_queryset(self): # This view doesn't need a queryset, it returns computed data return Hadis.objects.none() def list(self, request, *args, **kwargs): # Get statuses from HadisStatus model statuses = [] for status in HadisStatus.objects.all().order_by('order'): title_text = get_localized_text(status.title, request) if title_text and status.slug: statuses.append({ 'text': title_text, 'slug': status.slug }) # Get categories from HadisCategory model categories = [] for category in HadisCategory.objects.all().order_by('order'): title_text = get_localized_text(category.title, request) if title_text and category.slug: categories.append({ 'text': title_text, 'slug': category.slug }) response_data = { 'statuses': statuses, 'categories': categories } return Response(response_data)