Browse Source

feat(hadis): support M2M kunyas/madhhabs in narrator list & filters and normalize BH/AH date strings

master
Mohsen Taba 3 days ago
parent
commit
6b0aba32d3
  1. 8
      apps/hadis/models/transmitter.py
  2. 42
      apps/hadis/serializers/hadis.py
  3. 30
      apps/hadis/utils/date_engine.py
  4. 9
      apps/hadis/views/hadis.py
  5. 38
      apps/hadis/views/transmitter.py

8
apps/hadis/models/transmitter.py

@ -1407,6 +1407,10 @@ class YearValue(models.Model):
if not self.translations or not isinstance(self.translations, list): if not self.translations or not isinstance(self.translations, list):
return None return None
target = (lang or 'ru').split('-')[0].lower() target = (lang or 'ru').split('-')[0].lower()
if self.parse_type == 'unparsed':
from ..utils.date_engine import format_canonical_date
engine_lang = target if target in ('ru', 'en', 'ar') else 'en'
return format_canonical_date(self.raw_value, category=self.category, lang=engine_lang)
for item in self.translations: for item in self.translations:
if isinstance(item, dict) and item.get('language_code') == target: if isinstance(item, dict) and item.get('language_code') == target:
txt = item.get('text') or item.get('title') txt = item.get('text') or item.get('title')
@ -1426,6 +1430,10 @@ class YearValue(models.Model):
raw_clean = str(raw_value).strip() raw_clean = str(raw_value).strip()
parsed = parse_canonical_date(raw_clean, category=category) parsed = parse_canonical_date(raw_clean, category=category)
if not parsed.is_valid and parsed.suggestion:
parsed_sug = parse_canonical_date(parsed.suggestion, category=category)
if parsed_sug.is_valid:
parsed = parsed_sug
canonical_str = parsed.raw_value if parsed.is_valid else raw_clean canonical_str = parsed.raw_value if parsed.is_valid else raw_clean
existing = cls.objects.filter(category=category, raw_value=canonical_str).first() existing = cls.objects.filter(category=category, raw_value=canonical_str).first()

42
apps/hadis/serializers/hadis.py

@ -478,8 +478,9 @@ class TransmitterSerializer(serializers.ModelSerializer):
"""Serializer for Transmitters""" """Serializer for Transmitters"""
full_name = LocalizedField() full_name = LocalizedField()
arabic_full_name = ArabicLocalizedField(source='full_name') arabic_full_name = ArabicLocalizedField(source='full_name')
kunya = LocalizedField()
arabic_kunya = ArabicLocalizedField(source='kunya')
kunya = serializers.SerializerMethodField()
arabic_kunya = serializers.SerializerMethodField()
kunyas = serializers.SerializerMethodField()
known_as = LocalizedField() known_as = LocalizedField()
arabic_known_as = ArabicLocalizedField(source='known_as') arabic_known_as = ArabicLocalizedField(source='known_as')
nickname = LocalizedField() nickname = LocalizedField()
@ -491,6 +492,7 @@ class TransmitterSerializer(serializers.ModelSerializer):
death_year_raw = serializers.CharField(source='death_year_hijri', read_only=True) death_year_raw = serializers.CharField(source='death_year_hijri', read_only=True)
age_at_death_raw = serializers.CharField(source='age_at_death', read_only=True) age_at_death_raw = serializers.CharField(source='age_at_death', read_only=True)
reliability = serializers.SerializerMethodField() reliability = serializers.SerializerMethodField()
madhhabs = serializers.SerializerMethodField()
share_link = serializers.CharField(read_only=True) share_link = serializers.CharField(read_only=True)
class Meta: class Meta:
@ -499,10 +501,22 @@ class TransmitterSerializer(serializers.ModelSerializer):
'id', 'full_name', 'arabic_full_name', 'slug', 'id', 'full_name', 'arabic_full_name', 'slug',
'birth_year_hijri', 'death_year_hijri', 'age_at_death', 'birth_year_hijri', 'death_year_hijri', 'age_at_death',
'birth_year_raw', 'death_year_raw', 'age_at_death_raw', 'birth_year_raw', 'death_year_raw', 'age_at_death_raw',
'kunya', 'arabic_kunya', 'known_as', 'arabic_known_as', 'nickname', 'arabic_nickname',
'reliability', 'madhhab', 'generation', 'share_link', 'gender'
'kunya', 'arabic_kunya', 'kunyas', 'known_as', 'arabic_known_as', 'nickname', 'arabic_nickname',
'reliability', 'madhhab', 'madhhabs', 'generation', 'share_link', 'gender'
] ]
def get_kunya(self, obj):
return _resolve_transmitter_m2m_text(obj.kunyas.all(), obj.kunya, self.context.get('request'))
def get_arabic_kunya(self, obj):
return _resolve_transmitter_m2m_text(obj.kunyas.all(), obj.kunya, self.context.get('request'), arabic=True)
def get_kunyas(self, obj):
return _serialize_transmitter_m2m_items(obj.kunyas.all(), self.context.get('request'))
def get_madhhabs(self, obj):
return _serialize_transmitter_m2m_items(obj.madhhabs.all(), self.context.get('request'), include_description=True)
def get_birth_year_hijri(self, obj): def get_birth_year_hijri(self, obj):
return format_transmitter_date(obj, 'birth', self.context.get('request')) return format_transmitter_date(obj, 'birth', self.context.get('request'))
@ -530,8 +544,8 @@ class TransmitterShortSerializer(serializers.ModelSerializer):
"""Serializer for Transmitters""" """Serializer for Transmitters"""
full_name = LocalizedField() full_name = LocalizedField()
arabic_full_name = ArabicLocalizedField(source='full_name') arabic_full_name = ArabicLocalizedField(source='full_name')
kunya = LocalizedField()
arabic_kunya = ArabicLocalizedField(source='kunya')
kunya = serializers.SerializerMethodField()
arabic_kunya = serializers.SerializerMethodField()
known_as = LocalizedField() known_as = LocalizedField()
arabic_known_as = ArabicLocalizedField(source='known_as') arabic_known_as = ArabicLocalizedField(source='known_as')
nickname = LocalizedField() nickname = LocalizedField()
@ -550,6 +564,12 @@ class TransmitterShortSerializer(serializers.ModelSerializer):
'kunya', 'arabic_kunya', 'known_as', 'arabic_known_as', 'nickname', 'arabic_nickname', 'reliability', 'gender' 'kunya', 'arabic_kunya', 'known_as', 'arabic_known_as', 'nickname', 'arabic_nickname', 'reliability', 'gender'
] ]
def get_kunya(self, obj):
return _resolve_transmitter_m2m_text(obj.kunyas.all(), obj.kunya, self.context.get('request'))
def get_arabic_kunya(self, obj):
return _resolve_transmitter_m2m_text(obj.kunyas.all(), obj.kunya, self.context.get('request'), arabic=True)
def get_birth_year_hijri(self, obj): def get_birth_year_hijri(self, obj):
return format_transmitter_date(obj, 'birth', self.context.get('request')) return format_transmitter_date(obj, 'birth', self.context.get('request'))
@ -1797,8 +1817,8 @@ class NarratorRelationSerializer(serializers.ModelSerializer):
""" """
full_name = LocalizedField() full_name = LocalizedField()
arabic_full_name = ArabicLocalizedField(source='full_name') arabic_full_name = ArabicLocalizedField(source='full_name')
kunya = LocalizedField()
arabic_kunya = ArabicLocalizedField(source='kunya')
kunya = serializers.SerializerMethodField()
arabic_kunya = serializers.SerializerMethodField()
known_as = LocalizedField() known_as = LocalizedField()
arabic_known_as = ArabicLocalizedField(source='known_as') arabic_known_as = ArabicLocalizedField(source='known_as')
nickname = LocalizedField() nickname = LocalizedField()
@ -1818,6 +1838,12 @@ class NarratorRelationSerializer(serializers.ModelSerializer):
'reliability', 'gender' 'reliability', 'gender'
] ]
def get_kunya(self, obj):
return _resolve_transmitter_m2m_text(obj.kunyas.all(), obj.kunya, self.context.get('request'))
def get_arabic_kunya(self, obj):
return _resolve_transmitter_m2m_text(obj.kunyas.all(), obj.kunya, self.context.get('request'), arabic=True)
def get_birth_year_hijri(self, obj): def get_birth_year_hijri(self, obj):
return format_transmitter_date(obj, 'birth', self.context.get('request')) return format_transmitter_date(obj, 'birth', self.context.get('request'))

30
apps/hadis/utils/date_engine.py

@ -148,17 +148,33 @@ def suggest_canonical_fix(raw: str) -> Optional[str]:
cleaned_key = re.sub(r"[^a-zA-Z0-9_]", "_", s[1:].strip()).strip("_").lower() cleaned_key = re.sub(r"[^a-zA-Z0-9_]", "_", s[1:].strip()).strip("_").lower()
return f"#{cleaned_key}" if cleaned_key else None return f"#{cleaned_key}" if cleaned_key else None
# Replace approximate symbols
# Replace approximate symbols and words
s = re.sub(r"^(?:около|circa|approx\.?|حوالي)\s*", "~", s, flags=re.IGNORECASE)
if s.startswith(("±", "≈")): if s.startswith(("±", "≈")):
s = "~" + s[1:].strip() s = "~" + s[1:].strip()
elif s.startswith("~"): elif s.startswith("~"):
s = "~" + s[1:].strip() s = "~" + s[1:].strip()
# Replace greater/less spacing or words # Replace greater/less spacing or words
s = re.sub(r"^(?:больше|more\s+than|after|بعد)\s*", ">", s, flags=re.IGNORECASE)
s = re.sub(r"^(?:меньше|до|less\s+than|before|قبل)\s*", "<", s, flags=re.IGNORECASE)
s = re.sub(r"^(?:больше|более|после|more\s+than|after|بعد|أكثر\s+من)\s*", ">", s, flags=re.IGNORECASE)
s = re.sub(r"^(?:меньше|менее|до|less\s+than|before|قبل|أقل\s+من)\s*", "<", s, flags=re.IGNORECASE)
s = re.sub(r"([><~])\s+(?=-?\d)", r"\1", s) s = re.sub(r"([><~])\s+(?=-?\d)", r"\1", s)
# Replace Before Hijra (BH / год до хиджры / г. до хиджры / ق.هـ) with negative number
s = re.sub(
r"(?<!\d)(\d+)\s*(?:год(?:а|ов)?\s+до\s+хиджры|гг?\.?\s*до\s*хиджры|bh|ق\.?\s*هـ?)\b",
r"-\1",
s,
flags=re.IGNORECASE,
)
# Strip trailing AH / г. / هـ / лет / years / سنة units
s = re.sub(
r"(?<=\d)\s*(?:гг?\.?(?:\s*х\.?)?|год(?:а|ов)?|лет|ah|years?|هـ|سنة)\b\.?",
"",
s,
flags=re.IGNORECASE,
)
# Replace alternative separators ('или', 'or', 'أو', '∨', ',') with ';' # Replace alternative separators ('или', 'or', 'أو', '∨', ',') with ';'
s = re.sub(r"\s*(?:или|or|أو|∨|,|،|;)\s*", ";", s, flags=re.IGNORECASE) s = re.sub(r"\s*(?:или|or|أو|∨|,|،|;)\s*", ";", s, flags=re.IGNORECASE)
@ -655,6 +671,10 @@ def format_canonical_date(
cat = date_type or category or "birth" cat = date_type or category or "birth"
raw_str = str(raw_value).strip() raw_str = str(raw_value).strip()
parsed = parse_canonical_date(raw_str, category=cat) parsed = parse_canonical_date(raw_str, category=cat)
if not parsed.is_valid and parsed.suggestion:
parsed_sug = parse_canonical_date(parsed.suggestion, category=cat)
if parsed_sug.is_valid:
parsed = parsed_sug
if not parsed.is_valid: if not parsed.is_valid:
return raw_str return raw_str
return render_date_from_parsed(parsed, date_type=cat, lang=lang) return render_date_from_parsed(parsed, date_type=cat, lang=lang)
@ -672,6 +692,10 @@ def format_canonical_for_all_langs(
return {} return {}
raw_str = str(raw_value).strip() raw_str = str(raw_value).strip()
parsed = parse_canonical_date(raw_str, category=date_type) parsed = parse_canonical_date(raw_str, category=date_type)
if not parsed.is_valid and parsed.suggestion:
parsed_sug = parse_canonical_date(parsed.suggestion, category=date_type)
if parsed_sug.is_valid:
parsed = parsed_sug
if not parsed.is_valid: if not parsed.is_valid:
return {lang: raw_str for lang in SUPPORTED_LANGS} return {lang: raw_str for lang in SUPPORTED_LANGS}
return { return {

9
apps/hadis/views/hadis.py

@ -799,7 +799,14 @@ class HadisTransmittersView(RetrieveAPIView):
'transmitter', 'transmitter',
'narrator_layer', 'narrator_layer',
'status' 'status'
).prefetch_related('uncertain_transmitters').order_by('order')
).select_related('transmitter__reliability').prefetch_related(
'transmitter__narrator_dates__year_value',
'transmitter__kunyas',
'uncertain_transmitters',
'uncertain_transmitters__reliability',
'uncertain_transmitters__narrator_dates__year_value',
'uncertain_transmitters__kunyas',
).order_by('order')
# 3. Apply the filter to the Child Query (if param exists) # 3. Apply the filter to the Child Query (if param exists)
if layer_slug: if layer_slug:

38
apps/hadis/views/transmitter.py

@ -32,7 +32,11 @@ class TransmitterView(ListAPIView):
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
def get_queryset(self): def get_queryset(self):
queryset = Transmitters.objects.select_related('reliability').prefetch_related('narrator_dates__year_value')
queryset = Transmitters.objects.select_related('reliability').prefetch_related(
'narrator_dates__year_value',
'kunyas',
'madhhabs',
)
# Get query parameters # Get query parameters
search_query = self.request.query_params.get('search', None) search_query = self.request.query_params.get('search', None)
@ -54,7 +58,9 @@ class TransmitterView(ListAPIView):
queryset = queryset.filter(reliability__slug=status_filter) queryset = queryset.filter(reliability__slug=status_filter)
if madhhab_filter: if madhhab_filter:
queryset = queryset.filter(madhhab=madhhab_filter)
queryset = queryset.filter(
Q(madhhabs__slug__iexact=madhhab_filter) | Q(madhhab__iexact=madhhab_filter)
).distinct()
if generation_filter: if generation_filter:
queryset = queryset.filter(generation=generation_filter) queryset = queryset.filter(generation=generation_filter)
@ -299,7 +305,23 @@ def get_transmitter_filters(request):
# Get madhhab choices # Get madhhab choices
madhabs = [] madhabs = []
for choice in Transmitters.MadhhabChoices: for choice in Transmitters.MadhhabChoices:
madhabs.append(choice.value)
from ..models.transmitter import Madhhab
madhhab_qs = Madhhab.objects.all().order_by('id')
if madhhab_qs.exists():
madhabs = [
{
'id': m.id,
'text': get_localized_text(m.title, request),
'title': get_localized_text(m.title, request),
'slug': m.slug,
'description': get_localized_text(m.description, request),
}
for m in madhhab_qs
if m.title
]
break
else:
madhabs.append(choice.value)
# Get reliabilities from TransmitterReliability model # Get reliabilities from TransmitterReliability model
from ..models.transmitter import TransmitterReliability from ..models.transmitter import TransmitterReliability
@ -470,7 +492,10 @@ class NarratorTeachersView(ListAPIView):
is_teacher=Exists(is_teacher_subquery) is_teacher=Exists(is_teacher_subquery)
).filter(is_teacher=True).values_list('transmitter_id', flat=True).distinct() ).filter(is_teacher=True).values_list('transmitter_id', flat=True).distinct()
queryset = Transmitters.objects.filter(id__in=teacher_ids).select_related('reliability')
queryset = Transmitters.objects.filter(id__in=teacher_ids).select_related('reliability').prefetch_related(
'narrator_dates__year_value',
'kunyas',
)
search_query = self.request.query_params.get('search', None) search_query = self.request.query_params.get('search', None)
if search_query: if search_query:
@ -518,7 +543,10 @@ class NarratorStudentsView(ListAPIView):
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()
queryset = Transmitters.objects.filter(id__in=student_ids).select_related('reliability')
queryset = Transmitters.objects.filter(id__in=student_ids).select_related('reliability').prefetch_related(
'narrator_dates__year_value',
'kunyas',
)
search_query = self.request.query_params.get('search', None) search_query = self.request.query_params.get('search', None)
if search_query: if search_query:

Loading…
Cancel
Save