Browse Source

narrator api enhanced. relations added and populated

master
Mohsen Taba 3 weeks ago
parent
commit
1fa8bc04c2
  1. 105
      apps/hadis/management/commands/pop_data.py
  2. 23
      apps/hadis/migrations/0027_transmitterrelative_relation_value_and_more.py
  3. 69
      apps/hadis/models/transmitter.py
  4. 27
      apps/hadis/serializers/hadis.py
  5. 5
      apps/hadis/urls.py
  6. 95
      apps/hadis/views/transmitter.py

105
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!"))

23
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'),
),
]

69
apps/hadis/models/transmitter.py

@ -798,10 +798,69 @@ class OriginalTextReferenceImage(models.Model):
priority = models.IntegerField(default=0) 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 TransmitterRelative(models.Model):
class RelationTypeChoices(models.TextChoices): 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( related_to = models.ForeignKey(
Transmitters, Transmitters,
@ -814,6 +873,7 @@ class TransmitterRelative(models.Model):
choices=RelationTypeChoices.choices, choices=RelationTypeChoices.choices,
verbose_name=_('Relation Type') verbose_name=_('Relation Type')
) )
relation_value = models.JSONField(default=list, verbose_name=_('Relation Value'))
name = models.CharField(max_length=255, verbose_name=_('Name')) name = models.CharField(max_length=255, verbose_name=_('Name'))
narrator = models.ForeignKey( narrator = models.ForeignKey(
Transmitters, Transmitters,
@ -832,5 +892,10 @@ class TransmitterRelative(models.Model):
verbose_name_plural = _('Transmitter Relatives') verbose_name_plural = _('Transmitter Relatives')
ordering = ['relation_type', 'name'] 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): def __str__(self):
return f"{self.name} ({self.get_relation_type_display()} of {self.related_to})" return f"{self.name} ({self.get_relation_type_display()} of {self.related_to})"

27
apps/hadis/serializers/hadis.py

@ -380,12 +380,13 @@ class TransmitterOpinionSerializer(serializers.ModelSerializer):
""" Serializer for TransmitterOpinions """ """ Serializer for TransmitterOpinions """
scholar_name = LocalizedField() scholar_name = LocalizedField()
opinion_text = LocalizedField() opinion_text = LocalizedField()
arabic_opinion_text = ArabicLocalizedField(source='opinion_text')
status = serializers.SerializerMethodField() status = serializers.SerializerMethodField()
scholar = serializers.SerializerMethodField() scholar = serializers.SerializerMethodField()
class Meta: class Meta:
model = TransmitterOpinion 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): def get_scholar(self, obj):
request = self.context.get('request') request = self.context.get('request')
@ -418,7 +419,7 @@ class TransmitterOriginalTextSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = TransmitterOriginalText model = TransmitterOriginalText
fields = ['id', 'title', 'text', 'translation', 'share_link']
fields = ['id', 'title', 'text', 'translation', 'share_link', 'slug']
class TransmitterDetailSerializer(serializers.ModelSerializer): class TransmitterDetailSerializer(serializers.ModelSerializer):
@ -473,6 +474,7 @@ class TransmitterDetailSerializer(serializers.ModelSerializer):
return None return None
def get_relatives(self, obj): def get_relatives(self, obj):
request = self.context.get('request')
relatives_qs = obj.relatives.all() relatives_qs = obj.relatives.all()
grouped = {} 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 # Use narrator's localized name if exists and name is empty, otherwise use the direct name
name = rel.name name = rel.name
if rel.narrator and not name: if rel.narrator and not name:
request = self.context.get('request')
name = get_localized_text(rel.narrator.full_name, request) name = get_localized_text(rel.narrator.full_name, request)
item = { item = {
@ -492,15 +493,21 @@ class TransmitterDetailSerializer(serializers.ModelSerializer):
} }
if rel_type not in grouped: 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 = [] 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 return result

5
apps/hadis/urls.py

@ -7,7 +7,7 @@ from .views.hadis import (
) )
from .views.transmitter import ( from .views.transmitter import (
TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView, 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 import BookDetailView, BookReferencesView, BookReferenceSyncView, BookAttributeView
from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncView, BookReferenceV2ListView from .views.reference_v2 import BookReferenceV2DetailView, BookReferenceV2SyncView, BookReferenceV2ListView
@ -95,7 +95,8 @@ urlpatterns = [
path('arguments/filters/', HadisFiltersView.as_view(), name='hadis-filters'), path('arguments/filters/', HadisFiltersView.as_view(), name='hadis-filters'),
# Narrator paths # Narrator paths
path('narrators/<str:narrator_slug>/relations/', NarratorRelationsView.as_view(), name='narrator-relations'),
path('narrators/<str:narrator_slug>/teachers/', NarratorTeachersView.as_view(), name='narrator-teachers'),
path('narrators/<str:narrator_slug>/students/', NarratorStudentsView.as_view(), name='narrator-students'),
path('narrators/<str:narrator_slug>/arguments', NarratorArgumentsListView.as_view(), name='narrator-arguments'), path('narrators/<str:narrator_slug>/arguments', NarratorArgumentsListView.as_view(), name='narrator-arguments'),
path('narrators/<str:narrator_slug>/opinions', TransmitterOpinionView.as_view(), name='narrator-opinions'), path('narrators/<str:narrator_slug>/opinions', TransmitterOpinionView.as_view(), name='narrator-opinions'),
path('narrators/<str:narrator_slug>/original_texts', TransmitterOriginalTextView.as_view(), name='narrator-original-texts'), path('narrators/<str:narrator_slug>/original_texts', TransmitterOriginalTextView.as_view(), name='narrator-original-texts'),

95
apps/hadis/views/transmitter.py

@ -354,63 +354,98 @@ class NarratorArgumentsListView(ListAPIView):
from django.db.models import Exists, OuterRef # 👈 حتما این دو را ایمپورت کنید 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. based on their position in Hadith transmission chains.
Optimized to run fully inside the database. Optimized to run fully inside the database.
""" """
serializer_class = NarratorRelationSerializer
pagination_class = StandardResultsSetPagination
permission_classes = [AllowAny] permission_classes = [AllowAny]
@swagger_auto_schema( @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)'], 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) narrator = get_object_or_404(Transmitters, slug=narrator_slug)
# 2. ساب‌کوئری برای بررسی اینکه آیا در این سند خاص، این شخص نقش «استاد» را برای راوی اصلی دارد؟
is_teacher_subquery = HadisTransmitter.objects.filter( is_teacher_subquery = HadisTransmitter.objects.filter(
transmitter=narrator, transmitter=narrator,
hadis_id=OuterRef('hadis_id'), hadis_id=OuterRef('hadis_id'),
chain_index=OuterRef('chain_index'), 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( is_student_subquery = HadisTransmitter.objects.filter(
transmitter=narrator, transmitter=narrator,
hadis_id=OuterRef('hadis_id'), hadis_id=OuterRef('hadis_id'),
chain_index=OuterRef('chain_index'), 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( student_ids = HadisTransmitter.objects.annotate(
is_student=Exists(is_student_subquery) is_student=Exists(is_student_subquery)
).filter(is_student=True).values_list('transmitter_id', flat=True).distinct() ).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 from ..serializers.hadis import TransmitterOriginalTextDetailSerializer

Loading…
Cancel
Save