From 22b1a5f5a5d427a3173989a614471086d74070de Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Mon, 8 Jun 2026 09:25:41 +0330 Subject: [PATCH] fix sync hadis issue and upate documentations --- apps/hadis/docs.py | 298 +++++++++++++++++++++-------- apps/hadis/serializers/category.py | 12 +- apps/hadis/serializers/hadis.py | 36 +++- apps/hadis/views/hadis.py | 150 +-------------- 4 files changed, 261 insertions(+), 235 deletions(-) diff --git a/apps/hadis/docs.py b/apps/hadis/docs.py index 1f5ac31..cadbb44 100644 --- a/apps/hadis/docs.py +++ b/apps/hadis/docs.py @@ -165,7 +165,7 @@ hadis_category_tree_swagger = swagger_auto_schema( # Swagger documentation for HadisSyncView hadis_sync_swagger = swagger_auto_schema( - operation_description="Get all Hadis data for offline sync. Returns a dictionary keyed by Hadis ID.", + operation_description="Get all Hadis data for offline sync. Returns a paginated-style payload with `count` and `results`.", operation_summary="Sync Hadis Data", operation_id="syncHadisData", tags=['Dobodbi - Hadis'], @@ -184,100 +184,232 @@ hadis_sync_swagger = swagger_auto_schema( examples={ "application/json": { "count": 1, - "results":[ - { - "id": 1001, - "number": 45, - "slug": "достоинство-молитвы-и-ее-место-в-религии", - "category_id": 205, - "title": "The Reward of Intentions", - "title_narrator": "Imam Sadiq (as)", - "text": "إنما الأعمال بالنیات...", - "translation": "hadis translation", - "detail": - {"address": 'null', - "hadis_status": { - "id": 130, - "title": "Прерванный", - "color": "orange" + "results": [ + { + "id": 1891, + "number": 1, + "slug": "hadis-ru-1-7c2df773", + "category_id": 1036, + "title": "The Merit of Seeking Knowledge", + "title_narrator": "Jabir bin Abdullah", + "text": "اتَّقِ اللَّهَ حَيْثُمَا كُنْتَ، وَأَتْبِعِ السَّيِّئَةَ الْحَسَنَةَ تَمْحُهَا.", + "translation": "The best among you are those who have the best manners and character.", + "detail": { + "address": [ + "Sahih al-Bukhari", + "Book of Revelation", + "Chapter 1: How Revelation Began" + ], + "address_details": [], + "hadis_status": { + "id": 133, + "title":"Authentic / Accepted", + "color": "green", + "main_color_code": "#1DAC43" }, - "status_text": 'null', - "share_link": "https://imamjavad.nwhco.ir/hadis/None", - "links": [ - { - "link": "https://example.com/source1", - "title": "Source 1" - }, + "status_text": "Authentic / Accepted", + "share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773", + "links": { + "Dorar": "https://dorar.net/hadith/1804", + "Al-Islam": "https://www.al-islam.org/6596", + "IslamWeb": "https://www.islamweb.net/ar/fatwa/6463", + "Sunnah.com": "https://sunnah.com/bukhari/3147", + "IslamicFinder": "https://www.islamicfinder.org/1779" + }, + "tags": [ { - "link": "https://example.com/source2", - "title": "Source 2" + "id": 516, + "title": "Доверие" } ], - "tags": [ - { - "id": 520, - "title": "Постановления" - }, - { - "id": 514, - "title": "Терпение" - }, - ], - "references": [ + "references": [ { - "id": 2193, - "title": 'null', - "authors": [], - "description": 'null' + "id": 2538, + "title": None, + "authors": [ + { + "id": 9, + "name": "Sheykh Mofid" + } + ], + "description":"For him who is not in the presence of a jurisprudent - Shia collection.", + "share_link": "https://dovodi.newhorizonco.uk/arguments/sources/man-la-yahduruhu-al-faqih" } ], - "reference_images": [ - { - "id": 1768, - "thumbnail": "http://127.0.0.1:8000/media/hadis/reference_images/ref_2193.png", - "priority": 0 - } - ] - }, - "narrators": { - "description": 'null', - "transmitters": [ - { - "id": 53, - "name": "Мухаммад ибн аль-Хасан ат-Туси", - "reliability": "unknown", - "layer_level": 'null', - "layer_name": 'null', - "is_gap": 'false', - "birth_year_hijri": 385, - "death_year_hijri": 460, - "order": 1 + "reference_images": [ + { + "id": 2541, + "thumbnail": "http://127.0.0.1:8000/media/hadis/reference_images/1891_ref1.png", + "priority": 1 + } + ] }, - { - "id": 60, - "name": "Мухаммад ибн Муслим", - "reliability": "unknown", - "layer_level": 'null', - "layer_name": 'null', - "is_gap": 'false', - "birth_year_hijri": 70, - "death_year_hijri": 150, - "order": 2 + "narrators": { + "description": None, + "transmitters": [ + { + "id": 1017, + "name": "Хинд бинт Хузайфа ибн аль-Мугира ибн Абдуллах ибн Умар ибн Махзум", + "slug": "хинд-бинт-хузайфа-ибн-аль-мугира-ибн-абдуллах-ибн-умар-ибн-махзум", + "known_as": "Умм Саляма Завдж ан-Наби", + "nickname": "Умм аль-Муминин", + "reliability": { + "id": 58, + "title": "⚫ сподвижник", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_level": 1, + "layer_name": "Companions", + "is_gap": False, + "birth_year_hijri": "28 год до Хиджры", + "death_year_hijri": "63", + "order": 0 + }, + { + "id": 1015, + "name": "Саад ибн Малик ибн Синан ибн Убайд ибн Саалаба ибн Убайд ибн Худра ибн Ауф ибн аль-Харис ибн аль-Хазрадж", + "slug": "саад-ибн-малик-ибн-синан-ибн-убайд-ибн-саалаба-ибн-убайд-ибн-худра-ибн-ауф-ибн-аль-харис-ибн-аль-хазрадж", + "known_as": "Абу Саид аль-Худри", + "nickname": "", + "reliability": { + "id": 58, + "title": "⚫ сподвижник", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_level": 1, + "layer_name": "Companions", + "is_gap": False, + "birth_year_hijri": "10 год до Хиджры", + "death_year_hijri": "63", + "order": 1 + }, + { + "id": 1018, + "name": "Атыйя ибн Саад ибн Джунада", + "slug": "атыйя-ибн-саад-ибн-джунада", + "known_as": "Атыйя ибн Саад аль-Ауфи", + "nickname": "", + "reliability": { + "id": 59, + "title": "🔵 слабый", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_level": 3, + "layer_name": "Successors of Successors", + "is_gap": False, + "birth_year_hijri": "", + "death_year_hijri": "111", + "order": 2 + }, + { + "id": 1026, + "name": "Фудайль ибн Газван ибн Джарир", + "slug": "фудайль-ибн-газван-ибн-джарир", + "known_as": "аль-Фудайль ибн Газван ад-Дабби", + "nickname": "Вольноотпущенник племени бану Дабби", + "reliability": { + "id": 60, + "title": "🟢 надежный", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_level": 7, + "layer_name": "Layer 7", + "is_gap": False, + "birth_year_hijri": "", + "death_year_hijri": "141", + "order": 3 + }, + { + "id": 1027, + "name": "Абдуллах ибн Дауд ибн Амир ибн ар-Раби", + "slug": "абдуллах-ибн-дауд-ибн-амир-ибн-ар-раби", + "known_as": "Абдуллах ибн Дауд аль-Хурайби", + "nickname": "", + "reliability": { + "id": 60, + "title": "🟢 надежный", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_level": 9, + "layer_name": "Layer 9", + "is_gap": False, + "birth_year_hijri": "126", + "death_year_hijri": "213", + "order": 4 + }, + { + "id": 1019, + "name": "Мухаммад ибн Исмаил ибн Иса", + "slug": "мухаммад-ибн-исмаил-ибн-иса", + "known_as": "Мухаммад ибн Исмаил аль-Басри", + "nickname": "Ибн Абу Самина", + "reliability": { + "id": 60, + "title": "🟢 надежный", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_level": 10, + "layer_name": "Layer 10", + "is_gap": False, + "birth_year_hijri": "", + "death_year_hijri": "230", + "order": 5 + }, + { + "id": 1023, + "name": "Ахмад ибн Али ибн аль-Мусанна ибн Яхья ибн Иса ибн Хилаль", + "slug": "ахмад-ибн-али-ибн-аль-мусанна-ибн-яхья-ибн-иса-ибн-хилаль", + "known_as": "Абу Яла аль-Маусили", + "nickname": "", + "reliability": { + "id": 61, + "title": "🟢 супернадежный", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_level": 13, + "layer_name": "Layer 13", + "is_gap": False, + "birth_year_hijri": "210", + "death_year_hijri": "307", + "order": 6 + } + ] }, - ] + "explanations": [ + { + "title": "Vocabulary & Linguistic Analysis", + "detail": "The term 'Niyyah' (Intention) is pivotal in Islamic lexicon, derived from the root 'nawa', meaning to aim, direct, or purpose oneself towards a specific destination. In the terminology of the jurists (Fuqaha), it serves a dual function: firstly, to distinguish acts of worship (Ibadah) from mundane customary habits (Adah)—such as distinguishing the act of washing oneself for cooling off from the act of Ghusl (ritual purification). Secondly, it distinguishes different ranks of worship from one another, such as separating a noon prayer that is obligatory (Fard) from one that is voluntary (Sunnah). Philosophically, scholars like Al-Ghazali have elaborated that Niyyah is the 'spirit' of the action, while the physical movement is merely the 'body'; a body without a spirit is lifeless, just as an action without sincere intention holds no weight in the Divine Court." }, - "explanations": "Example explanation...", + { + "title": "Context of Revelation (Asbab al-Wurud)", + "detail": "This tradition is famously known as the 'Hadith of the Migrant to Umm Qais'. It was delivered by the Prophet (PBUH) from the pulpit in Medina to address a subtle but critical issue within the community. Reports indicate that a man had migrated from Mecca to Medina not solely for the sake of Allah and His Messenger, but because a woman named Umm Qais had refused to marry him unless he undertook the migration. While his physical journey was identical to the other companions who sacrificed their homes for faith, his internal compass was directed towards a worldly gain. The Prophet (PBUH) used this specific incident to establish a universal maxim: that the validity and reward of any deed are inextricably bound to the hidden motive behind it, warning that one who targets the world will attain only that, with no share in the Hereafter." + } + ], "corrections": [ - { - "id":"id", - 'title':'title', - 'description':'description', - 'translation':'translation', - 'share_link':'share_link' - }, + { + "id": 26, + "title": "Scholar Note #4", + "description": "", + "translation": "Revised text segment for correction 4...", + "share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773/corrections/scholar-note-4" + }, + { + "id": 25, + "title": "Alternative Translation #3", + "description": "", + "translation": "Revised text segment for correction 3...", + "share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773/corrections/alternative-translation-3" + }, ] } - ] + ] } } ) @@ -2694,4 +2826,4 @@ hadis_category_xmind_swagger = swagger_auto_schema( description="Internal server error" ) } -) \ No newline at end of file +) diff --git a/apps/hadis/serializers/category.py b/apps/hadis/serializers/category.py index 38cdb13..f4830d4 100644 --- a/apps/hadis/serializers/category.py +++ b/apps/hadis/serializers/category.py @@ -25,16 +25,16 @@ def get_localized_text(json_list, request=None, fallback_lang="en", language_cod # 1) Exact match for item in json_list: if isinstance(item, dict) and item.get("language_code") == language_code: - return item.get("text") + return item.get("title") # 2) Fallback to English for item in json_list: if isinstance(item, dict) and item.get("language_code") == "en": - return item.get("text") + return item.get("title") # 3) First available if json_list and isinstance(json_list[0], dict): - return json_list[0].get("text") + return json_list[0].get("title") return None @@ -60,16 +60,16 @@ class LocalizedField(serializers.Field): # 1) Exact match with request language for item in value: if item.get("language_code") == language_code: - return item.get("text") + return item.get("title") # 2) Fallback to English for item in value: if item.get("language_code") == "en": - return item.get("text") + return item.get("title") # 3) Fallback to first item first = value[0] - return first.get("text") if isinstance(first, dict) else None + return first.get("title") if isinstance(first, dict) else None class SimpleCategory(serializers.ModelSerializer): title = LocalizedField() diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 89eeb18..8ef58ec 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -1,4 +1,6 @@ from rest_framework import serializers +import json + from django.utils.translation import gettext_lazy as _ from rest_framework.fields import SerializerMethodField from urllib3 import request @@ -14,6 +16,33 @@ from ..models import ( ) +def get_localized_correction_text(value, request=None): + """ + Resolve HadisCorrection.text in the same multilingual style used across the hadis app. + + Supports: + - a parsed JSON list: [{"language_code": "en", "text": "..."}] + - a JSON string containing that list + - plain text fallback + """ + if value in (None, ""): + return value + + parsed_value = value + if isinstance(value, str): + stripped = value.strip() + if stripped.startswith("[") or stripped.startswith("{"): + try: + parsed_value = json.loads(stripped) + except (TypeError, ValueError, json.JSONDecodeError): + return value + + if isinstance(parsed_value, list): + return get_localized_text(parsed_value, request=request) + + return value + + class PinnedHadisCollectionSerializer(serializers.ModelSerializer): title = LocalizedField() summary = LocalizedField() @@ -205,7 +234,7 @@ class HadisSyncSerializer(serializers.ModelSerializer): corrections_data.append({ 'id': correction.id, 'title': get_localized_text(correction.title, request), - 'description': correction.text, + 'description': get_localized_correction_text(correction.text, request=request), 'translation': get_localized_text(correction.translation, request), 'share_link': correction.share_link, }) @@ -713,7 +742,8 @@ class HadisCorrectionSerializer(serializers.ModelSerializer): model = HadisCorrection fields = ['id', 'title','slug','description', 'translation','share_link','bookmark'] def get_description(self, obj): - return obj.text + request = self.context.get('request') + return get_localized_correction_text(obj.text, request=request) def get_bookmark(self, obj): """Get bookmark information for this correction.""" @@ -867,4 +897,4 @@ class NarratorLayerSerializer(serializers.Serializer): def get_description(self, obj): """Get localized description""" request = self.context.get('request') - return get_localized_text(obj.description, request=request) \ No newline at end of file + return get_localized_text(obj.description, request=request) diff --git a/apps/hadis/views/hadis.py b/apps/hadis/views/hadis.py index e864800..5cb3762 100644 --- a/apps/hadis/views/hadis.py +++ b/apps/hadis/views/hadis.py @@ -137,151 +137,15 @@ class HadisSyncView(ListAPIView): return self.list(request, *args, **kwargs) def list(self, request, *args, **kwargs): - from django.utils.translation import get_language - - # 1. PRE-FETCH DATA ONCE queryset = self.get_queryset() - - # Get language once for the entire bulk operation - lang = getattr(request, "LANGUAGE_CODE", None) or get_language() or "en" - - # Pre-calculate base URL for images - media_url = request.build_absolute_uri('/')[:-1] # Remove trailing slash - - results = [] - for obj in queryset: - # --- Detail Block --- - status_block = None - if obj.hadis_status: - status_block = { - 'id': obj.hadis_status.id, - 'title': get_localized_text(obj.hadis_status.title, language_code=lang), - 'color': obj.hadis_status.color, - 'main_color_code': obj.hadis_status.main_color_code, - } - - tags_block = [ - {'id': tag.id, 'title': get_localized_text(tag.title, language_code=lang)} - for tag in obj.tags.all() - ] - - references_block = [] - reference_images_block = [] - for ref in obj.references.all(): - book = ref.book_reference - references_block.append({ - 'id': ref.id, - 'title': get_localized_text(book.title, language_code=lang) if book else None, - 'authors': [ - {'id': a.id, 'name': get_localized_text(a.name, language_code=lang)} - for a in (book.authors.all() if book else []) - ], - 'description': book.description if book else None, - }) - - for img in ref.images.all(): - reference_images_block.append({ - 'id': img.id, - 'thumbnail': f"{media_url}{img.thumbnail.url}" if img.thumbnail else None, - 'priority': img.priority, - }) - - address_details_list = [] - if hasattr(obj, 'address_details') and obj.address_details and isinstance(obj.address_details, list): - address_details_list = sorted(obj.address_details, key=lambda x: x.get('priority', 0)) - - detail_block = { - 'address': get_localized_text(obj.address, language_code=lang), - 'address_details': address_details_list, - 'hadis_status': status_block, - 'status_text': get_localized_text(obj.hadis_status_text, language_code=lang), - 'share_link': obj.share_link, - 'links': obj.links, - 'tags': tags_block, - 'references': references_block, - 'reference_images': reference_images_block, - } - - # --- Narrators Block --- - transmitters_data = [] - for tr_rel in obj.transmitters.all(): - t = tr_rel.transmitter - layer = tr_rel.narrator_layer - - rel_data = None - if t.reliability: - rel_data = { - 'id': t.reliability.id, - 'title': get_localized_text(t.reliability.title, language_code=lang), - 'color': t.reliability.color, - 'main_color_code': t.reliability.main_color_code, - } - - transmitters_data.append({ - 'id': t.id, - 'name': get_localized_text(t.full_name, language_code=lang), - 'slug': t.slug, - 'known_as': get_localized_text(t.known_as, language_code=lang), - 'nickname': get_localized_text(t.nickname, language_code=lang), - 'reliability': rel_data, - 'layer_level': layer.number if layer else None, - 'layer_name': get_localized_text(layer.name, language_code=lang) if layer else None, - 'is_gap': tr_rel.is_gap, - 'birth_year_hijri': t.birth_year_hijri, - 'death_year_hijri': t.death_year_hijri, - 'order': tr_rel.order, - }) - - narrators_block = { - 'description': get_localized_text(obj.description, language_code=lang), - 'transmitters': transmitters_data, - } - - # --- Explanations (Complex Logic) --- - explanation_data = [] - if hasattr(obj, 'explanations') and obj.explanations and isinstance(obj.explanations, list): - for item in obj.explanations: - if isinstance(item, dict) and item.get('language_code') == lang: - explanation_data.append({ - 'title': item.get('title', ''), - 'description': item.get('description', '') - }) - - if not explanation_data and obj.explanation: - explanation_data = get_localized_text(obj.explanation, language_code=lang) - else: - explanation_data = explanation_data if explanation_data else None - - # --- Corrections --- - corrections_block = [] - for c in obj.hadiscorrection_set.all(): - corrections_block.append({ - 'id': c.id, - 'title': get_localized_text(c.title, language_code=lang), - 'description': get_localized_text(c.description, language_code=lang), - 'translation': get_localized_text(c.translation, language_code=lang), - 'share_link': c.share_link, - }) - - # --- Assemble Hadis Item --- - results.append({ - 'id': obj.id, - 'number': obj.number, - 'slug': obj.slug, - 'category_id': obj.category_id, - 'title': get_localized_text(obj.title, language_code=lang), - 'title_narrator': get_localized_text(obj.title_narrator, language_code=lang), - 'text': obj.text, # Usually JSON structure in our app - 'translation': get_localized_text(obj.translation, language_code=lang), - 'detail': detail_block, - 'narrators': narrators_block, - 'explanations': explanation_data, - 'corrections': corrections_block, - }) - + serializer = self.get_serializer( + queryset, + many=True, + context={'request': request}, + ) return Response({ - 'count': len(results), - 'results': results + 'count': len(serializer.data), + 'results': serializer.data })