diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 6f102f0..cf77941 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -453,11 +453,12 @@ class TransmitterSyncSerializer(serializers.ModelSerializer): original_texts = serializers.SerializerMethodField() full_name = LocalizedField() hadis = serializers.SerializerMethodField() + relations = serializers.SerializerMethodField() class Meta: model = Transmitters fields = [ - 'id', 'full_name','slug' ,'biographical', 'scholars_opinions', 'original_texts','hadis' + 'id', 'full_name','slug' ,'biographical', 'scholars_opinions', 'original_texts','hadis','relations' ] def get_biographical(self, obj): @@ -540,6 +541,20 @@ class TransmitterSyncSerializer(serializers.ModelSerializer): # لیست تمام رکوردهای واسط را می‌گیریم. # استفاده از set باعث می‌شود اگر یک راوی در ۲ سند از یک حدیث بود، آیدی آن حدیث تکراری نشود. return list(set(item.hadis_id for item in obj.hadises.all() if item.hadis_id)) + def get_relations(self, obj): + """ + استخراج آنی آیدی اساتید و شاگردان از کانتکست بدون اجرای کوئری اضافه + """ + teachers_map = self.context.get('teachers_map', {}) + students_map = self.context.get('students_map', {}) + + # حتماً آیدی را به عدد صحیح تبدیل می‌کنیم تا از مچ شدن با دیکشنری مطمئن شویم + obj_id = int(obj.id) + + return { + "teachers": list(teachers_map.get(obj_id, set())), + "students": list(students_map.get(obj_id, set())) + } @@ -922,3 +937,26 @@ class NarratorLayerSerializer(serializers.Serializer): """Get localized description""" request = self.context.get('request') return get_localized_text(obj.description, request=request) + + +class NarratorRelationSerializer(serializers.ModelSerializer): + """ + سریالایزر سبک برای نمایش راویان به عنوان استاد یا شاگرد + """ + full_name = LocalizedField() + known_as = LocalizedField() + reliability = SerializerMethodField() + + class Meta: + model = Transmitters + fields = ['id', 'slug', 'full_name', 'known_as', 'birth_year_hijri', 'death_year_hijri','reliability'] + + def get_reliability(self, obj): + """Serialize the reliability foreign key""" + if obj.reliability: + return { + 'id': obj.reliability.id, + 'title': get_localized_text(obj.reliability.title, self.context.get('request')), + 'color': obj.reliability.color, + 'main_color_code': obj.reliability.main_color_code, + } \ No newline at end of file diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index d344bbb..fa9796b 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -4,7 +4,7 @@ from .views.category import HadisCategorySectListView, HadisCategoryTreeView, Ca from .views.hadis import HadisCollectionListView, HadisListView, HadisBasicView, HadisDetailView, HadisSyncView, HadisTransmittersView, HadisCorrectionsView,HadisMainListView, HadisFiltersView, HadisLayersView,PinnedHadisCollectionListView, HadisFiltersSyncAPIView from .views.transmitter import ( TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView, - NarratorArgumentsListView + NarratorArgumentsListView , NarratorRelationsView ) from .views.reference import BookDetailView, BookReferencesView, BookReferenceSyncView, BookAttributeView from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncView, BookReferenceV2ListView @@ -92,6 +92,7 @@ urlpatterns = [ path('arguments/filters/', HadisFiltersView.as_view(), name='hadis-filters'), # Narrator paths + path('narrators//relations/', NarratorRelationsView.as_view(), name='narrator-relations'), path('narrators//arguments', NarratorArgumentsListView.as_view(), name='narrator-arguments'), path('narrators//opinions', TransmitterOpinionView.as_view(), name='narrator-opinions'), path('narrators//original_texts', TransmitterOriginalTextView.as_view(), name='narrator-original-texts'), diff --git a/apps/hadis/views/transmitter.py b/apps/hadis/views/transmitter.py index a807083..e7a3969 100644 --- a/apps/hadis/views/transmitter.py +++ b/apps/hadis/views/transmitter.py @@ -2,7 +2,15 @@ from django.contrib.admin.utils import lookup_field from django.db.models import Prefetch, Q from rest_framework.generics import ListAPIView , RetrieveAPIView from rest_framework.response import Response +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from django.shortcuts import get_object_or_404 +from django.db.models import F +from drf_yasg.utils import swagger_auto_schema +from ..models import Transmitters, HadisTransmitter +from ..serializers import NarratorRelationSerializer from utils.pagination import StandardResultsSetPagination from ..models import Transmitters , TransmitterOpinion, TransmitterOriginalText, Hadis from ..serializers import TransmitterSerializer , TransmitterDetailSerializer, TransmitterSyncSerializer,TransmitterOpinionSerializer, TransmitterOriginalTextSerializer, HadisListSerializer @@ -114,7 +122,7 @@ class TransmitterOriginalTextView(ListAPIView): class TransmitterSyncView(ListAPIView): """ API view to sync all transmitter data for offline mode - Returns all transmitters with biographical data and scholarly opinions + Returns all transmitters with biographical data, opinions, and exact flat relation IDs. """ serializer_class = TransmitterSyncSerializer pagination_class = NoPagination @@ -124,9 +132,6 @@ class TransmitterSyncView(ListAPIView): return self.list(request, *args, **kwargs) def get_queryset(self): - """ - Prefetch ALL related data at once - """ return ( Transmitters.objects .prefetch_related( @@ -138,11 +143,57 @@ class TransmitterSyncView(ListAPIView): ) def list(self, request, *args, **kwargs): + # 🧠 ۱. واکشی لینک‌ها با values_list برای جلوگیری از هرگونه تداخل دیکشنری + all_links = HadisTransmitter.objects.values_list( + 'hadis_id', 'chain_index', 'order', 'transmitter_id' + ) + + # 🧠 ۲. بازسازی کل زنجیره‌های حدیثی به صورت کاملاً ایمن و عددی + chains = {} + for hadis_id, chain_index, order, transmitter_id in all_links: + if transmitter_id is None: + continue + + key = (int(hadis_id), int(chain_index)) + if key not in chains: + chains[key] = {} + chains[key][int(order)] = int(transmitter_id) + + # دایرکتوری‌های نگاشت روابط + teachers_map = {} + students_map = {} + + # 🧠 ۳. پردازش خطی ساختار درختواره + for key, orders in chains.items(): + for order, tx_id in orders.items(): + if tx_id not in teachers_map: + teachers_map[tx_id] = set() + if tx_id not in students_map: + students_map[tx_id] = set() + + # استاد (پوزیشن قبلی در ترتیب روایی / order + 1) + if (order + 1) in orders: + teachers_map[tx_id].add(orders[order + 1]) + + # شاگرد (پوزیشن بعدی در ترتیب روایی / order - 1) + if (order - 1) in orders: + students_map[tx_id].add(orders[order - 1]) + queryset = self.get_queryset() - serializer = self.get_serializer(queryset, many=True, context={'request': request}) + + # 🧠 ۴. تزریق اجباری کانتکست به سریالایزر (جلوگیری از اوررایت شدن توسط DRF) + context = self.get_serializer_context() + context['teachers_map'] = teachers_map + context['students_map'] = students_map + + serializer = self.get_serializer_class()( + queryset, + many=True, + context=context + ) response_data = { - 'count': len(serializer.data), # ← No extra query! + 'count': len(serializer.data), 'filters': get_transmitter_filters(request), 'results': serializer.data } @@ -223,4 +274,124 @@ class NarratorArgumentsListView(ListAPIView): ).select_related( 'category__sect', 'hadis_status' - ).distinct().order_by('id') \ No newline at end of file + ).distinct().order_by('id') + + +# class NarratorRelationsView(APIView): +# """ +# API view to get the Teachers and Students of a specific narrator +# based on their position in Hadith transmission chains. +# """ +# permission_classes = [AllowAny] + +# @swagger_auto_schema( +# operation_summary="Get Narrator Teachers and Students", +# operation_description="Analyzes transmission chains to find the teachers (order + 1) and students (order - 1) of a narrator.", +# tags=['Dobodbi - Hadis (V2)'], +# ) +# def get(self, request, narrator_slug, *args, **kwargs): +# # 1. پیدا کردن راوی اصلی +# narrator = get_object_or_404(Transmitters, slug=narrator_slug) + +# # 2. پیدا کردن تمام اتصالاتی که این راوی در آن‌ها حضور دارد +# narrator_links = HadisTransmitter.objects.filter(transmitter=narrator) + +# teacher_ids = set() +# student_ids = set() + +# # 3. تحلیل زنجیره‌ها برای پیدا کردن اساتید و شاگردان +# for link in narrator_links: +# # استاد = کسی که در همان حدیث و همان زنجیره، Order او یکی بیشتر از راوی ماست +# teacher_link = HadisTransmitter.objects.filter( +# hadis_id=link.hadis_id, +# chain_index=link.chain_index, +# order=link.order + 1 +# ).first() +# if teacher_link: +# teacher_ids.add(teacher_link.transmitter_id) + +# # شاگرد = کسی که در همان حدیث و همان زنجیره، Order او یکی کمتر از راوی ماست +# student_link = HadisTransmitter.objects.filter( +# hadis_id=link.hadis_id, +# chain_index=link.chain_index, +# order=link.order - 1 +# ).first() +# if student_link: +# student_ids.add(student_link.transmitter_id) + +# # 4. واکشی اطلاعات اساتید و شاگردان از دیتابیس به صورت یکجا +# teachers_qs = Transmitters.objects.filter(id__in=teacher_ids) +# students_qs = Transmitters.objects.filter(id__in=student_ids) + +# # 5. سریالایز کردن داده‌ها با در نظر گرفتن زبان ریکوئست +# teachers_data = NarratorRelationSerializer(teachers_qs, many=True, context={'request': request}).data +# students_data = NarratorRelationSerializer(students_qs, many=True, context={'request': request}).data + +# # 6. بازگرداندن پاسخ +# return Response({ +# "narrator_slug": narrator.slug, +# "teachers_count": len(teachers_data), +# "students_count": len(students_data), +# "teachers": teachers_data, +# "students": students_data +# }) + +from django.db.models import Exists, OuterRef # 👈 حتما این دو را ایمپورت کنید + +class NarratorRelationsView(APIView): + """ + API view to get the Teachers and Students of a specific narrator + based on their position in Hadith transmission chains. + Optimized to run fully inside the database. + """ + permission_classes = [AllowAny] + + @swagger_auto_schema( + operation_summary="Get Narrator Teachers and Students", + operation_description="Analyzes transmission chains to find the teachers (order + 1) and students (order - 1) of a narrator.", + tags=['Dobodbi - Hadis (V2)'], + ) + def get(self, request, narrator_slug, *args, **kwargs): + # 1. پیدا کردن راوی اصلی + narrator = get_object_or_404(Transmitters, slug=narrator_slug) + + # 2. ساب‌کوئری برای بررسی اینکه آیا در این سند خاص، این شخص نقش «استاد» را برای راوی اصلی دارد؟ + is_teacher_subquery = HadisTransmitter.objects.filter( + transmitter=narrator, + hadis_id=OuterRef('hadis_id'), + chain_index=OuterRef('chain_index'), + order=OuterRef('order') - 1 # استاد یعنی Order او یکی کمتر از راوی تارگت باشد (بالاتر در زنجیره) + ) + + # 3. ساب‌کوئری برای بررسی نقش «شاگرد» + is_student_subquery = HadisTransmitter.objects.filter( + transmitter=narrator, + hadis_id=OuterRef('hadis_id'), + chain_index=OuterRef('chain_index'), + order=OuterRef('order') + 1 # شاگرد یعنی Order او یکی بیشتر باشد (پایین‌تر در زنجیره) + ) + + # 4. واکشی سریع آیدی‌ها فقط با اجرای ۲ کوئری داخل خود SQL بدون حلقه پایتون + teacher_ids = HadisTransmitter.objects.annotate( + is_teacher=Exists(is_teacher_subquery) + ).filter(is_teacher=True).values_list('transmitter_id', flat=True).distinct() + + student_ids = HadisTransmitter.objects.annotate( + is_student=Exists(is_student_subquery) + ).filter(is_student=True).values_list('transmitter_id', flat=True).distinct() + + # 5. واکشی دیتای نهاییِ راویان با select_related برای جلوگیری از N+1 روی Reliability + teachers_qs = Transmitters.objects.filter(id__in=teacher_ids).select_related('reliability') + students_qs = Transmitters.objects.filter(id__in=student_ids).select_related('reliability') + + # 6. ساخت خروجی + teachers_data = NarratorRelationSerializer(teachers_qs, many=True, context={'request': request}).data + students_data = NarratorRelationSerializer(students_qs, many=True, context={'request': request}).data + + return Response({ + "narrator_slug": narrator.slug, + "teachers_count": len(teachers_data), + "students_count": len(students_data), + "teachers": teachers_data, + "students": students_data + }) \ No newline at end of file