Browse Source

feat(hadis): add lightweight on-demand source details endpoint and documentation

master
Mohsen Taba 3 weeks ago
parent
commit
9560f18c72
  1. 142
      apps/hadis/serializers/hadis.py
  2. 4
      apps/hadis/urls.py
  3. 47
      apps/hadis/views/hadis.py
  4. 105
      docs/FLUTTER_MODALS_API_GUIDE.md

142
apps/hadis/serializers/hadis.py

@ -418,26 +418,93 @@ class TransmitterOpinionSerializer(serializers.ModelSerializer):
} }
return None return None
def format_address_output(ref):
if not ref or not ref.address:
def format_address_output(ref_or_data):
if not ref_or_data:
return [] return []
addr = ref.address
if hasattr(ref_or_data, 'address'):
addr = ref_or_data.address
else:
addr = ref_or_data
if not addr:
return []
if isinstance(addr, str): if isinstance(addr, str):
trimmed = addr.strip() trimmed = addr.strip()
if trimmed.startswith("["):
if trimmed.startswith("[") or trimmed.startswith("{"):
try: try:
import json import json
parsed = json.loads(trimmed) parsed = json.loads(trimmed)
if isinstance(parsed, list):
return parsed
return format_address_output(parsed)
except (ValueError, TypeError): except (ValueError, TypeError):
pass pass
return [addr] if trimmed else []
return [trimmed] if trimmed else []
elif isinstance(addr, list): elif isinstance(addr, list):
return addr
result = []
for item in addr:
if isinstance(item, str):
s = item.strip()
if s.startswith("[") or s.startswith("{"):
try:
import json
parsed = json.loads(s)
result.extend(format_address_output(parsed))
continue
except (ValueError, TypeError):
pass
if s:
result.append(s)
elif isinstance(item, dict):
# could be {'title': ['pok'], 'language_code': 'fa'} or {'title': 'pok'} or {'address': '...'} or {'text': '...'}
val = item.get('title') or item.get('address') or item.get('text') or item.get('value')
if val:
result.extend(format_address_output(val))
elif isinstance(item, list):
result.extend(format_address_output(item))
return result
return [str(addr)] return [str(addr)]
def format_links_output(links_data):
if not links_data:
return []
if isinstance(links_data, str):
trimmed = links_data.strip()
if trimmed.startswith("[") or trimmed.startswith("{"):
try:
import json
parsed = json.loads(trimmed)
return format_links_output(parsed)
except (ValueError, TypeError):
pass
return [trimmed] if trimmed else []
elif isinstance(links_data, dict):
out = []
for k, v in links_data.items():
if isinstance(v, str) and v.strip():
out.append(v.strip())
elif isinstance(v, (list, dict)):
out.extend(format_links_output(v))
return out
elif isinstance(links_data, list):
out = []
for item in links_data:
if isinstance(item, str):
s = item.strip()
if s:
out.append(s)
elif isinstance(item, dict):
url = item.get('url') or item.get('link') or item.get('href') or item.get('value')
if url and isinstance(url, str) and url.strip():
out.append(url.strip())
elif isinstance(item, list):
out.extend(format_links_output(item))
return out
return []
class TransmitterOriginalTextSerializer(serializers.ModelSerializer): class TransmitterOriginalTextSerializer(serializers.ModelSerializer):
""" Serializer for TransmitterOriginalText """ """ Serializer for TransmitterOriginalText """
title = LocalizedField() title = LocalizedField()
@ -1336,3 +1403,62 @@ class TransmitterOriginalTextDetailSerializer(serializers.ModelSerializer):
"thumbnail": url, "thumbnail": url,
}) })
return images_list return images_list
class HadisSourceDetailsSerializer(serializers.ModelSerializer):
"""
Lightweight serializer for Hadis Source Details (Address, Links, Images).
Used on-demand when opening source detail modals in web and mobile clients.
"""
title = LocalizedField()
narrator = LocalizedField(source='title_narrator')
source_type = serializers.SerializerMethodField()
address = serializers.SerializerMethodField()
links = serializers.SerializerMethodField()
images = serializers.SerializerMethodField()
class Meta:
model = Hadis
fields = [
'id', 'slug', 'title', 'narrator', 'source_type',
'address', 'links', 'images', 'share_link'
]
def get_source_type(self, obj):
if obj.category and obj.category.source_type:
return obj.category.source_type
return 'hadith'
def get_address(self, obj):
# 1. Primary: obj.address
formatted = format_address_output(obj.address)
if formatted:
return formatted
# 2. Fallback: references address
first_ref = obj.references.first()
if first_ref and hasattr(first_ref, 'address') and first_ref.address:
return format_address_output(first_ref.address)
return []
def get_links(self, obj):
return format_links_output(obj.links)
def get_images(self, obj):
request = self.context.get('request')
images_list = []
for ref in obj.references.all():
for img in ref.images.all().order_by('priority'):
url = None
if img.thumbnail:
url = absolute_https_url(img.thumbnail.url, request) if request else absolute_https_url(img.thumbnail.url)
elif hasattr(img, 'image') and img.image:
url = absolute_https_url(img.image.url, request) if request else absolute_https_url(img.image.url)
if url:
images_list.append({
"id": img.id,
"image": url,
"thumbnail": url,
"priority": img.priority,
})
return images_list

4
apps/hadis/urls.py

@ -3,7 +3,7 @@ from rest_framework.routers import SimpleRouter
from .views.category import HadisCategorySectListView, HadisCategoryTreeView, CategoriesView, CategoriesBySectView, HadisCategorySelectBySectView, HadisCategorySelectBySectSourceView , HadisCategoryTreeNormalView ,test_deploy,debug_headers,HadisCategoryXMindView from .views.category import HadisCategorySectListView, HadisCategoryTreeView, CategoriesView, CategoriesBySectView, HadisCategorySelectBySectView, HadisCategorySelectBySectSourceView , HadisCategoryTreeNormalView ,test_deploy,debug_headers,HadisCategoryXMindView
from .views.hadis import ( from .views.hadis import (
HadisCollectionListView, HadisListView, HadisBasicView, HadisDetailView, HadisSyncView, HadisTransmittersView, HadisCorrectionsView,HadisMainListView, HadisFiltersView, HadisLayersView,PinnedHadisCollectionListView, HadisFiltersSyncAPIView, HadisCollectionListView, HadisListView, HadisBasicView, HadisDetailView, HadisSyncView, HadisTransmittersView, HadisCorrectionsView,HadisMainListView, HadisFiltersView, HadisLayersView,PinnedHadisCollectionListView, HadisFiltersSyncAPIView,
HadisCorrectionDetailView, HadisInterpretationDetailView, HadisInterpretsListView
HadisCorrectionDetailView, HadisInterpretationDetailView, HadisInterpretsListView, HadisSourceDetailsView
) )
from .views.transmitter import ( from .views.transmitter import (
TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView, TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView,
@ -124,6 +124,8 @@ urlpatterns = [
path('references/', BookReferencesView.as_view(), name='references'), path('references/', BookReferencesView.as_view(), name='references'),
# Hadis detail paths (with slug, more specific) # Hadis detail paths (with slug, more specific)
path('<str:hadis_slug>/source-details/', HadisSourceDetailsView.as_view(), name='hadis-source-details'),
path('<str:hadis_slug>/sources/', HadisSourceDetailsView.as_view(), name='hadis-sources'),
path('<str:hadis_slug>/detail/', HadisDetailView.as_view(), name='hadis-detail'), path('<str:hadis_slug>/detail/', HadisDetailView.as_view(), name='hadis-detail'),
path('<str:hadis_slug>/transmitters/', HadisTransmittersView.as_view(), name='hadis-transmitters'), path('<str:hadis_slug>/transmitters/', HadisTransmittersView.as_view(), name='hadis-transmitters'),
path('<str:hadis_slug>/transmitters/layers/', HadisLayersView.as_view(), name='hadis-layers'), path('<str:hadis_slug>/transmitters/layers/', HadisLayersView.as_view(), name='hadis-layers'),

47
apps/hadis/views/hadis.py

@ -9,7 +9,7 @@ from django.db.models import Count
from django.db.models import Prefetch from django.db.models import Prefetch
from ..serializers.category import get_localized_text from ..serializers.category import get_localized_text
from ..models import Transmitters, HadisCategory, Hadis, HadisCollection,HadisTransmitter , HadisCorrection ,HadisReference, HadisStatus ,ReferenceImage from ..models import Transmitters, HadisCategory, Hadis, HadisCollection,HadisTransmitter , HadisCorrection ,HadisReference, HadisStatus ,ReferenceImage
from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer , PinnedHadisCollectionSerializer
from ..serializers import HadisListSerializer, HadisBasicSerializer, HadisDetailSerializer, HadisCollectionListSerializer, HadisSyncSerializer,HadisCorrectionSerializer,HadisTransmitterListSerializer , SimpleCategory, NarratorLayerSerializer , PinnedHadisCollectionSerializer, HadisSourceDetailsSerializer
from ..docs import arguments_filters_swagger ,hadis_list_swagger, hadis_detail_swagger, hadis_collections_swagger, hadis_sync_swagger, hadis_transmitters_swagger, hadis_corrections_swagger, hadis_basic_swagger, hadis_main_list_swagger, hadis_layers_swagger from ..docs import arguments_filters_swagger ,hadis_list_swagger, hadis_detail_swagger, hadis_collections_swagger, hadis_sync_swagger, hadis_transmitters_swagger, hadis_corrections_swagger, hadis_basic_swagger, hadis_main_list_swagger, hadis_layers_swagger
from django.db.models import Q from django.db.models import Q
from ..serializers.category import get_localized_text from ..serializers.category import get_localized_text
@ -974,4 +974,47 @@ class HadisInterpretsListView(ListAPIView):
'references__images' 'references__images'
).order_by('priority', 'id') ).order_by('priority', 'id')
except Hadis.DoesNotExist: except Hadis.DoesNotExist:
return HadisInterpretation.objects.none()
return HadisInterpretation.objects.none()
class HadisSourceDetailsView(RetrieveAPIView):
"""
API view to retrieve lightweight source details (addresses, links, reference images)
for a hadis/argument on demand when opening modals.
Supports lookup by slug or by numeric id.
"""
serializer_class = HadisSourceDetailsSerializer
permission_classes = [AllowAny]
lookup_field = 'slug'
lookup_url_kwarg = 'hadis_slug'
@swagger_auto_schema(
operation_summary="Get Hadis Source Details",
operation_description="Returns lightweight source details (addresses, links, images) for a specific hadis by slug or ID.",
tags=['Dobodbi - Hadis (V2)'],
)
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
def get_object(self):
slug_or_id = self.kwargs.get(self.lookup_url_kwarg or self.lookup_field)
queryset = Hadis.objects.filter(status=True).select_related(
'category'
).prefetch_related(
Prefetch(
'references',
queryset=HadisReference.objects.prefetch_related(
Prefetch('images', queryset=ReferenceImage.objects.order_by('priority'))
)
)
)
if slug_or_id.isdigit():
obj = queryset.filter(id=int(slug_or_id)).first()
else:
obj = queryset.filter(slug=slug_or_id).first()
if not obj:
from django.http import Http404
raise Http404("No Hadis matches the given query.")
self.check_object_permissions(self.request, obj)
return obj

105
docs/FLUTTER_MODALS_API_GUIDE.md

@ -0,0 +1,105 @@
# 📱 راهنمای API و مپینگ دیتا برای مودال‌های فلاتر (Source Details)
این مستند نحوه نمایش اطلاعات منبع در مودال‌های کلاینت فلاتر (Flutter) را مشخص می‌کند.
---
### 📌 قاعده کلی مودال‌ها
> **توجه:** در مودال‌ها **متن اصلی (Original Text)** و **ترجمه (Translation)** قرار نمی‌گیرند (زیرا این دو مورد روی خود کارت در صفحه اصلی نمایش داده شده‌اند).
> هدف اصلی این مودال‌ها نمایش **اطلاعات و مستندات منبع (Source Details)** است.
---
### ۱. تفکیک محتوای مودال‌ها بر اساس نوع ارگیومنت
| نوع ارگیومنت (Source Type) | فیلدهای مورد نیاز در مودال | فیلدهای نادیده‌گرفته‌شده در مودال |
| :--- | :--- | :--- |
| **`quote`** (نقل‌قول / متن اصلی راوی) | **آدرس‌ها (`address`)** + **عکس‌های کتاب (`images`)** | فاقد لینک و متن تکراری |
| **`fatwa`** (فتوا) | **آدرس‌ها (`address`)** + **لینک‌ها (`links`)** + **عکس‌های کتاب (`images`)** | فاقد متن تکراری |
| **`history`** (تاریخ) | **آدرس‌ها (`address`)** + **لینک‌ها (`links`)** + **عکس‌های کتاب (`images`)** | فاقد متن تکراری |
| **`interpretations`** (تفاسیر) | **آدرس‌ها (`address`)** + **لینک‌ها (`links`)** + **عکس‌های کتاب (`images`)** | فاقد متن تکراری |
| **`corrections`** (تصحیحات) | **آدرس‌ها (`address`)** + **لینک‌ها (`links`)** + **عکس‌های کتاب (`images`)** | فاقد متن تکراری |
---
### ۲. منابع دریافت دیتا (API Endpoints)
#### 🚀 اندپوینت سبک و اختصاصی باز کردن مودال (On-Demand Modal API):
برای بهینه‌سازی و عدم بارگذاری سنگین در لیست‌ها، اندپوینت اختصاصی زیر ساخته شده تا هنگام باز شدن هر مودال، اطلاعات منبع (آدرس، لینک‌ها و تصاویر) را مستقیماً دریافت کنید:
* **اندپوینت:** `GET /api/hadis/<hadis_slug_or_id>/source-details/` (یا `GET /api/hadis/<hadis_slug_or_id>/sources/`)
* **ورودی:** اسلاگ حدیث یا آی‌دی عددی آن.
* **خروجی:** آبجکت کامل منبع شامل `address` (لیست رشته‌ها)، `links` (لیست لینک‌ها) و `images` (لیست تصاویر).
---
#### الف) تفاسیر و تصحیحات (Interpretations & Corrections):
* **حالت آفلاین (Sync):**
* اندپوینت: `GET /api/hadis/sync/hadis/`
* تفاسیر: داخل آرایه `interpretations` هر آبجکت حدیث.
* تصحیحات: داخل آرایه `corrections` هر آبجکت حدیث.
* **حالت آنلاین:**
* اندپوینت جزئیات تصحیح: `GET /api/hadis/corrections/<correction_slug>/`
* اندپوینت جزئیات تفسیر: `GET /api/hadis/interpretations/<interpretation_slug>/`
#### ب) متون اصلی راوی (Transmitter Original Texts / Quotes):
* **حالت آنلاین:** `GET /api/hadis/narrators/<narrator_slug>/original_texts/` یا `GET /api/hadis/original-texts/<original_text_slug>/`
* **حالت آفلاین (Sync):** `GET /api/hadis/sync/narrators/` $\rightarrow$ داخل آرایه `original_texts` هر آبجکت راوی.
#### ج) سایر ارگیومنت‌ها (فتاوا، تاریخ، نقل‌قول‌ها):
* **حالت آنلاین (درخواست مودال):** `GET /api/hadis/<hadis_slug_or_id>/source-details/`
* **حالت آفلاین (Sync):** `GET /api/hadis/sync/hadis/` $\rightarrow$ فیلدهای `detail.address`، `detail.links` و `detail.reference_images`.
---
### ۳. جدول مپینگ فیلدها در UI مودال
| فیلد در API | نوع داده | نحوه استفاده و رندر در مودال فلاتر |
| :--- | :--- | :--- |
| `title` | `String` | عنوان بالای مودال (مثلاً نام کتاب یا عنوان موضوع) |
| `address` | `List<String>` یا `String` | لیست خطوط آدرس، جلد، صفحه و چاپ منبع |
| `images` | `List<{ id, image, thumbnail }>` | گالری افقی بندانگشتی تصاویر نسخ خطی با قابلیت کلیک و نمایش تمام‌صفحه |
| `links` | `List<String>` | دکمه‌ها یا لیست لینک‌های وب‌سایت منبع *(فقط در fatwa، history، interpretations و corrections)* |
---
### 📦 نمونه دیتای مپینگ برای فلاتر
#### ۱. نمونه مودال برای `quote` (بدون لینک):
```json
{
"title": "رواية زرارة",
"address": [
"الکافی، ج ۱، ص ۲۵، طبع دارالحدیث",
"التهذیب، ج ۲، ص ۱۰"
],
"images": [
{
"id": 10,
"image": "https://dovodi.newhorizonco.uk/media/hadis/book_references/kafi_p25.png",
"thumbnail": "https://dovodi.newhorizonco.uk/media/hadis/book_references/kafi_p25.png"
}
]
}
```
#### ۲. نمونه مودال برای `fatwa` / `history` / `interpretations` / `corrections` (همراه با لینک):
```json
{
"title": "تفسیر آیه تطهیر",
"address": [
"التبیان فی تفسیر القرآن، ج ۲، ص ۴۵، طبع دار احیاء التراث العربی",
"مجمع البیان، ج ۴، ص ۱۲"
],
"links": [
"https://shamela.ws/book/123/45",
"https://lib.eshia.ir/12345/2/45"
],
"images": [
{
"id": 25,
"image": "https://dovodi.newhorizonco.uk/media/hadis/book_references/tibyan_p45.png",
"thumbnail": "https://dovodi.newhorizonco.uk/media/hadis/book_references/tibyan_p45.png"
}
]
}
```
Loading…
Cancel
Save