Browse Source

fix sync hadis issue and upate documentations

master
Mohsen Taba 1 month ago
parent
commit
22b1a5f5a5
  1. 294
      apps/hadis/docs.py
  2. 12
      apps/hadis/serializers/category.py
  3. 34
      apps/hadis/serializers/hadis.py
  4. 150
      apps/hadis/views/hadis.py

294
apps/hadis/docs.py

@ -165,7 +165,7 @@ hadis_category_tree_swagger = swagger_auto_schema(
# Swagger documentation for HadisSyncView # Swagger documentation for HadisSyncView
hadis_sync_swagger = swagger_auto_schema( 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_summary="Sync Hadis Data",
operation_id="syncHadisData", operation_id="syncHadisData",
tags=['Dobodbi - Hadis'], tags=['Dobodbi - Hadis'],
@ -184,97 +184,229 @@ hadis_sync_swagger = swagger_auto_schema(
examples={ examples={
"application/json": { "application/json": {
"count": 1, "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": [ "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"
},
] ]
} }
] ]

12
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 # 1) Exact match
for item in json_list: for item in json_list:
if isinstance(item, dict) and item.get("language_code") == language_code: if isinstance(item, dict) and item.get("language_code") == language_code:
return item.get("text")
return item.get("title")
# 2) Fallback to English # 2) Fallback to English
for item in json_list: for item in json_list:
if isinstance(item, dict) and item.get("language_code") == "en": if isinstance(item, dict) and item.get("language_code") == "en":
return item.get("text")
return item.get("title")
# 3) First available # 3) First available
if json_list and isinstance(json_list[0], dict): if json_list and isinstance(json_list[0], dict):
return json_list[0].get("text")
return json_list[0].get("title")
return None return None
@ -60,16 +60,16 @@ class LocalizedField(serializers.Field):
# 1) Exact match with request language # 1) Exact match with request language
for item in value: for item in value:
if item.get("language_code") == language_code: if item.get("language_code") == language_code:
return item.get("text")
return item.get("title")
# 2) Fallback to English # 2) Fallback to English
for item in value: for item in value:
if item.get("language_code") == "en": if item.get("language_code") == "en":
return item.get("text")
return item.get("title")
# 3) Fallback to first item # 3) Fallback to first item
first = value[0] 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): class SimpleCategory(serializers.ModelSerializer):
title = LocalizedField() title = LocalizedField()

34
apps/hadis/serializers/hadis.py

@ -1,4 +1,6 @@
from rest_framework import serializers from rest_framework import serializers
import json
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from rest_framework.fields import SerializerMethodField from rest_framework.fields import SerializerMethodField
from urllib3 import request 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): class PinnedHadisCollectionSerializer(serializers.ModelSerializer):
title = LocalizedField() title = LocalizedField()
summary = LocalizedField() summary = LocalizedField()
@ -205,7 +234,7 @@ class HadisSyncSerializer(serializers.ModelSerializer):
corrections_data.append({ corrections_data.append({
'id': correction.id, 'id': correction.id,
'title': get_localized_text(correction.title, request), '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), 'translation': get_localized_text(correction.translation, request),
'share_link': correction.share_link, 'share_link': correction.share_link,
}) })
@ -713,7 +742,8 @@ class HadisCorrectionSerializer(serializers.ModelSerializer):
model = HadisCorrection model = HadisCorrection
fields = ['id', 'title','slug','description', 'translation','share_link','bookmark'] fields = ['id', 'title','slug','description', 'translation','share_link','bookmark']
def get_description(self, obj): 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): def get_bookmark(self, obj):
"""Get bookmark information for this correction.""" """Get bookmark information for this correction."""

150
apps/hadis/views/hadis.py

@ -137,151 +137,15 @@ class HadisSyncView(ListAPIView):
return self.list(request, *args, **kwargs) return self.list(request, *args, **kwargs)
def list(self, 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() 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({ return Response({
'count': len(results),
'results': results
'count': len(serializer.data),
'results': serializer.data
}) })

Loading…
Cancel
Save