diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 66689da..045605b 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -418,26 +418,93 @@ class TransmitterOpinionSerializer(serializers.ModelSerializer): } 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 [] - 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): trimmed = addr.strip() - if trimmed.startswith("["): + if trimmed.startswith("[") or trimmed.startswith("{"): try: import json parsed = json.loads(trimmed) - if isinstance(parsed, list): - return parsed + return format_address_output(parsed) except (ValueError, TypeError): pass - return [addr] if trimmed else [] + return [trimmed] if trimmed else [] + 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)] +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): """ Serializer for TransmitterOriginalText """ title = LocalizedField() @@ -1336,3 +1403,62 @@ class TransmitterOriginalTextDetailSerializer(serializers.ModelSerializer): "thumbnail": url, }) 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 + diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index 2c06a48..897abea 100644 --- a/apps/hadis/urls.py +++ b/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.hadis import ( HadisCollectionListView, HadisListView, HadisBasicView, HadisDetailView, HadisSyncView, HadisTransmittersView, HadisCorrectionsView,HadisMainListView, HadisFiltersView, HadisLayersView,PinnedHadisCollectionListView, HadisFiltersSyncAPIView, - HadisCorrectionDetailView, HadisInterpretationDetailView, HadisInterpretsListView + HadisCorrectionDetailView, HadisInterpretationDetailView, HadisInterpretsListView, HadisSourceDetailsView ) from .views.transmitter import ( TransmitterView ,TransmitterDetailView, TransmitterSyncView,TransmitterOpinionView, TransmitterOriginalTextView, TransmitterFiltersView, @@ -124,6 +124,8 @@ urlpatterns = [ path('references/', BookReferencesView.as_view(), name='references'), # Hadis detail paths (with slug, more specific) + path('/source-details/', HadisSourceDetailsView.as_view(), name='hadis-source-details'), + path('/sources/', HadisSourceDetailsView.as_view(), name='hadis-sources'), path('/detail/', HadisDetailView.as_view(), name='hadis-detail'), path('/transmitters/', HadisTransmittersView.as_view(), name='hadis-transmitters'), path('/transmitters/layers/', HadisLayersView.as_view(), name='hadis-layers'), diff --git a/apps/hadis/views/hadis.py b/apps/hadis/views/hadis.py index 0ee6880..2f59388 100644 --- a/apps/hadis/views/hadis.py +++ b/apps/hadis/views/hadis.py @@ -9,7 +9,7 @@ from django.db.models import Count from django.db.models import Prefetch from ..serializers.category import get_localized_text 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 django.db.models import Q from ..serializers.category import get_localized_text @@ -974,4 +974,47 @@ class HadisInterpretsListView(ListAPIView): 'references__images' ).order_by('priority', 'id') except Hadis.DoesNotExist: - return HadisInterpretation.objects.none() \ No newline at end of file + 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 + \ No newline at end of file diff --git a/docs/FLUTTER_MODALS_API_GUIDE.md b/docs/FLUTTER_MODALS_API_GUIDE.md new file mode 100644 index 0000000..7aa1423 --- /dev/null +++ b/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//source-details/` (یا `GET /api/hadis//sources/`) +* **ورودی:** اسلاگ حدیث یا آی‌دی عددی آن. +* **خروجی:** آبجکت کامل منبع شامل `address` (لیست رشته‌ها)، `links` (لیست لینک‌ها) و `images` (لیست تصاویر). + +--- + +#### الف) تفاسیر و تصحیحات (Interpretations & Corrections): +* **حالت آفلاین (Sync):** + * اندپوینت: `GET /api/hadis/sync/hadis/` + * تفاسیر: داخل آرایه `interpretations` هر آبجکت حدیث. + * تصحیحات: داخل آرایه `corrections` هر آبجکت حدیث. +* **حالت آنلاین:** + * اندپوینت جزئیات تصحیح: `GET /api/hadis/corrections//` + * اندپوینت جزئیات تفسیر: `GET /api/hadis/interpretations//` + +#### ب) متون اصلی راوی (Transmitter Original Texts / Quotes): +* **حالت آنلاین:** `GET /api/hadis/narrators//original_texts/` یا `GET /api/hadis/original-texts//` +* **حالت آفلاین (Sync):** `GET /api/hadis/sync/narrators/` $\rightarrow$ داخل آرایه `original_texts` هر آبجکت راوی. + +#### ج) سایر ارگیومنت‌ها (فتاوا، تاریخ، نقل‌قول‌ها): +* **حالت آنلاین (درخواست مودال):** `GET /api/hadis//source-details/` +* **حالت آفلاین (Sync):** `GET /api/hadis/sync/hadis/` $\rightarrow$ فیلدهای `detail.address`، `detail.links` و `detail.reference_images`. + +--- + +### ۳. جدول مپینگ فیلدها در UI مودال + +| فیلد در API | نوع داده | نحوه استفاده و رندر در مودال فلاتر | +| :--- | :--- | :--- | +| `title` | `String` | عنوان بالای مودال (مثلاً نام کتاب یا عنوان موضوع) | +| `address` | `List` یا `String` | لیست خطوط آدرس، جلد، صفحه و چاپ منبع | +| `images` | `List<{ id, image, thumbnail }>` | گالری افقی بندانگشتی تصاویر نسخ خطی با قابلیت کلیک و نمایش تمام‌صفحه | +| `links` | `List` | دکمه‌ها یا لیست لینک‌های وب‌سایت منبع *(فقط در 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" + } + ] +} +```