from rest_framework.authentication import TokenAuthentication from rest_framework.permissions import IsAuthenticated, AllowAny from rest_framework.generics import ListAPIView, RetrieveAPIView from drf_yasg.utils import swagger_auto_schema from drf_yasg import openapi 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 Transmitters, HadisCategory, Hadis, HadisCollection,HadisTransmitter , HadisCorrection ,HadisReference, HadisStatus ,ReferenceImage, CorrectionReference, HadisInterpretation, InterpretationReference from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer , PinnedHadisCollectionSerializer, HadisSourceDetailsSerializer 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 from django.db.models import Q from ..serializers.category import get_localized_text from rest_framework import status from utils.mixins import CanonicalSlugViewSetMixin, Gone410ViewMixin class PinnedHadisCollectionListView(ListAPIView): """ API view to list pinned hadis collections with metadata info """ serializer_class = PinnedHadisCollectionSerializer pagination_class = NoPagination permission_classes = [IsAuthenticated] authentication_classes = [TokenAuthentication] @hadis_collections_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def get_queryset(self): from django.db.models import Prefetch from apps.hadis.models import HadisInCollection return HadisCollection.objects.filter( status=True, display_position=HadisCollection.DisplayPosition.PINNED, hadis_items__hadis__status=True ).prefetch_related( Prefetch( 'hadis_items', queryset=HadisInCollection.objects.filter(hadis__status=True).order_by('order') ) ).distinct().order_by('order', '-created_at') def list(self, request, *args, **kwargs): # 1. گرفتن خروجی استاندارد لیست مجموعه‌ها response = super().list(request, *args, **kwargs) # 2. محاسبه آمار (Info) # تعداد کتگوری‌های فعال categories_count = HadisCategory.objects.count() # تعداد نریتورها (راویان) narrators_count = Transmitters.objects.count() # تعداد سورس‌ها (کتاب‌های منبع) # فرض بر این است که BookReference مدل منابع شماست from apps.hadis.models import BookReference sources_count = BookReference.objects.count() # تعداد بوکمارک‌های کاربر در سرویس حدیث from apps.bookmark.models.bookmark import Bookmark bookmarks_count = Bookmark.objects.filter( user=request.user, service=Bookmark.ServiceChoices.HADITH, status=True ).count() # تعداد نویسنده‌ها from apps.hadis.models import BookAuthor authors_count = BookAuthor.objects.count() # 3. ساختن دیکشنری info info = { "categories_count": categories_count, # "bookmarks_count": bookmarks_count, "narrators_count": narrators_count, "sources_count": sources_count, "authors_count": authors_count, } # 4. بازسازی دیتای نهایی مشابه اپ ویدیو custom_data = { "count": len(response.data) if isinstance(response.data, list) else response.data.get("count"), "next": response.data.get("next") if isinstance(response.data, dict) else None, "previous": response.data.get("previous") if isinstance(response.data, dict) else None, "info": info, "results": response.data if isinstance(response.data, list) else response.data.get("results") } return Response(custom_data, status=status.HTTP_200_OK) class HadisCollectionListView(ListAPIView): """ API view to list all hadis collections """ serializer_class = HadisCollectionListSerializer pagination_class = NoPagination @hadis_collections_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def get_queryset(self): from django.db.models import Prefetch from apps.hadis.models import HadisInCollection return HadisCollection.objects.filter( status=True, display_position=HadisCollection.DisplayPosition.MIDDLE ).prefetch_related( Prefetch( 'hadis_items', queryset=HadisInCollection.objects.filter(hadis__status=True).order_by('order') ) ).order_by('order', '-created_at') class HadisSyncView(ListAPIView): """ API view to sync all hadis data for offline mode """ serializer_class = HadisSyncSerializer pagination_class = NoPagination def get_queryset(self): qs = ( Hadis.objects .filter(status=True) .select_related('category', 'hadis_status') .prefetch_related( 'tags', Prefetch( 'transmitters', queryset=HadisTransmitter.objects.select_related( 'transmitter', 'transmitter__reliability', 'narrator_layer' ).prefetch_related( 'uncertain_transmitters', 'uncertain_transmitters__reliability' ).order_by('order') ), Prefetch( 'references', queryset=HadisReference.objects .select_related('book_reference') .prefetch_related( 'book_reference__author', Prefetch( 'images', queryset=ReferenceImage.objects.order_by('priority') ) ) ), # 👇 اضافه شدن واکشی عمیق برای تصحیحات (بدون N+1) Prefetch( 'hadiscorrection_set', queryset=HadisCorrection.objects.select_related('hadis').prefetch_related( Prefetch( 'references', queryset=CorrectionReference.objects.select_related( 'book_reference', 'book_reference__author', 'edition', 'book_volume', 'book_volume__edition' ).prefetch_related( 'images', 'book_reference__editions', 'book_reference__volumes' ) ) ) ), # 👇 اضافه شدن واکشی عمیق برای تفاسیر (بدون N+1) Prefetch( 'category__interpretations', queryset=HadisInterpretation.objects.select_related('category').prefetch_related( Prefetch( 'references', queryset=InterpretationReference.objects.select_related( 'book_reference', 'book_reference__author', 'edition', 'book_volume', 'book_volume__edition' ).prefetch_related( 'images', 'book_reference__editions', 'book_reference__volumes' ) ) ) ), ) .order_by('id') ) # Incremental sync support to prevent transferring 25MB on subsequent syncs updated_after = self.request.query_params.get('updated_after') or self.request.query_params.get('since') if updated_after: from django.utils.dateparse import parse_datetime try: dt = parse_datetime(updated_after) if dt: qs = qs.filter(updated_at__gte=dt) except Exception: pass return qs @hadis_sync_swagger def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def list(self, request, *args, **kwargs): queryset = self.get_queryset() serializer = self.get_serializer( queryset, many=True, context={'request': request}, ) return Response({ 'count': len(serializer.data), 'results': serializer.data }) from ..serializers.category import CategoryQuranVerseSerializer 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 category_data = SimpleCategory(category_obj, context={'request': request}).data # 4. Check if it's a Quran Category and fetch verse data quran_data = None if category_obj.source_type == HadisCategory.SourceType.QURAN: try: from ..models.category import CategoryQuranVerse verse_obj = CategoryQuranVerse.objects.prefetch_related( 'secondary_translations' ).get(category=category_obj) from ..serializers.category import CategoryQuranVerseSerializer quran_data = CategoryQuranVerseSerializer( verse_obj, context={'request': request} ).data except CategoryQuranVerse.DoesNotExist: pass # 👇 5. NEW LOGIC: Fetch all statuses for frontend filtering UI lang = getattr(request, "LANGUAGE_CODE", None) or "fa" statuses_list = [ { "id": status.id, "slug": status.slug, "title": get_localized_text(status.title, request=request, language_code=lang) or "", "color": status.color, "main_color_code": status.main_color_code, } for status in HadisStatus.objects.all().order_by('order') ] # 6. Inject it into the response data if isinstance(response.data, dict): 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 ordered_data['quran_data'] = quran_data # 👇 تزریق لیست استاتوس‌ها قبل از نتایج ordered_data['statuses'] = statuses_list 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( layer_count=Count('transmitters__narrator_layer', distinct=True) ).select_related('category').prefetch_related( Prefetch( 'references', queryset=HadisReference.objects.prefetch_related( Prefetch('images', queryset=ReferenceImage.objects.order_by('priority')) ) ) ) # 👇 1. Apply Search Filter search_query = self.request.query_params.get('search', None) if search_query: search_conditions = Q(text__icontains=search_query) search_conditions |= Q(title__icontains=search_query) search_conditions |= Q(title_narrator__icontains=search_query) search_conditions |= Q(translation__icontains=search_query) queryset = queryset.filter(search_conditions) # 👇 2. Apply Status Filter (Supports multiple comma-separated slugs) status_filter = self.request.query_params.get('status', None) if status_filter: status_slugs = [s.strip() for s in status_filter.split(',')] queryset = queryset.filter(hadis_status__slug__in=status_slugs) # 👇 3. Apply Transmitter Filter (Supports multiple comma-separated slugs) transmitter_filter = self.request.query_params.get('transmitter', None) if transmitter_filter: transmitter_slugs = [t.strip() for t in transmitter_filter.split(',')] queryset = queryset.filter(transmitters__transmitter__slug__in=transmitter_slugs) # 👇 4. Apply Source Filter (Supports multiple comma-separated slugs) source_filter = self.request.query_params.get('source', None) if source_filter: source_slugs = [s.strip() for s in source_filter.split(',')] queryset = queryset.filter(references__book_reference__slug__in=source_slugs) # 👇 5. Apply Source Author Filter (Supports author or source_author, multiple comma-separated slugs/IDs) author_filter = self.request.query_params.get('author', None) or self.request.query_params.get('source_author', None) if author_filter: author_slugs = [a.strip() for a in author_filter.split(',') if a.strip()] author_q = Q(references__book_reference__author__slug__in=author_slugs) numeric_ids = [int(a) for a in author_slugs if a.isdigit()] if numeric_ids: author_q |= Q(references__book_reference__author_id__in=numeric_ids) queryset = queryset.filter(author_q) # 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) # ❗️ distinct() برای جلوگیری از دابلیکیت شدن احادیث به خاطر ManyToMany ها الزامی است return queryset.distinct() 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__sect', 'hadis_status').prefetch_related( Prefetch( 'references', queryset=HadisReference.objects.select_related('book_reference__author').prefetch_related( Prefetch('images', queryset=ReferenceImage.objects.order_by('priority')) ) ) ) # 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(Gone410ViewMixin, CanonicalSlugViewSetMixin, 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' active_field_name = 'status' active_expected_value = True @hadis_detail_swagger def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) def get_queryset(self): return Hadis.objects.all().select_related( 'category', 'hadis_status' ).prefetch_related( 'tags', 'references__book_reference', 'references__book_reference__author', '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', 'status' ).prefetch_related('uncertain_transmitters').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 CategoryHadisCorrectionsView(ListAPIView): """ API view to retrieve all corrections across all hadiths belonging to a specific category """ serializer_class = HadisCorrectionSerializer authentication_classes = [TokenAuthentication] permission_classes = [AllowAny] pagination_class = StandardResultsSetPagination @swagger_auto_schema( operation_summary="Get Category Hadis Corrections", operation_description="Returns all text corrections across all hadiths belonging to a specific category, including hadis_info for each correction. Supports filtering by multiple hadiths and/or categories.", tags=['Dobodbi - Hadis (V2)'], manual_parameters=[ openapi.Parameter('search', openapi.IN_QUERY, description="Search in text or narrator", type=openapi.TYPE_STRING), openapi.Parameter('hadis_slug', openapi.IN_QUERY, description="Filter corrections by specific hadith slug (comma-separated for multiple)", type=openapi.TYPE_STRING), openapi.Parameter('hadis_slugs', openapi.IN_QUERY, description="Filter corrections by multiple hadith slugs (comma-separated)", type=openapi.TYPE_STRING), openapi.Parameter('category_slugs', openapi.IN_QUERY, description="Filter corrections by multiple category slugs (comma-separated)", type=openapi.TYPE_STRING), openapi.Parameter('is_bookmark', openapi.IN_QUERY, description="Filter only bookmarked corrections (requires auth)", type=openapi.TYPE_BOOLEAN), ] ) def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def get_queryset(self): category_slug = self.kwargs.get('category_slug') try: category_ids = [] if category_slug: category = HadisCategory.objects.get(slug=category_slug) category_ids = list(category.get_descendants(include_self=True).values_list('id', flat=True)) # Optional filter by multiple categories (selected subcategories or category slugs/IDs) cat_param_keys = ['category_slugs', 'categories', 'category_slug'] cat_slug_list = [] for key in cat_param_keys: vals = self.request.query_params.getlist(key) if hasattr(self.request.query_params, 'getlist') else [] if not vals and key in self.request.query_params: val = self.request.query_params.get(key) if val: vals = [val] if isinstance(val, str) else list(val) for v in vals: if isinstance(v, str): cat_slug_list.extend([s.strip() for s in v.split(',') if s.strip()]) elif isinstance(v, (list, tuple)): cat_slug_list.extend(v) elif v is not None: cat_slug_list.append(str(v)) if cat_slug_list: cat_filter = Q(slug__in=cat_slug_list) id_list = [int(s) for s in cat_slug_list if str(s).isdigit()] if id_list: cat_filter |= Q(id__in=id_list) selected_categories = HadisCategory.objects.filter(cat_filter) selected_cat_ids = set() for sc in selected_categories: selected_cat_ids.update(sc.get_descendants(include_self=True).values_list('id', flat=True)) if selected_cat_ids: if category_ids: category_ids = list(set(category_ids).intersection(selected_cat_ids)) else: category_ids = list(selected_cat_ids) else: return HadisCorrection.objects.none() hadis_filter_kwargs = {'status': True} if category_ids: hadis_filter_kwargs['category_id__in'] = category_ids hadis_ids = Hadis.objects.filter(**hadis_filter_kwargs).values_list('id', flat=True) queryset = HadisCorrection.objects.filter( hadis_id__in=hadis_ids ).select_related('hadis').prefetch_related( 'references', 'references__images' ).order_by('priority', '-created_at') # Optional search query filter search_query = self.request.query_params.get('search', None) if search_query: search_conditions = ( Q(title__icontains=search_query) | Q(text__icontains=search_query) | Q(narrator__icontains=search_query) | Q(translation__icontains=search_query) | Q(hadis__title__icontains=search_query) | Q(hadis__title_narrator__icontains=search_query) ) queryset = queryset.filter(search_conditions) # Optional filter by specific hadith slug(s) or ID(s) hadis_param_keys = ['hadis_slug', 'hadis_slugs', 'hadiths'] hadis_slug_list = [] for key in hadis_param_keys: vals = self.request.query_params.getlist(key) if hasattr(self.request.query_params, 'getlist') else [] if not vals and key in self.request.query_params: val = self.request.query_params.get(key) if val: vals = [val] if isinstance(val, str) else list(val) for v in vals: if isinstance(v, str): hadis_slug_list.extend([s.strip() for s in v.split(',') if s.strip()]) elif isinstance(v, (list, tuple)): hadis_slug_list.extend(v) elif v is not None: hadis_slug_list.append(str(v)) if hadis_slug_list: hadis_query = Q(hadis__slug__in=hadis_slug_list) id_list = [int(s) for s in hadis_slug_list if str(s).isdigit()] if id_list: hadis_query |= Q(hadis__id__in=id_list) queryset = queryset.filter(hadis_query) # 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 HadisCategory.DoesNotExist: return HadisCorrection.objects.none() def list(self, request, *args, **kwargs): response = super().list(request, *args, **kwargs) category_slug = self.kwargs.get('category_slug') category_obj = HadisCategory.objects.filter(slug=category_slug).first() if category_slug else None category_data = SimpleCategory(category_obj, context={'request': request}).data if category_obj else None available_hadiths = [] available_categories = [] if category_obj: cat_ids = category_obj.get_descendants(include_self=True).values_list('id', flat=True) hadiths_in_cat = Hadis.objects.filter( category_id__in=cat_ids, status=True ).order_by('number', 'id') for h in hadiths_in_cat: h_title = get_localized_text(h.title, request) if hasattr(h, 'title') and h.title else f"Hadith {h.number}" available_hadiths.append({ "id": h.id, "number": h.number, "slug": h.slug, "title": h_title, }) subcats = category_obj.get_children() if hasattr(category_obj, 'get_children') else category_obj.children.all() for sc in subcats: sc_title = get_localized_text(sc.title, request) if hasattr(sc, 'title') and sc.title else sc.slug available_categories.append({ "id": sc.id, "slug": sc.slug, "title": sc_title, }) if isinstance(response.data, dict): ordered_data = { 'count': response.data.get('count', 0), 'next': response.data.get('next'), 'previous': response.data.get('previous'), 'current_category': category_data, 'available_hadiths': available_hadiths, 'available_categories': available_categories, 'results': response.data.get('results', []), } response.data = ordered_data return response 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) # Return the layer objects for this hadis ordered by number return NarratorLayer.objects.filter(hadis=hadis).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 (excluding empty leaf categories) from django.db.models import Count, Q categories = [] for category in HadisCategory.objects.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'): 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) from rest_framework.views import APIView from rest_framework.response import Response from rest_framework.permissions import AllowAny from rest_framework.authentication import TokenAuthentication from drf_yasg.utils import swagger_auto_schema from django.utils.translation import get_language from apps.hadis.models import HadisStatus, HadisCategory, BookReference, Transmitters from drf_yasg import openapi class HadisFiltersSyncAPIView(APIView): """ API view to return all filter data for hadis (Sync API for Mobile App) Returns statuses, categories, sources (books), and narrators. Optimized for offline client-side searching and sorting. """ permission_classes = [AllowAny] authentication_classes = [TokenAuthentication] @swagger_auto_schema( operation_description=( "Sync API for Flutter/mobile clients: returns flat hadis filter data for offline " "searching and local filtering. Titles are localized from multilingual fields based " "on the active request language." ), operation_summary="Sync Hadis Filters", tags=['Dobodbi - Hadis'], manual_parameters=[ openapi.Parameter( 'Accept-Language', openapi.IN_HEADER, description="Language code for localized titles, for example `en`, `fa`, `ar`, `ur`, or `ru`.", type=openapi.TYPE_STRING, required=False, ) ], responses={ status.HTTP_200_OK: openapi.Response( description="Flat filter payload for hadis categories, statuses, sources, and narrators.", examples={ "application/json": { "statuses": [ { "id": 133, "slug": "sahih", "title": "Authentic", "order": 1, "color": "green", "main_color_code": "#1DAC43" } ], "categories": [ { "id": 1036, "slug": "general-hadiths", "title": "General Hadiths", "order": 1 } ], "sources": [ { "id": 2538, "slug": "man-la-yahduruhu-al-faqih", "title": "Man La Yahduruhu al-Faqih", "search_keywords": "Shaykh al-Saduq" } ], "narrators": [ { "id": 501, "slug": "muhammad-ibn-muslim", "title": "Muhammad ibn Muslim", "search_aliases": "Abu Ja'far" } ] } } ) } ) def get(self, request, *args, **kwargs): # تشخیص زبان برای ارسال دیتای لوکالایز شده lang = getattr(request, "LANGUAGE_CODE", None) or get_language() or "ru" # ۱. استتوس‌ها (مرتب‌شده بر اساس فیلد order) statuses = [ { "id": status.id, "slug": status.slug, "title": get_localized_text(status.title, request=request, language_code=lang) or "", "order": status.order, "color": status.color, "main_color_code": status.main_color_code, } for status in HadisStatus.objects.all().order_by('order') ] # ۲. دسته‌بندی‌ها (مرتب‌شده بر اساس فیلد order، به استثنای دسته‌بندی‌های خالی) from django.db.models import Count, Q categories = [ { "id": cat.id, "slug": cat.slug, "title": get_localized_text(cat.title, request=request, language_code=lang) or "", "order": cat.order, } for cat in HadisCategory.objects.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') ] # ۳. منابع / کتاب‌ها sources = [] # استفاده از select_related برای جلوگیری از مشکل N+1 Query هنگام گرفتن نویسنده‌ها books = BookReference.objects.select_related('author').all().order_by('order', '-id') for book in books: # استخراج نام نویسنده‌ها برای سرچ آفلاین فلاتر authors_names = [ get_localized_text(book.author.name, request=request, language_code=lang) or "" ] if book.author else [] sources.append({ "id": book.id, "slug": book.slug, "title": get_localized_text(book.title, request=request, language_code=lang) or "", # تمام اسم‌های نویسنده‌ها را به هم می‌چسبانیم تا فلاتر بتواند روی آن‌ها سرچ بزند # "search_keywords": " ".join(authors_names).strip() }) # ۴. راویان (نریتورها) narrators = [] transmitters = Transmitters.objects.all().order_by('id') for narrator in transmitters: # گرفتن نام‌های مستعار known_as = get_localized_text(narrator.known_as, request=request, language_code=lang) or "" nickname = get_localized_text(narrator.nickname, request=request, language_code=lang) or "" kunya = get_localized_text(narrator.kunya, request=request, language_code=lang) or "" # تجمیع نام‌های جایگزین search_aliases = f"{known_as} {nickname} {kunya}".strip() narrators.append({ "id": narrator.id, "slug": narrator.slug, "title": get_localized_text(narrator.full_name, request=request, language_code=lang) or "", # فلاتر روی این فیلد + title سرچ می‌زند # "search_aliases": search_aliases }) # خروجی نهایی بدون پجینیشن return Response({ "statuses": statuses, "categories": categories, "sources": sources, "narrators": narrators }) from ..serializers.hadis import HadisCorrectionDetailSerializer, HadisInterpretationDetailSerializer from ..models import HadisInterpretation class HadisCorrectionDetailView(RetrieveAPIView): """API View to get full details of a specific HadisCorrection by slug""" serializer_class = HadisCorrectionDetailSerializer lookup_field = 'slug' lookup_url_kwarg = 'correction_slug' permission_classes = [AllowAny] @swagger_auto_schema( operation_summary="Get Hadis Correction Detail", operation_description="Returns the full details of a specific hadis correction, including text, translations, and its book references.", tags=['Dobodbi - Hadis (V2)'], ) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def get_queryset(self): return HadisCorrection.objects.all().select_related( 'hadis' ).prefetch_related( 'references__book_reference__author', 'references__edition', 'references__book_volume', 'references__images' ) class HadisInterpretationDetailView(RetrieveAPIView): """API View to get full details of a specific HadisInterpretation by slug""" serializer_class = HadisInterpretationDetailSerializer lookup_field = 'slug' # 👈 تغییر به slug lookup_url_kwarg = 'interpretation_slug' permission_classes = [AllowAny] @swagger_auto_schema( operation_summary="Get Hadis Interpretation Detail", operation_description="Returns the full details of a specific hadis interpretation, including text, translations, and its book references.", tags=['Dobodbi - Hadis (V2)'], ) def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) def get_queryset(self): return HadisInterpretation.objects.all().select_related( 'category' ).prefetch_related( 'references__book_reference__author', 'references__edition', 'references__book_volume', 'references__images' ) # 👈 هندل کردن حالت امن برای زمانی که چند تفسیر در یک کتگوری باشند def get_object(self): queryset = self.filter_queryset(self.get_queryset()) lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} obj = queryset.filter(**filter_kwargs).first() # گرفتن اولین آبجکت if not obj: raise Http404("No HadisInterpretation matches the given query.") self.check_object_permissions(self.request, obj) return obj class HadisInterpretsListView(ListAPIView): """ API View to list all interpretations for the category of a specific Hadis. """ serializer_class = HadisInterpretationDetailSerializer authentication_classes = [TokenAuthentication] permission_classes = [AllowAny] pagination_class = StandardResultsSetPagination lookup_field = 'slug' lookup_url_kwarg = 'hadis_slug' @swagger_auto_schema( operation_summary="Get Hadis Interpretations", operation_description="Returns a paginated list of all interpretations registered for the category of this hadis, with full details and references.", tags=['Dobodbi - Hadis (V2)'], ) def get(self, request, *args, **kwargs): return self.list(request, *args, **kwargs) def get_queryset(self): hadis_slug = self.kwargs.get('hadis_slug') try: hadis = Hadis.objects.select_related('category').get(slug=hadis_slug, status=True) if not hadis.category_id: return HadisInterpretation.objects.none() return HadisInterpretation.objects.filter( category_id=hadis.category_id ).select_related( 'category' ).prefetch_related( 'references__book_reference__author', 'references__edition', 'references__book_volume', 'references__images' ).order_by('priority', 'id') except Hadis.DoesNotExist: return HadisInterpretation.objects.none() class HadisSourceDetailsView(RetrieveAPIView): """ API view to retrieve lightweight source details (addresses, links, reference images) for a hadis/argument on demand when opening modals. Supports lookup by slug or by numeric id. """ serializer_class = HadisSourceDetailsSerializer permission_classes = [AllowAny] lookup_field = 'slug' lookup_url_kwarg = 'hadis_slug' @swagger_auto_schema( operation_summary="Get Hadis Source Details", operation_description="Returns lightweight source details (addresses, links, images) for a specific hadis by slug or ID.", tags=['Dobodbi - Hadis (V2)'], ) def get(self, request, *args, **kwargs): return self.retrieve(request, *args, **kwargs) def get_object(self): slug_or_id = self.kwargs.get(self.lookup_url_kwarg or self.lookup_field) queryset = Hadis.objects.filter(status=True).select_related( 'category' ).prefetch_related( Prefetch( 'references', queryset=HadisReference.objects.prefetch_related( Prefetch('images', queryset=ReferenceImage.objects.order_by('priority')) ) ) ) if slug_or_id.isdigit(): obj = queryset.filter(id=int(slug_or_id)).first() else: obj = queryset.filter(slug=slug_or_id).first() if not obj: from django.http import Http404 raise Http404("No Hadis matches the given query.") self.check_object_permissions(self.request, obj) return obj