From 6b0aba32d3214c6aed342580ab490475804142f7 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Tue, 27 Oct 2026 11:54:08 +0330 Subject: [PATCH] feat(hadis): support M2M kunyas/madhhabs in narrator list & filters and normalize BH/AH date strings --- apps/hadis/models/transmitter.py | 8 ++++++ apps/hadis/serializers/hadis.py | 42 ++++++++++++++++++++++++++------ apps/hadis/utils/date_engine.py | 30 ++++++++++++++++++++--- apps/hadis/views/hadis.py | 9 ++++++- apps/hadis/views/transmitter.py | 38 +++++++++++++++++++++++++---- 5 files changed, 110 insertions(+), 17 deletions(-) diff --git a/apps/hadis/models/transmitter.py b/apps/hadis/models/transmitter.py index 881d513..0ab683e 100644 --- a/apps/hadis/models/transmitter.py +++ b/apps/hadis/models/transmitter.py @@ -1407,6 +1407,10 @@ class YearValue(models.Model): if not self.translations or not isinstance(self.translations, list): return None 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: if isinstance(item, dict) and item.get('language_code') == target: txt = item.get('text') or item.get('title') @@ -1426,6 +1430,10 @@ class YearValue(models.Model): raw_clean = str(raw_value).strip() 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 existing = cls.objects.filter(category=category, raw_value=canonical_str).first() diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 938f9b6..4f22a3d 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -478,8 +478,9 @@ class TransmitterSerializer(serializers.ModelSerializer): """Serializer for Transmitters""" full_name = LocalizedField() 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() arabic_known_as = ArabicLocalizedField(source='known_as') nickname = LocalizedField() @@ -491,6 +492,7 @@ class TransmitterSerializer(serializers.ModelSerializer): 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) reliability = serializers.SerializerMethodField() + madhhabs = serializers.SerializerMethodField() share_link = serializers.CharField(read_only=True) class Meta: @@ -499,10 +501,22 @@ class TransmitterSerializer(serializers.ModelSerializer): 'id', 'full_name', 'arabic_full_name', 'slug', 'birth_year_hijri', 'death_year_hijri', 'age_at_death', '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): return format_transmitter_date(obj, 'birth', self.context.get('request')) @@ -530,8 +544,8 @@ class TransmitterShortSerializer(serializers.ModelSerializer): """Serializer for Transmitters""" full_name = LocalizedField() arabic_full_name = ArabicLocalizedField(source='full_name') - kunya = LocalizedField() - arabic_kunya = ArabicLocalizedField(source='kunya') + kunya = serializers.SerializerMethodField() + arabic_kunya = serializers.SerializerMethodField() known_as = LocalizedField() arabic_known_as = ArabicLocalizedField(source='known_as') nickname = LocalizedField() @@ -550,6 +564,12 @@ class TransmitterShortSerializer(serializers.ModelSerializer): '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): return format_transmitter_date(obj, 'birth', self.context.get('request')) @@ -1797,8 +1817,8 @@ class NarratorRelationSerializer(serializers.ModelSerializer): """ full_name = LocalizedField() arabic_full_name = ArabicLocalizedField(source='full_name') - kunya = LocalizedField() - arabic_kunya = ArabicLocalizedField(source='kunya') + kunya = serializers.SerializerMethodField() + arabic_kunya = serializers.SerializerMethodField() known_as = LocalizedField() arabic_known_as = ArabicLocalizedField(source='known_as') nickname = LocalizedField() @@ -1818,6 +1838,12 @@ class NarratorRelationSerializer(serializers.ModelSerializer): '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): return format_transmitter_date(obj, 'birth', self.context.get('request')) diff --git a/apps/hadis/utils/date_engine.py b/apps/hadis/utils/date_engine.py index 1997c15..6ae8e62 100644 --- a/apps/hadis/utils/date_engine.py +++ b/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() 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(("±", "≈")): s = "~" + s[1:].strip() elif s.startswith("~"): s = "~" + s[1:].strip() # 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) + # Replace Before Hijra (BH / год до хиджры / г. до хиджры / ق.هـ) with negative number + s = re.sub( + r"(?