diff --git a/apps/hadis/management/commands/pop_data.py b/apps/hadis/management/commands/pop_data.py new file mode 100644 index 0000000..72cf500 --- /dev/null +++ b/apps/hadis/management/commands/pop_data.py @@ -0,0 +1,105 @@ +# backend/apps/hadis/management/commands/populate_remaining_data.py + +import random +from django.core.management.base import BaseCommand +from django.db import transaction +from django.db.models import Q +from apps.hadis.models import Transmitters, TransmitterRelative, TransmitterOpinion +from apps.hadis.models.transmitter import RELATION_TYPE_MAP + +class Command(BaseCommand): + help = 'Populates missing companion_type, default Arabic opinions, and generates 10 random relatives.' + + def handle(self, *args, **options): + self.stdout.write(self.style.WARNING("Starting population script...")) + + # ========================================== + # 1. Populate Companion Type + # ========================================== + comp_updated = Transmitters.objects.filter( + Q(companion_type__isnull=True) | Q(companion_type="") + ).update(companion_type="al-A'la ibn Wasil") + + self.stdout.write(self.style.SUCCESS(f"✅ Updated {comp_updated} narrators with companion_type.")) + + # ========================================== + # 2. Populate Transmitter Opinions (Arabic Text) + # ========================================== + opinions = TransmitterOpinion.objects.all() + opinions_to_update = [] + default_ar_text = {"language_code": "ar", "text": "ثقة، وهو أثبت من عبد الرحمن بن مهدي."} + + for op in opinions: + if not op.opinion_text: + op.opinion_text = [default_ar_text] + opinions_to_update.append(op) + elif isinstance(op.opinion_text, list): + # بررسی اینکه آیا رکورد عربی وجود دارد یا خیر + has_ar = any(isinstance(item, dict) and item.get('language_code') == 'ar' for item in op.opinion_text) + if not has_ar: + op.opinion_text.append(default_ar_text) + opinions_to_update.append(op) + + if opinions_to_update: + TransmitterOpinion.objects.bulk_update(opinions_to_update, ['opinion_text'], batch_size=500) + self.stdout.write(self.style.SUCCESS(f"✅ Updated {len(opinions_to_update)} opinions with default Arabic text.")) + else: + self.stdout.write(self.style.SUCCESS("✅ All opinions already have Arabic text.")) + + # ========================================== + # 3. Populate Relatives (10 per narrator: 5, 4, 1) + # ========================================== + # پیدا کردن راویانی که هیچ نسبتی برایشان ثبت نشده است + narrators_without_relatives = Transmitters.objects.filter(relatives__isnull=True).distinct() + count = narrators_without_relatives.count() + + if count == 0: + self.stdout.write(self.style.SUCCESS("✅ All narrators already have relatives.")) + return + + all_transmitter_ids = list(Transmitters.objects.values_list('id', flat=True)) + if not all_transmitter_ids: + self.stdout.write(self.style.ERROR("❌ No transmitters found in DB to link.")) + return + + # 10 اسم رندوم پیش‌فرض + NAMES = ["عمر", "علي", "فاطمة", "خديجة", "زينب", "حسن", "حسين", "محمد", "أحمد", "عائشة"] + relation_choices = [choice[0] for choice in TransmitterRelative.RelationTypeChoices.choices] + + new_relatives = [] + + for narrator in narrators_without_relatives: + group_sizes = [5, 4, 1] + # انتخاب ۳ نوع رابطه رندوم و متفاوت برای این راوی + selected_types = random.sample(relation_choices, 3) + # بر هم زدن لیست اسم‌ها + shuffled_names = random.sample(NAMES, 10) + # انتخاب ۳ ایندکس رندوم (از بین 0 تا 9) که قرار است به راویان دیگر متصل شوند + linked_indices = set(random.sample(range(10), 3)) + + current_idx = 0 + for i, size in enumerate(group_sizes): + rel_type = selected_types[i] + rel_val = RELATION_TYPE_MAP.get(rel_type, []) + + for _ in range(size): + linked_narrator_id = None + # اگر ایندکس فعلی جزو ۳ ایندکس خوش‌شانس بود، یک راوی رندوم به آن متصل کن + if current_idx in linked_indices: + linked_narrator_id = random.choice(all_transmitter_ids) + + new_relatives.append( + TransmitterRelative( + related_to=narrator, + relation_type=rel_type, + relation_value=rel_val, # اعمال دستی به دلیل استفاده از bulk_create + name=shuffled_names[current_idx], + narrator_id=linked_narrator_id + ) + ) + current_idx += 1 + + with transaction.atomic(): + TransmitterRelative.objects.bulk_create(new_relatives, batch_size=1000) + + self.stdout.write(self.style.SUCCESS(f"🎉 Successfully created {len(new_relatives)} relatives for {count} narrators!")) \ No newline at end of file diff --git a/apps/hadis/migrations/0027_transmitterrelative_relation_value_and_more.py b/apps/hadis/migrations/0027_transmitterrelative_relation_value_and_more.py new file mode 100644 index 0000000..55437d2 --- /dev/null +++ b/apps/hadis/migrations/0027_transmitterrelative_relation_value_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 5.2.12 on 2026-06-28 14:31 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hadis', '0026_transmitterrelative'), + ] + + operations = [ + migrations.AddField( + model_name='transmitterrelative', + name='relation_value', + field=models.JSONField(default=list, verbose_name='Relation Value'), + ), + migrations.AlterField( + model_name='transmitterrelative', + name='relation_type', + field=models.CharField(choices=[('stepfather', 'Stepfather'), ('stepmother', 'Stepmother'), ('uncles_and_aunts', 'Uncles and aunts'), ('siblings', 'Siblings'), ('parents', 'Parents'), ('nephews_and_nieces', 'Nephews and nieces'), ('children', 'Children'), ('stepchildren', 'Stepchildren'), ('spouses', 'Spouses'), ('cousins', 'Cousins'), ('granduncles_and_grandaunts', 'Granduncles and grandaunts'), ('grandnephews_and_grandnieces', 'Grandnephews and grandnieces'), ('grandchildren_and_great_grandchildren', 'Grandchildren and great-grandchildren'), ('grandparents', 'Grandparents'), ('foster_mother', 'Foster mother'), ('tribe', 'Tribe'), ('foster_children', 'Foster children'), ('in_laws', 'In-laws'), ('students', 'Students'), ('owners_and_masters', 'Owners and masters'), ('other_relatives', 'Other relatives'), ('friends_and_companions', 'Friends and companions'), ('allies', 'Allies'), ('neighbors', 'Neighbors'), ('wards_and_proteges', 'Wards and proteges'), ('cousins_of_parents', 'Cousins of parents'), ('cousins_of_grandparents', 'Cousins of grandparents'), ('cousins_children', 'Cousins children'), ('cousins_grandchildren_and_great_grandchildren', 'Cousins grandchildren and great-grandchildren')], max_length=50, verbose_name='Relation Type'), + ), + ] diff --git a/apps/hadis/models/transmitter.py b/apps/hadis/models/transmitter.py index 669dbf4..917c7dd 100644 --- a/apps/hadis/models/transmitter.py +++ b/apps/hadis/models/transmitter.py @@ -798,10 +798,69 @@ class OriginalTextReferenceImage(models.Model): priority = models.IntegerField(default=0) +RELATION_TYPE_MAP = { + 'stepfather': [{'language_code': 'ru', 'text': 'Отчим'}, {'language_code': 'ar', 'text': 'زوج الأم'}, {'language_code': 'en', 'text': 'Stepfather'}], + 'stepmother': [{'language_code': 'ru', 'text': 'Мачеха'}, {'language_code': 'ar', 'text': 'زوجة الأب'}, {'language_code': 'en', 'text': 'Stepmother'}], + 'uncles_and_aunts': [{'language_code': 'ru', 'text': 'Дяди и тёти'}, {'language_code': 'ar', 'text': 'الأعمام والعمات والأخوال والخالات'}, {'language_code': 'en', 'text': 'Uncles and aunts'}], + 'siblings': [{'language_code': 'ru', 'text': 'Сиблинги'}, {'language_code': 'ar', 'text': 'الإخوة'}, {'language_code': 'en', 'text': 'Siblings'}], + 'parents': [{'language_code': 'ru', 'text': 'Родители'}, {'language_code': 'ar', 'text': 'الوالدان'}, {'language_code': 'en', 'text': 'Parents'}], + 'nephews_and_nieces': [{'language_code': 'ru', 'text': 'Племянники'}, {'language_code': 'ar', 'text': 'أبناء الإخوة'}, {'language_code': 'en', 'text': 'Nephews and nieces'}], + 'children': [{'language_code': 'ru', 'text': 'Дети'}, {'language_code': 'ar', 'text': 'الأولاد'}, {'language_code': 'en', 'text': 'Children'}], + 'stepchildren': [{'language_code': 'ru', 'text': 'Пасынки и падчерицы'}, {'language_code': 'ar', 'text': 'الربائب'}, {'language_code': 'en', 'text': 'Stepchildren'}], + 'spouses': [{'language_code': 'ru', 'text': 'Супруги'}, {'language_code': 'ar', 'text': 'الأزواج'}, {'language_code': 'en', 'text': 'Spouses'}], + 'cousins': [{'language_code': 'ru', 'text': 'Двоюродные братья и сестры'}, {'language_code': 'ar', 'text': 'أبناء العمومة والخؤولة'}, {'language_code': 'en', 'text': 'Cousins'}], + 'granduncles_and_grandaunts': [{'language_code': 'ru', 'text': 'Великие дяди и тети'}, {'language_code': 'ar', 'text': 'إخوة الأجداد'}, {'language_code': 'en', 'text': 'Granduncles and grandaunts'}], + 'grandnephews_and_grandnieces': [{'language_code': 'ru', 'text': 'Внучатые племянники'}, {'language_code': 'ar', 'text': 'أحفاد الإخوة'}, {'language_code': 'en', 'text': 'Grandnephews and grandnieces'}], + 'grandchildren_and_great_grandchildren': [{'language_code': 'ru', 'text': 'Внуки и правнуки'}, {'language_code': 'ar', 'text': 'الأحفاد وأبناء الأحفاد'}, {'language_code': 'en', 'text': 'Grandchildren and great-grandchildren'}], + 'grandparents': [{'language_code': 'ru', 'text': 'Прародители'}, {'language_code': 'ar', 'text': 'الأجداد'}, {'language_code': 'en', 'text': 'Grandparents'}], + 'foster_mother': [{'language_code': 'ru', 'text': 'Молочная мать'}, {'language_code': 'ar', 'text': 'المرضعة'}, {'language_code': 'en', 'text': 'Foster mother'}], + 'tribe': [{'language_code': 'ru', 'text': 'Племя'}, {'language_code': 'ar', 'text': 'القبيلة'}, {'language_code': 'en', 'text': 'Tribe'}], + 'foster_children': [{'language_code': 'ru', 'text': 'Молочные дети'}, {'language_code': 'ar', 'text': 'الرضاء'}, {'language_code': 'en', 'text': 'Foster children'}], + 'in_laws': [{'language_code': 'ru', 'text': 'Свойственники'}, {'language_code': 'ar', 'text': 'الأصهار والأختان'}, {'language_code': 'en', 'text': 'In-laws'}], + 'students': [{'language_code': 'ru', 'text': 'Ученики'}, {'language_code': 'ar', 'text': 'التلاميذ'}, {'language_code': 'en', 'text': 'Students'}], + 'owners_and_masters': [{'language_code': 'ru', 'text': 'Хозяева и владельцы'}, {'language_code': 'ar', 'text': 'الملاك'}, {'language_code': 'en', 'text': 'Owners and masters'}], + 'other_relatives': [{'language_code': 'ru', 'text': 'Другие родственники'}, {'language_code': 'ar', 'text': 'الأقارب'}, {'language_code': 'en', 'text': 'Other relatives'}], + 'friends_and_companions': [{'language_code': 'ru', 'text': 'Друзья и сподвижники'}, {'language_code': 'ar', 'text': 'الأصحاب'}, {'language_code': 'en', 'text': 'Friends and companions'}], + 'allies': [{'language_code': 'ru', 'text': 'Союзники'}, {'language_code': 'ar', 'text': 'الحلفاء'}, {'language_code': 'en', 'text': 'Allies'}], + 'neighbors': [{'language_code': 'ru', 'text': 'Соседи'}, {'language_code': 'ar', 'text': 'الجيران'}, {'language_code': 'en', 'text': 'Neighbors'}], + 'wards_and_proteges': [{'language_code': 'ru', 'text': 'Подопечные и воспитанники'}, {'language_code': 'ar', 'text': 'المحضونون'}, {'language_code': 'en', 'text': 'Wards and proteges'}], + 'cousins_of_parents': [{'language_code': 'ru', 'text': 'Двоюродные дяди и тети'}, {'language_code': 'ar', 'text': 'أبناء عمومة الآباء'}, {'language_code': 'en', 'text': 'Cousins of parents'}], + 'cousins_of_grandparents': [{'language_code': 'ru', 'text': 'Двоюродные дедушки и бабушки'}, {'language_code': 'ar', 'text': 'أبناء عمومة الأجداد'}, {'language_code': 'en', 'text': 'Cousins of grandparents'}], + 'cousins_children': [{'language_code': 'ru', 'text': 'Двоюродные племянники'}, {'language_code': 'ar', 'text': 'أبناء أبناء العمومة'}, {'language_code': 'en', 'text': 'Cousins children'}], + 'cousins_grandchildren_and_great_grandchildren': [{'language_code': 'ru', 'text': 'Двоюродные внуки и правнуки'}, {'language_code': 'ar', 'text': 'أحفاد أبناء العمومة'}, {'language_code': 'en', 'text': 'Cousins grandchildren and great-grandchildren'}], +} + class TransmitterRelative(models.Model): class RelationTypeChoices(models.TextChoices): - FATHER = 'father', _('Father') - SON = 'son', _('Son') + STEPFATHER = 'stepfather', _('Stepfather') + STEPMOTHER = 'stepmother', _('Stepmother') + UNCLES_AND_AUNTS = 'uncles_and_aunts', _('Uncles and aunts') + SIBLINGS = 'siblings', _('Siblings') + PARENTS = 'parents', _('Parents') + NEPHEWS_AND_NIECES = 'nephews_and_nieces', _('Nephews and nieces') + CHILDREN = 'children', _('Children') + STEPCHILDREN = 'stepchildren', _('Stepchildren') + SPOUSES = 'spouses', _('Spouses') + COUSINS = 'cousins', _('Cousins') + GRANDUNCLES_AND_GRANDAUNTS = 'granduncles_and_grandaunts', _('Granduncles and grandaunts') + GRANDNEPHEWS_AND_GRANDNIECES = 'grandnephews_and_grandnieces', _('Grandnephews and grandnieces') + GRANDCHILDREN_AND_GREAT_GRANDCHILDREN = 'grandchildren_and_great_grandchildren', _('Grandchildren and great-grandchildren') + GRANDPARENTS = 'grandparents', _('Grandparents') + FOSTER_MOTHER = 'foster_mother', _('Foster mother') + TRIBE = 'tribe', _('Tribe') + FOSTER_CHILDREN = 'foster_children', _('Foster children') + IN_LAWS = 'in_laws', _('In-laws') + STUDENTS = 'students', _('Students') + OWNERS_AND_MASTERS = 'owners_and_masters', _('Owners and masters') + OTHER_RELATIVES = 'other_relatives', _('Other relatives') + FRIENDS_AND_COMPANIONS = 'friends_and_companions', _('Friends and companions') + ALLIES = 'allies', _('Allies') + NEIGHBORS = 'neighbors', _('Neighbors') + WARDS_AND_PROTEGES = 'wards_and_proteges', _('Wards and proteges') + COUSINS_OF_PARENTS = 'cousins_of_parents', _('Cousins of parents') + COUSINS_OF_GRANDPARENTS = 'cousins_of_grandparents', _('Cousins of grandparents') + COUSINS_CHILDREN = 'cousins_children', _('Cousins children') + COUSINS_GRANDCHILDREN_AND_GREAT_GRANDCHILDREN = 'cousins_grandchildren_and_great_grandchildren', _('Cousins grandchildren and great-grandchildren') related_to = models.ForeignKey( Transmitters, @@ -814,6 +873,7 @@ class TransmitterRelative(models.Model): choices=RelationTypeChoices.choices, verbose_name=_('Relation Type') ) + relation_value = models.JSONField(default=list, verbose_name=_('Relation Value')) name = models.CharField(max_length=255, verbose_name=_('Name')) narrator = models.ForeignKey( Transmitters, @@ -832,5 +892,10 @@ class TransmitterRelative(models.Model): verbose_name_plural = _('Transmitter Relatives') ordering = ['relation_type', 'name'] + def save(self, *args, **kwargs): + if self.relation_type and not self.relation_value: + self.relation_value = RELATION_TYPE_MAP.get(self.relation_type, []) + super().save(*args, **kwargs) + def __str__(self): return f"{self.name} ({self.get_relation_type_display()} of {self.related_to})" \ No newline at end of file diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 04160e6..05f0502 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -380,12 +380,13 @@ class TransmitterOpinionSerializer(serializers.ModelSerializer): """ Serializer for TransmitterOpinions """ scholar_name = LocalizedField() opinion_text = LocalizedField() + arabic_opinion_text = ArabicLocalizedField(source='opinion_text') status = serializers.SerializerMethodField() scholar = serializers.SerializerMethodField() class Meta: model = TransmitterOpinion - fields = ['id','transmitter','scholar_name','scholar','opinion_text','status'] + fields = ['id','transmitter','scholar_name','scholar','opinion_text','arabic_opinion_text','status'] def get_scholar(self, obj): request = self.context.get('request') @@ -418,7 +419,7 @@ class TransmitterOriginalTextSerializer(serializers.ModelSerializer): class Meta: model = TransmitterOriginalText - fields = ['id', 'title', 'text', 'translation', 'share_link'] + fields = ['id', 'title', 'text', 'translation', 'share_link', 'slug'] class TransmitterDetailSerializer(serializers.ModelSerializer): @@ -473,6 +474,7 @@ class TransmitterDetailSerializer(serializers.ModelSerializer): return None def get_relatives(self, obj): + request = self.context.get('request') relatives_qs = obj.relatives.all() grouped = {} @@ -483,7 +485,6 @@ class TransmitterDetailSerializer(serializers.ModelSerializer): # Use narrator's localized name if exists and name is empty, otherwise use the direct name name = rel.name if rel.narrator and not name: - request = self.context.get('request') name = get_localized_text(rel.narrator.full_name, request) item = { @@ -492,15 +493,21 @@ class TransmitterDetailSerializer(serializers.ModelSerializer): } if rel_type not in grouped: - grouped[rel_type] = [] - grouped[rel_type].append(item) + grouped[rel_type] = { + "relation_value": get_localized_text(rel.relation_value, request) if rel.relation_value else None, + "arabic_relation_value": get_arabic_localized_text(rel.relation_value) if rel.relation_value else None, + "items": [] + } + grouped[rel_type]["items"].append(item) result = [] - for rel_type, items in grouped.items(): - if rel_type == 'father' and items: - result.append({"father": items[0]}) - else: - result.append({rel_type: items}) + for rel_type, data in grouped.items(): + result.append({ + "relation_type": rel_type, + "relation_value": data["relation_value"], + "arabic_relation_value": data["arabic_relation_value"], + "members": data["items"] + }) return result diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index 625fdc4..9889d0f 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -7,7 +7,7 @@ from .views.hadis import ( ) from .views.transmitter import ( TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView, - NarratorArgumentsListView , NarratorRelationsView,TransmitterOriginalTextDetailView, TransmitterOriginalTextDetailView + NarratorArgumentsListView , NarratorTeachersView, NarratorStudentsView, TransmitterOriginalTextDetailView ) from .views.reference import BookDetailView, BookReferencesView, BookReferenceSyncView, BookAttributeView from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncView, BookReferenceV2ListView @@ -95,7 +95,8 @@ urlpatterns = [ path('arguments/filters/', HadisFiltersView.as_view(), name='hadis-filters'), # Narrator paths - path('narrators//relations/', NarratorRelationsView.as_view(), name='narrator-relations'), + path('narrators//teachers/', NarratorTeachersView.as_view(), name='narrator-teachers'), + path('narrators//students/', NarratorStudentsView.as_view(), name='narrator-students'), 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 b3a2207..80b9cd4 100644 --- a/apps/hadis/views/transmitter.py +++ b/apps/hadis/views/transmitter.py @@ -354,63 +354,98 @@ class NarratorArgumentsListView(ListAPIView): from django.db.models import Exists, OuterRef # 👈 حتما این دو را ایمپورت کنید -class NarratorRelationsView(APIView): +class NarratorTeachersView(ListAPIView): """ - API view to get the Teachers and Students of a specific narrator + API view to get the Teachers of a specific narrator based on their position in Hadith transmission chains. Optimized to run fully inside the database. """ + serializer_class = NarratorRelationSerializer + pagination_class = StandardResultsSetPagination 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.", + operation_summary="Get Narrator Teachers", + operation_description="Analyzes transmission chains to find the teachers (order + 1) of a narrator, with optional search.", tags=['Dobodbi - Hadis (V2)'], ) - def get(self, request, narrator_slug, *args, **kwargs): - # 1. پیدا کردن راوی اصلی + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + + def get_queryset(self): + narrator_slug = self.kwargs.get('narrator_slug') 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 او یکی کمتر از راوی تارگت باشد (بالاتر در زنجیره) + order=OuterRef('order') - 1 # استاد یعنی Order او یکی کمتر از راوی تارگت باشد ) - # 3. ساب‌کوئری برای بررسی نقش «شاگرد» + teacher_ids = HadisTransmitter.objects.annotate( + is_teacher=Exists(is_teacher_subquery) + ).filter(is_teacher=True).values_list('transmitter_id', flat=True).distinct() + + queryset = Transmitters.objects.filter(id__in=teacher_ids).select_related('reliability') + + search_query = self.request.query_params.get('search', None) + if search_query: + queryset = queryset.filter( + Q(full_name__icontains=search_query) | + Q(kunya__icontains=search_query) | + Q(known_as__icontains=search_query) | + Q(nickname__icontains=search_query) + ) + + return queryset + + +class NarratorStudentsView(ListAPIView): + """ + API view to get the Students of a specific narrator + based on their position in Hadith transmission chains. + Optimized to run fully inside the database. + """ + serializer_class = NarratorRelationSerializer + pagination_class = StandardResultsSetPagination + permission_classes = [AllowAny] + + @swagger_auto_schema( + operation_summary="Get Narrator Students", + operation_description="Analyzes transmission chains to find the students (order - 1) of a narrator, with optional search.", + tags=['Dobodbi - Hadis (V2)'], + ) + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + + def get_queryset(self): + narrator_slug = self.kwargs.get('narrator_slug') + narrator = get_object_or_404(Transmitters, slug=narrator_slug) + is_student_subquery = HadisTransmitter.objects.filter( transmitter=narrator, hadis_id=OuterRef('hadis_id'), chain_index=OuterRef('chain_index'), - order=OuterRef('order') + 1 # شاگرد یعنی Order او یکی بیشتر باشد (پایین‌تر در زنجیره) + 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 - }) + queryset = Transmitters.objects.filter(id__in=student_ids).select_related('reliability') + + search_query = self.request.query_params.get('search', None) + if search_query: + queryset = queryset.filter( + Q(full_name__icontains=search_query) | + Q(kunya__icontains=search_query) | + Q(known_as__icontains=search_query) | + Q(nickname__icontains=search_query) + ) + + return queryset from ..serializers.hadis import TransmitterOriginalTextDetailSerializer