diff --git a/apps/hadis/HADIS_SYNC_SAMPLE.md b/apps/hadis/HADIS_SYNC_SAMPLE.md new file mode 100644 index 0000000..99dc165 --- /dev/null +++ b/apps/hadis/HADIS_SYNC_SAMPLE.md @@ -0,0 +1,492 @@ +# نمونه کامل خروجی یک آبجکت حدیث در اندپوینت سینک (`/api/hadis/sync/hadis/`) + +این سند ساختار دقیق، واقعی و تودرتوی **فقط یک رکورد حدیث** (حدیث شماره ۱) را نشان می‌دهد که مستقیماً از دیتابیس فعلی و از طریق سریالایزر `HadisSyncSerializer` استخراج شده است. + +--- + +## 📊 مشخصات این آبجکت: +- **تعداد خطوط JSON برای یک حدیث:** ۶۸۹ خط! +- **حجم خالص متنی برای یک حدیث:** حدود **۳۲ کیلوبایت** +- **حجم کل برای ۱۰۵۳ حدیث:** حدود **۳۳ مگابایت** (به دلیل تکرار داده‌های تودرتو) + +--- + +## 🔍 نقاط بحرانی تکرار و تودرتو بودن (Redundancy & Bloat): + +1. **تکرار تفاسیر دسته‌بندی در هر حدیث (`interpretations`):** + - تفاسیر به دسته‌بندی تعلق دارند، اما در خروجی **داخل تک‌تک احادیث آن دسته تکرار می‌شوند**. متون تفاسیر شامل کدهای طولانی HTML عربی و روسی هستند. +2. **تکرار مشخصات راویان و لایه‌ها (`transmitters`):** + - برای هر راوی در زنجیره سند (مثلاً ۱۰ راوی)، آبجکت‌های کامل `reliability` و `layer_name` و `layer_description` به همراه لیستی از `uncertain_transmitters` تکرار می‌شوند. +3. **تکرار اطلاعات کتاب‌ها و رفرنس‌ها در تصحیحات (`corrections -> references`):** + - داخل هر تصحیح، آبجکت‌های رفرنس، تصاویر، مشخصات چاپ (`edition_info`) و لینک‌ها به صورت تودرتو دوباره تکرار شده‌اند. +4. **ارجاع به خودِ حدیث در بخش تصحیحات (`hadis_info`):** + - داخل آرایه تصحیحاتِ همین حدیث، فیلد `hadis_info` دوباره `id` و `slug` خودِ همین حدیث را تکرار می‌کند! + +--- + +## 📄 ساختار کامل JSON (نمونه واقعی استخراج‌شده از دیتابیس): + +```json +{ + "id": 1891, + "number": 1, + "slug": "the-merit-of-seeking-knowledge", + "category_id": 1036, + "category_slug": "reflections", + "title": "The Merit of Seeking Knowledge", + "title_narrator": "Jabir bin Abdullah", + "text": "اتَّقِ اللَّهَ حَيْثُمَا كُنْتَ، وَأَتْبِعِ السَّيِّئَةَ الْحَسَنَةَ تَمْحُهَا.", + "translation": [ + { + "title": "Передается от Пророка (мир ему): Правда ведет к благочестию. Это путь праведных.", + "language_code": "ru" + }, + { + "title": "The best among you are those who have the best manners and character.", + "language_code": "en" + } + ], + "detail": { + "address": [ + "Sahih al-Bukhari", + "Book of Revelation", + "Chapter 1: How Revelation Began" + ], + "address_details": [], + "hadis_status": { + "id": 133, + "title": "Authentic / Accepted", + "description": "A Sahih (Authentic) Hadith represents the highest level of reliability. It possesses a continuous chain of narrators (Isnad) made up of upright (Adl) and retentive (Dabit) transmitters, free from any hidden defects (Illah) or irregularities (Shudhudh). It is universally accepted for deriving Islamic laws.", + "color": "green", + "main_color_code": "#1DAC43" + }, + "status_text": null, + "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": [ + { + "id": 510, + "title": "Хадж" + }, + { + "id": 516, + "title": "Доверие" + }, + { + "id": 561, + "title": "Kindness" + }, + { + "id": 565, + "title": "Hellfire (Jahannam)" + }, + { + "id": 567, + "title": "Marriage (Nikah)" + } + ], + "references": [ + { + "id": 2538, + "title": "Man La Yahduruhu al-Faqih", + "authors": [ + { + "id": 9, + "name": "sheykh hamid" + } + ], + "description": [ + { + "title": "For him who is not in the presence of a jurisprudent - Shia collection.", + "language_code": "en" + }, + { + "title": "توضیحات درباره من لا یحضره الفقیه", + "language_code": "fa" + } + ], + "share_link": "https://dovodi.newhorizonco.uk/arguments/sources/man-la-yahduruhu-al-faqih" + } + ], + "reference_images": [ + { + "id": 2541, + "thumbnail": "https://dovodi.newhorizonco.uk/media/hadis/reference_images/1891_ref1.png", + "priority": 1 + }, + { + "id": 2542, + "thumbnail": "https://dovodi.newhorizonco.uk/media/hadis/reference_images/1891_ref2.png", + "priority": 2 + }, + { + "id": 2543, + "thumbnail": "https://dovodi.newhorizonco.uk/media/hadis/reference_images/1891_ref3.png", + "priority": 3 + }, + { + "id": 2544, + "thumbnail": "https://dovodi.newhorizonco.uk/media/hadis/reference_images/1891_ref4.png", + "priority": 4 + } + ] + }, + "narrators": { + "description": "Focuses on the relationship between the Creator and the creation.", + "transmitters": [ + { + "id": 850, + "name": "Ali ibn Yaqtin", + "slug": "ali-ibn-yaqtin", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 14, + "title": "Reliable", + "slug": "reliable-1-190858903395", + "color": "blue", + "main_color_code": "#5172E1" + }, + "layer_id": 5699, + "narrator_layer_id": 5699, + "layer_level": 1, + "layer_name": "Primary Chain & Early Narrators", + "layer_description": "Early generation transmitters, companions, and direct students.", + "is_gap": true, + "is_uncertain": false, + "uncertain_transmitters": [], + "birth_year_hijri": null, + "death_year_hijri": "40", + "order": 0 + }, + { + "id": 859, + "name": "Al-Muhaqqiq al-Hilli (Ja'far ibn al-Hasan)", + "slug": "al-muhaqqiq-al-hilli-jafar-ibn-al-hasan", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 17, + "title": "Very Weak", + "slug": "very-weak-1-190858905701", + "color": "red", + "main_color_code": "#D33A3A" + }, + "layer_id": 5699, + "narrator_layer_id": 5699, + "layer_level": 1, + "layer_name": "Primary Chain & Early Narrators", + "layer_description": "Early generation transmitters, companions, and direct students.", + "is_gap": false, + "is_uncertain": false, + "uncertain_transmitters": [], + "birth_year_hijri": null, + "death_year_hijri": "36", + "order": 1 + }, + { + "id": 867, + "name": "Al-Miqdad ibn al-Aswad", + "slug": "al-miqdad-ibn-al-aswad", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 12, + "title": "Unknown", + "slug": "unknown", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_id": 5699, + "narrator_layer_id": 5699, + "layer_level": 1, + "layer_name": "Primary Chain & Early Narrators", + "layer_description": "Early generation transmitters, companions, and direct students.", + "is_gap": true, + "is_uncertain": false, + "uncertain_transmitters": [], + "birth_year_hijri": null, + "death_year_hijri": "117", + "order": 2 + }, + { + "id": 1016, + "name": "Muhammad ibn Yahya al-Attar al-Qummi", + "slug": "muhammad-ibn-yahya-al-attar-al-qummi", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 58, + "title": "сподвижник", + "slug": "сподвижник", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_id": 5699, + "narrator_layer_id": 5699, + "layer_level": 1, + "layer_name": "Primary Chain & Early Narrators", + "layer_description": "Early generation transmitters, companions, and direct students.", + "is_gap": false, + "is_uncertain": true, + "uncertain_transmitters": [ + { + "id": 871, + "name": "Malik al-Ashtar al-Nakha'i", + "slug": "malik-al-ashtar-al-nakhai", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "birth_year_hijri": null, + "death_year_hijri": "161", + "reliability": { + "id": 7, + "title": "Very Reliable", + "slug": "very-reliable", + "color": "green", + "main_color_code": "#1DAC43" + } + }, + { + "id": 1016, + "name": "Muhammad ibn Yahya al-Attar al-Qummi", + "slug": "muhammad-ibn-yahya-al-attar-al-qummi", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "birth_year_hijri": "", + "death_year_hijri": "57", + "reliability": { + "id": 58, + "title": "сподвижник", + "slug": "сподвижник", + "color": "gray", + "main_color_code": "#374151" + } + } + ], + "birth_year_hijri": "", + "death_year_hijri": "57", + "order": 3 + }, + { + "id": 886, + "name": "Ali ibn Mahziyar al-Ahwazi", + "slug": "ali-ibn-mahziyar-al-ahwazi", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 16, + "title": "Weak", + "slug": "weak-1-190858898727", + "color": "orange", + "main_color_code": "#E67E22" + }, + "layer_id": 5699, + "narrator_layer_id": 5699, + "layer_level": 1, + "layer_name": "Primary Chain & Early Narrators", + "layer_description": "Early generation transmitters, companions, and direct students.", + "is_gap": false, + "is_uncertain": true, + "uncertain_transmitters": [ + { + "id": 884, + "name": "Abd Allah ibn Mughirah", + "slug": "abd-allah-ibn-mughirah", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "birth_year_hijri": null, + "death_year_hijri": "385", + "reliability": { + "id": 13, + "title": "Very Reliable", + "slug": "very-reliable-1-190858893632", + "color": "green", + "main_color_code": "#1DAC43" + } + }, + { + "id": 886, + "name": "Ali ibn Mahziyar al-Ahwazi", + "slug": "ali-ibn-mahziyar-al-ahwazi", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "birth_year_hijri": null, + "death_year_hijri": "405", + "reliability": { + "id": 16, + "title": "Weak", + "slug": "weak-1-190858898727", + "color": "orange", + "main_color_code": "#E67E22" + } + } + ], + "birth_year_hijri": null, + "death_year_hijri": "405", + "order": 4 + }, + { + "id": 1033, + "name": "Muhammad Hasan al-Shirazi", + "slug": "muhammad-hasan-al-shirazi", + "known_as": "زراره", + "nickname": "زراره شیبانی", + "reliability": { + "id": 7, + "title": "Very Reliable", + "slug": "very-reliable", + "color": "green", + "main_color_code": "#1DAC43" + }, + "layer_id": 5700, + "narrator_layer_id": 5700, + "layer_level": 2, + "layer_name": "Prominent Hadith Compilers", + "layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.", + "is_gap": false, + "is_uncertain": true, + "uncertain_transmitters": [ + { + "id": 1033, + "name": "Muhammad Hasan al-Shirazi", + "slug": "muhammad-hasan-al-shirazi", + "known_as": "زراره", + "nickname": "زراره شیبانی", + "birth_year_hijri": "80", + "death_year_hijri": "150", + "reliability": { + "id": 7, + "title": "Very Reliable", + "slug": "very-reliable", + "color": "green", + "main_color_code": "#1DAC43" + } + }, + { + "id": 853, + "name": "Muhammad ibn Ya'qub al-Kulayni", + "slug": "muhammad-ibn-yaqub-al-kulayni", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "birth_year_hijri": null, + "death_year_hijri": "35", + "reliability": { + "id": 15, + "title": "Acceptable", + "slug": "acceptable-1-190858896271", + "color": "yellow", + "main_color_code": "#EDC130" + } + } + ], + "birth_year_hijri": "80", + "death_year_hijri": "150", + "order": 5 + } + ] + }, + "explanations": [ + { + "title": "Vocabulary & Linguistic Analysis", + "detail": "The term 'Niyyah' (Intention) is pivotal in Islamic lexicon, derived from the root 'nawa'..." + }, + { + "title": "Context of Revelation (Asbab al-Wurud)", + "detail": "This tradition is famously known as the 'Hadith of the Migrant to Umm Qais'..." + } + ], + "corrections": [ + { + "id": 26, + "title": "Scholar Note #4", + "slug": "scholar-note-4", + "narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ", + "description": "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ\nالْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ...", + "translation": "Revised text segment for correction 4...", + "share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773/corrections/scholar-note-4", + "hadis_info": { + "id": 1891, + "number": 1, + "slug": "the-merit-of-seeking-knowledge" + }, + "address": [ + "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\"..." + ], + "images": [], + "references": [ + { + "id": 4034, + "book": null, + "author": null, + "edition": null, + "volume_info": null, + "hadith_number": null, + "volume": null, + "pages": null, + "address": "Tirmidhi, Abu Isa Muhammad ibn Isa...", + "url": null, + "open_book_url": null, + "citation_summary": "", + "images": [], + "images_count": 0, + "book_title": null, + "book_authors": [], + "edition_info": null + } + ], + "links": [ + { + "link": "google.com", + "title": "Первая ссылка" + } + ] + } + ], + "interpretations": [ + { + "id": 12, + "title": null, + "slug": "cat-l3-25-7f5bcb", + "narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ", + "text": "

الْمَسْأَلَةُ الثَّامِنَةُ : قَوْلُهُ : { أَهْلَ الْبَيْتِ }...

", + "translation": "

Вопрос восьмой: (что означают) слова: Ахл аль-бейт...

", + "category_info": { + "id": 1036, + "slug": "reflections", + "title": "Reflections" + }, + "address": [ + "Tirmidhi, Abu Isa Muhammad ibn Isa..." + ], + "images": [], + "links": [], + "references": [ + { + "id": 76, + "book_title": "last book", + "book_authors": [ + { + "id": 9, + "name": "sheykh hamid" + } + ], + "edition_info": null, + "volume_info": null, + "hadith_number": null, + "volume": "", + "pages": "", + "address": "Tirmidhi, Abu Isa Muhammad ibn Isa...", + "url": null, + "images": [] + } + ] + } + ] +} +``` diff --git a/apps/hadis/hadis_sample.json b/apps/hadis/hadis_sample.json new file mode 100644 index 0000000..43d6113 --- /dev/null +++ b/apps/hadis/hadis_sample.json @@ -0,0 +1,689 @@ +{ + "id": 1891, + "number": 1, + "slug": "the-merit-of-seeking-knowledge", + "category_id": 1036, + "category_slug": "reflections", + "title": "The Merit of Seeking Knowledge", + "title_narrator": "Jabir bin Abdullah", + "text": "اتَّقِ اللَّهَ حَيْثُمَا كُنْتَ، وَأَتْبِعِ السَّيِّئَةَ الْحَسَنَةَ تَمْحُهَا.", + "translation": [ + { + "title": "Передается от Пророка (мир ему): Правда ведет к благочестию. Это путь праведных.", + "language_code": "ru" + }, + { + "title": "The best among you are those who have the best manners and character.", + "language_code": "en" + } + ], + "detail": { + "address": [ + "Sahih al-Bukhari", + "Book of Revelation", + "Chapter 1: How Revelation Began" + ], + "address_details": [], + "hadis_status": { + "id": 133, + "title": "Authentic / Accepted", + "description": "A Sahih (Authentic) Hadith represents the highest level of reliability. It possesses a continuous chain of narrators (Isnad) made up of upright (Adl) and retentive (Dabit) transmitters, free from any hidden defects (Illah) or irregularities (Shudhudh). It is universally accepted for deriving Islamic laws.", + "color": "green", + "main_color_code": "#1DAC43" + }, + "status_text": null, + "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": [ + { + "id": 510, + "title": "Хадж" + }, + { + "id": 516, + "title": "Доверие" + }, + { + "id": 561, + "title": "Kindness" + }, + { + "id": 565, + "title": "Hellfire (Jahannam)" + }, + { + "id": 567, + "title": "Marriage (Nikah)" + } + ], + "references": [ + { + "id": 2538, + "title": "Man La Yahduruhu al-Faqih", + "authors": [ + { + "id": 9, + "name": "sheykh hamid" + } + ], + "description": [ + { + "title": "For him who is not in the presence of a jurisprudent - Shia collection.", + "language_code": "en" + }, + { + "title": "توضیحات درباره من لا یحضره الفقیه", + "language_code": "fa" + } + ], + "share_link": "https://dovodi.newhorizonco.uk/arguments/sources/man-la-yahduruhu-al-faqih" + } + ], + "reference_images": [ + { + "id": 2541, + "thumbnail": "https://testserver/media/hadis/reference_images/1891_ref1.png", + "priority": 1 + }, + { + "id": 2542, + "thumbnail": "https://testserver/media/hadis/reference_images/1891_ref2.png", + "priority": 2 + }, + { + "id": 2543, + "thumbnail": "https://testserver/media/hadis/reference_images/1891_ref3.png", + "priority": 3 + }, + { + "id": 2544, + "thumbnail": "https://testserver/media/hadis/reference_images/1891_ref4.png", + "priority": 4 + } + ] + }, + "narrators": { + "description": "Focuses on the relationship between the Creator and the creation.", + "transmitters": [ + { + "id": 850, + "name": "Ali ibn Yaqtin", + "slug": "ali-ibn-yaqtin", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 14, + "title": "Reliable", + "slug": "reliable-1-190858903395", + "color": "blue", + "main_color_code": "#5172E1" + }, + "layer_id": 5699, + "narrator_layer_id": 5699, + "layer_level": 1, + "layer_name": "Primary Chain & Early Narrators", + "layer_description": "Early generation transmitters, companions, and direct students.", + "is_gap": true, + "is_uncertain": false, + "uncertain_transmitters": [], + "birth_year_hijri": null, + "death_year_hijri": "40", + "order": 0 + }, + { + "id": 859, + "name": "Al-Muhaqqiq al-Hilli (Ja'far ibn al-Hasan)", + "slug": "al-muhaqqiq-al-hilli-jafar-ibn-al-hasan", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 17, + "title": "Very Weak", + "slug": "very-weak-1-190858905701", + "color": "red", + "main_color_code": "#D33A3A" + }, + "layer_id": 5699, + "narrator_layer_id": 5699, + "layer_level": 1, + "layer_name": "Primary Chain & Early Narrators", + "layer_description": "Early generation transmitters, companions, and direct students.", + "is_gap": false, + "is_uncertain": false, + "uncertain_transmitters": [], + "birth_year_hijri": null, + "death_year_hijri": "36", + "order": 1 + }, + { + "id": 867, + "name": "Al-Miqdad ibn al-Aswad", + "slug": "al-miqdad-ibn-al-aswad", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 12, + "title": "Unknown", + "slug": "unknown", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_id": 5699, + "narrator_layer_id": 5699, + "layer_level": 1, + "layer_name": "Primary Chain & Early Narrators", + "layer_description": "Early generation transmitters, companions, and direct students.", + "is_gap": true, + "is_uncertain": false, + "uncertain_transmitters": [], + "birth_year_hijri": null, + "death_year_hijri": "117", + "order": 2 + }, + { + "id": 1016, + "name": "Muhammad ibn Yahya al-Attar al-Qummi", + "slug": "muhammad-ibn-yahya-al-attar-al-qummi", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 58, + "title": "сподвижник", + "slug": "сподвижник", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_id": 5699, + "narrator_layer_id": 5699, + "layer_level": 1, + "layer_name": "Primary Chain & Early Narrators", + "layer_description": "Early generation transmitters, companions, and direct students.", + "is_gap": false, + "is_uncertain": true, + "uncertain_transmitters": [ + { + "id": 871, + "name": "Malik al-Ashtar al-Nakha'i", + "slug": "malik-al-ashtar-al-nakhai", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "birth_year_hijri": null, + "death_year_hijri": "161", + "reliability": { + "id": 7, + "title": "Very Reliable", + "slug": "very-reliable", + "color": "green", + "main_color_code": "#1DAC43" + } + }, + { + "id": 1016, + "name": "Muhammad ibn Yahya al-Attar al-Qummi", + "slug": "muhammad-ibn-yahya-al-attar-al-qummi", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "birth_year_hijri": "", + "death_year_hijri": "57", + "reliability": { + "id": 58, + "title": "сподвижник", + "slug": "сподвижник", + "color": "gray", + "main_color_code": "#374151" + } + } + ], + "birth_year_hijri": "", + "death_year_hijri": "57", + "order": 3 + }, + { + "id": 886, + "name": "Ali ibn Mahziyar al-Ahwazi", + "slug": "ali-ibn-mahziyar-al-ahwazi", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 16, + "title": "Weak", + "slug": "weak-1-190858898727", + "color": "orange", + "main_color_code": "#E67E22" + }, + "layer_id": 5699, + "narrator_layer_id": 5699, + "layer_level": 1, + "layer_name": "Primary Chain & Early Narrators", + "layer_description": "Early generation transmitters, companions, and direct students.", + "is_gap": false, + "is_uncertain": true, + "uncertain_transmitters": [ + { + "id": 884, + "name": "Abd Allah ibn Mughirah", + "slug": "abd-allah-ibn-mughirah", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "birth_year_hijri": null, + "death_year_hijri": "385", + "reliability": { + "id": 13, + "title": "Very Reliable", + "slug": "very-reliable-1-190858893632", + "color": "green", + "main_color_code": "#1DAC43" + } + }, + { + "id": 886, + "name": "Ali ibn Mahziyar al-Ahwazi", + "slug": "ali-ibn-mahziyar-al-ahwazi", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "birth_year_hijri": null, + "death_year_hijri": "405", + "reliability": { + "id": 16, + "title": "Weak", + "slug": "weak-1-190858898727", + "color": "orange", + "main_color_code": "#E67E22" + } + } + ], + "birth_year_hijri": null, + "death_year_hijri": "405", + "order": 4 + }, + { + "id": 1033, + "name": "Muhammad Hasan al-Shirazi", + "slug": "muhammad-hasan-al-shirazi", + "known_as": "زراره", + "nickname": "زراره شیبانی", + "reliability": { + "id": 7, + "title": "Very Reliable", + "slug": "very-reliable", + "color": "green", + "main_color_code": "#1DAC43" + }, + "layer_id": 5700, + "narrator_layer_id": 5700, + "layer_level": 2, + "layer_name": "Prominent Hadith Compilers", + "layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.", + "is_gap": false, + "is_uncertain": true, + "uncertain_transmitters": [ + { + "id": 1033, + "name": "Muhammad Hasan al-Shirazi", + "slug": "muhammad-hasan-al-shirazi", + "known_as": "زراره", + "nickname": "زراره شیبانی", + "birth_year_hijri": "80", + "death_year_hijri": "150", + "reliability": { + "id": 7, + "title": "Very Reliable", + "slug": "very-reliable", + "color": "green", + "main_color_code": "#1DAC43" + } + }, + { + "id": 853, + "name": "Muhammad ibn Ya'qub al-Kulayni", + "slug": "muhammad-ibn-yaqub-al-kulayni", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "birth_year_hijri": null, + "death_year_hijri": "35", + "reliability": { + "id": 15, + "title": "Acceptable", + "slug": "acceptable-1-190858896271", + "color": "yellow", + "main_color_code": "#EDC130" + } + } + ], + "birth_year_hijri": "80", + "death_year_hijri": "150", + "order": 5 + }, + { + "id": 1016, + "name": "Muhammad ibn Yahya al-Attar al-Qummi", + "slug": "muhammad-ibn-yahya-al-attar-al-qummi", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 58, + "title": "сподвижник", + "slug": "сподвижник", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_id": 5700, + "narrator_layer_id": 5700, + "layer_level": 2, + "layer_name": "Prominent Hadith Compilers", + "layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.", + "is_gap": true, + "is_uncertain": false, + "uncertain_transmitters": [], + "birth_year_hijri": "", + "death_year_hijri": "57", + "order": 6 + }, + { + "id": 856, + "name": "Ali ibn al-Husayn al-Sharif al-Murtada", + "slug": "ali-ibn-al-husayn-al-sharif-al-murtada", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 12, + "title": "Unknown", + "slug": "unknown", + "color": "gray", + "main_color_code": "#374151" + }, + "layer_id": 5700, + "narrator_layer_id": 5700, + "layer_level": 2, + "layer_name": "Prominent Hadith Compilers", + "layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.", + "is_gap": false, + "is_uncertain": false, + "uncertain_transmitters": [], + "birth_year_hijri": null, + "death_year_hijri": "37", + "order": 7 + }, + { + "id": 89, + "name": "Al-Fudayl ibn Yasar", + "slug": "al-fudayl-ibn-yasar", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 9, + "title": "Acceptable", + "slug": "acceptable", + "color": "yellow", + "main_color_code": "#EDC130" + }, + "layer_id": 5700, + "narrator_layer_id": 5700, + "layer_level": 2, + "layer_name": "Prominent Hadith Compilers", + "layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.", + "is_gap": false, + "is_uncertain": false, + "uncertain_transmitters": [], + "birth_year_hijri": null, + "death_year_hijri": "250", + "order": 8 + }, + { + "id": 863, + "name": "Al-Hurr al-Amili (Muhammad ibn al-Hasan)", + "slug": "al-hurr-al-amili-muhammad-ibn-al-hasan", + "known_as": "ibn Hilal", + "nickname": "Al-Basri", + "reliability": { + "id": 10, + "title": "Weak", + "slug": "weak", + "color": "orange", + "main_color_code": "#E67E22" + }, + "layer_id": 5700, + "narrator_layer_id": 5700, + "layer_level": 2, + "layer_name": "Prominent Hadith Compilers", + "layer_description": "Leading scholars, authors, and systematizers of prophetic narrations.", + "is_gap": true, + "is_uncertain": false, + "uncertain_transmitters": [], + "birth_year_hijri": null, + "death_year_hijri": "94", + "order": 9 + } + ] + }, + "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." + }, + { + "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": 26, + "title": "Scholar Note #4", + "slug": "scholar-note-4", + "narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ", + "description": "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ\nالْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ، وَالصَّلَاةُ وَالسَّلَامُ عَلَى سَيِّدِنَا مُحَمَّدٍ وَآلِهِ الطَّاهِرِينَ.\nاللَّهُمَّ زَيِّنَّا بِالْعِلْمِ وَالْحِلْمِ، وَاجْعَلْ أَلْسِنَتَنَا ذَاكِرَةً لَكَ وَقُلُوبَنَا خَاشِعَةً لَكَ.\nوَارْزُقْنَا صِدْقَ الْقَوْلِ وَحُسْنَ الْعَمَلِ، إِنَّكَ أَنْتَ الْوَهَّابُ الْكَرِيمُ.", + "translation": "Revised text segment for correction 4...", + "share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773/corrections/scholar-note-4", + "hadis_info": { + "id": 1891, + "number": 1, + "slug": "the-merit-of-seeking-knowledge" + }, + "address": [ + "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes." + ], + "images": [], + "references": [ + { + "id": 4034, + "book": null, + "author": null, + "edition": null, + "volume_info": null, + "hadith_number": null, + "volume": null, + "pages": null, + "address": "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes.", + "url": null, + "open_book_url": null, + "citation_summary": "", + "images": [], + "images_count": 0, + "book_title": null, + "book_authors": [], + "edition_info": null + } + ], + "links": [ + { + "link": "google.com", + "title": "Первая ссылка Первая ссылка ссылкассы..." + }, + { + "link": "google.com", + "title": "Первая ссылка." + }, + { + "link": "google.com", + "title": "Первая ссылка." + } + ] + }, + { + "id": 25, + "title": "Alternative Translation #3", + "slug": "alternative-translation-3", + "narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ", + "description": "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ\nالْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ، وَالصَّلَاةُ وَالسَّلَامُ عَلَى سَيِّدِنَا مُحَمَّدٍ وَآلِهِ الطَّاهِرِينَ.\nاللَّهُمَّ زَيِّنَّا بِالْعِلْمِ وَالْحِلْمِ، وَاجْعَلْ أَلْسِنَتَنَا ذَاكِرَةً لَكَ وَقُلُوبَنَا خَاشِعَةً لَكَ.\nوَارْزُقْنَا صِدْقَ الْقَوْلِ وَحُسْنَ الْعَمَلِ، إِنَّكَ أَنْتَ الْوَهَّابُ الْكَرِيمُ.", + "translation": "Revised text segment for correction 3...", + "share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773/corrections/alternative-translation-3", + "hadis_info": { + "id": 1891, + "number": 1, + "slug": "the-merit-of-seeking-knowledge" + }, + "address": [ + "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes." + ], + "images": [], + "references": [ + { + "id": 4035, + "book": null, + "author": null, + "edition": null, + "volume_info": null, + "hadith_number": null, + "volume": null, + "pages": null, + "address": "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes.", + "url": null, + "open_book_url": null, + "citation_summary": "", + "images": [], + "images_count": 0, + "book_title": null, + "book_authors": [], + "edition_info": null + } + ], + "links": [ + { + "link": "google.com", + "title": "Первая ссылка Первая ссылка ссылкассы..." + }, + { + "link": "google.com", + "title": "Первая ссылка." + }, + { + "link": "google.com", + "title": "Первая ссылка." + } + ] + }, + { + "id": 24, + "title": "Context Clarification #2", + "slug": "context-clarification-2", + "narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ", + "description": "بِسْمِ اللَّهِ الرَّحْمَنِ الرَّحِيمِ\r\nالْحَمْدُ لِلَّهِ رَبِّ الْعَالَمِينَ، وَالصَّلَاةُ وَالسَّلَامُ عَلَى سَيِّدِنَا مُحَمَّدٍ وَآلِهِ الطَّاهِرِينَ.\r\nاللَّهُمَّ زَيِّنَّا بِالْعِلْمِ وَالْحِلْمِ، وَاجْعَلْ أَلْسِنَتَنَا ذَاكِرَةً لَكَ وَقُلُوبَنَا خَاشِعَةً لَكَ.\r\nوَارْزُقْنَا صِدْقَ الْقَوْلِ وَحُسْنَ الْعَمَلِ، إِنَّكَ أَنْتَ الْوَهَّابُ الْكَرِيمُ.", + "translation": "Revised text segment for correction 2...", + "share_link": "https://dovodi.newhorizonco.uk/arguments/hadith/hadis-ru-1-7c2df773/corrections/context-clarification-2", + "hadis_info": { + "id": 1891, + "number": 1, + "slug": "the-merit-of-seeking-knowledge" + }, + "address": [ + "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes." + ], + "images": [ + { + "id": 85, + "image": "https://testserver/media/hadis/correction_images/image.png", + "thumbnail": "https://testserver/media/hadis/correction_images/image.png" + } + ], + "references": [ + { + "id": 4036, + "book": null, + "author": null, + "edition": null, + "volume_info": null, + "hadith_number": null, + "volume": null, + "pages": null, + "address": "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes.", + "url": null, + "open_book_url": null, + "citation_summary": "", + "images": [ + { + "id": 85, + "image": "https://testserver/media/hadis/correction_images/image.png", + "thumbnail": "https://testserver/media/hadis/correction_images/image.png", + "priority": 0 + } + ], + "images_count": 1, + "book_title": null, + "book_authors": [], + "edition_info": null + } + ], + "links": {} + } + ], + "interpretations": [ + { + "id": 12, + "title": null, + "slug": "cat-l3-25-7f5bcb", + "narrator": "زُرارَةُ بْنُ أَعْيَنَ الشَّيبانِيّ", + "text": "

الْمَسْأَلَةُ الثَّامِنَةُ : قَوْلُهُ : { أَهْلَ الْبَيْتِ }

\n\n

رُوِيَ عَنْ عُمَرَ بْنِ أَبِي سَلَمَةَ أَنَّهُ قَالَ : { لَمَّا نَزَلَتْ هَذِهِ الْآيَةُ عَلَى النَّبِيِّ صَلَّى اللَّهُ عَلَيْهِ وَسَلَّمَ : { إنَّمَا يُرِيدُ اللَّهُ لِيُذْهِبَ عَنْكُمْ الرِّجْسَ أَهْلَ الْبَيْتِ وَيُطَهِّرَكُمْ تَطْهِيرًا } فِي بَيْتِ أُمِّ سَلَمَةَ دَعَا النَّبِيُّ صَلَّى اللَّهُ عَلَيْهِ وَسَلَّمَ فَاطِمَةَ وَحَسَنًا وَحُسَيْنًا ، وَجَعَلَ عَلِيًّا خَلْفَ ظَهْرِهِ ، وَجَلَّلَهُمْ بِكِسَاءٍ ، ثُمَّ قَالَ : اللَّهُمَّ إنَّ هَؤُلَاءِ أَهْلَ بَيْتِي ، فَأَذْهِبْ عَنْهُمْ الرِّجْسَ وَطَهِّرْهُمْ تَطْهِيرًا . قَالَتْ أُمُّ سَلَمَةَ : وَأَنَا مَعَهُمْ يَا نَبِيَّ اللَّهِ . قَالَ : أَنْتِ عَلَى مَكَانِك وَأَنْتِ عَلَى خَيْرٍ } .

\n\n

وَرَوَى أَنَسُ بْنُ مَالِكٍ { أَنَّ رَسُولَ اللَّهِ صَلَّى اللَّهُ عَلَيْهِ وَسَلَّمَ كَانَ يَمُرُّ بِبَابِ فَاطِمَةَ سِتَّةَ أَشْهُرٍ إذَا خَرَجَ إلَى صَلَاةِ الْفَجْرِ يَقُولُ : الصَّلَاةَ يَا أَهْلَ الْبَيْتِ ، إنَّمَا يُرِيدُ اللَّهُ لِيُذْهِبَ عَنْكُمْ الرِّجْسَ أَهْلَ الْبَيْتِ وَيُطَهِّرَكُمْ تَطْهِيرًا } . خَرَّجَ هَذَيْنِ الْحَدِيثَيْنِ التِّرْمِذِيُّ وَغَيْرُهُ .

", + "translation": "

Вопрос восьмой: (что означают) слова: Ахл аль-бейт

\n\n

Передают от Умар ибн Аби Саламе, что когда был ниспослан аят «Аллах хочет удалить скверну от вас, о Люди дома (Ахл аль-бейт), и очистить вас наилучшим очищением» (Сура 33, аят 33) в доме Ум Саламы, Посланник Аллаха (с) позвал к себе Фатиму, Хасана и Хусейна, затем накрыл их своей накидкой, а Али был за Пророком, Пророк и его накрыл. После того, как Пророк накрыл их всех накидкой, он изволил сказать следующее: «О, Аллах, они люди моего дома, очисти же их наилучшим очищением!». Ум Салама говорит: «Я сказала: «О, Пророк Аллаха! Я тоже с ними?» Посланник Аллаха изволил сказать: «Ты оставайся при своем положении, и ты в добре».

\n\n

Передают от Анас Ибн Малика: Поистине посланник Аллах (мир ему и благословение Аллаха!) шесть месяцев проходил мимо двери Фатимы, когда выходил на утреннюю молитву, и говорил: время молитвы, о Ахл аль бейт! \"Аллах хочет удалить скверну от вас, о Люди дома (Ахл аль-бейт), и очистить вас наилучшим очищением\" (Сура 33, аят 33).

\n\n

Эти два хадиса привели Тирмизи и другие.

", + "category_info": { + "id": 1036, + "slug": "reflections", + "title": "Reflections" + }, + "address": [ + "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes." + ], + "images": [], + "links": [ + { + "link": "google.com", + "title": "Первая ссылка Первая ссылка ссылкассы..." + }, + { + "link": "google.com", + "title": "Первая ссылка." + }, + { + "link": "google.com", + "title": "Первая ссылка." + } + ], + "references": [ + { + "id": 76, + "book_title": "last book", + "book_authors": [ + { + "id": 9, + "name": "sheykh hamid" + } + ], + "edition_info": null, + "volume_info": null, + "hadith_number": null, + "volume": "", + "pages": "", + "address": "Tirmidhi, Abu Isa Muhammad ibn Isa (died 279 AH), \"Jami' al-Sahih (Sunan At-Tirmidhi)\", book: virtues, chapter: virtues of Ali ibn Abu Talib, volume 5, page 633, hadith 3713, research: Ahmad Muhammad Shakir and others, Mustafa al-Babi al-Halabi Publishing House, Egypt, 2nd edition, 1395 - 1975, in five volumes.", + "url": null, + "images": [] + } + ] + } + ] +} \ No newline at end of file diff --git a/apps/hadis/serializers/category.py b/apps/hadis/serializers/category.py index b1b09c3..10d5005 100644 --- a/apps/hadis/serializers/category.py +++ b/apps/hadis/serializers/category.py @@ -8,14 +8,14 @@ from django.utils.translation import get_language def _get_localized_item_value(item): """ - Prefer `title`, but fall back to `text` or `name` for older/mis-shaped localized entries. + Prefer `title`, but fall back to `text`, `name`, or `description` for localized entries. """ if not item: return None if isinstance(item, str): return item if isinstance(item, dict): - return item.get("title") or item.get("text") or item.get("name") + return item.get("title") or item.get("text") or item.get("name") or item.get("description") return str(item) def get_localized_text(json_list, request=None, fallback_lang="en", language_code=None): diff --git a/apps/hadis/serializers/hadis.py b/apps/hadis/serializers/hadis.py index 28ead78..9358968 100644 --- a/apps/hadis/serializers/hadis.py +++ b/apps/hadis/serializers/hadis.py @@ -1318,6 +1318,7 @@ class HadisShortSerializer(serializers.ModelSerializer): class HadisDetailSerializer(serializers.ModelSerializer): """Detailed serializer for Hadis with core information (transmitters and corrections moved to separate endpoints)""" hadis_status = HadisStatusSerializer(read_only=True) + status_text = LocalizedField(source='hadis_status_text') tags = HadisTagSerializer(many=True, read_only=True) references = HadisReferenceSerializer( many=True, @@ -1326,13 +1327,12 @@ class HadisDetailSerializer(serializers.ModelSerializer): reference_images = SerializerMethodField() address = LocalizedField() category_slug = serializers.CharField(source='category.slug', read_only=True) - # address_details = serializers.SerializerMethodField() class Meta: model = Hadis fields = [ - 'id', 'number', 'slug', 'category_id', 'category_slug', 'hadis_status','links','share_link', - 'tags','references','reference_images','address' + 'id', 'number', 'slug', 'category_id', 'category_slug', 'hadis_status', 'status_text', 'links', 'share_link', + 'tags', 'references', 'reference_images', 'address' ] # def get_address_details(self, obj): diff --git a/apps/hadis/views/hadis.py b/apps/hadis/views/hadis.py index f0f9781..61f70bc 100644 --- a/apps/hadis/views/hadis.py +++ b/apps/hadis/views/hadis.py @@ -1,1282 +1,1290 @@ -from rest_framework.authentication import TokenAuthentication -from rest_framework.permissions import IsAuthenticated, AllowAny -from rest_framework.generics import ListAPIView, RetrieveAPIView -from drf_yasg.utils import swagger_auto_schema -from drf_yasg import openapi -from django.shortcuts import get_object_or_404 -from utils.pagination import NoPagination, StandardResultsSetPagination -from rest_framework.pagination import PageNumberPagination -from rest_framework.response import Response -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, CorrectionReference, HadisInterpretation, InterpretationReference -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 - -from rest_framework import status -from utils.mixins import CanonicalSlugViewSetMixin, Gone410ViewMixin - -class PinnedHadisCollectionListView(ListAPIView): - """ - API view to list pinned hadis collections with metadata info - """ - serializer_class = PinnedHadisCollectionSerializer - pagination_class = NoPagination - permission_classes = [IsAuthenticated] - authentication_classes = [TokenAuthentication] - - @hadis_collections_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - from django.db.models import Prefetch - from apps.hadis.models import HadisInCollection - return HadisCollection.objects.filter( - status=True, - display_position=HadisCollection.DisplayPosition.PINNED, - hadis_items__hadis__status=True - ).prefetch_related( - Prefetch( - 'hadis_items', - queryset=HadisInCollection.objects.filter(hadis__status=True).order_by('order') - ) - ).distinct().order_by('order', '-created_at') - - def list(self, request, *args, **kwargs): - # 1. گرفتن خروجی استاندارد لیست مجموعه‌ها - response = super().list(request, *args, **kwargs) - - # 2. محاسبه آمار (Info) - # تعداد کتگوری‌های فعال - categories_count = HadisCategory.objects.count() - - # تعداد نریتورها (راویان) - narrators_count = Transmitters.objects.count() - - # تعداد سورس‌ها (کتاب‌های منبع) - # فرض بر این است که BookReference مدل منابع شماست - from apps.hadis.models import BookReference - sources_count = BookReference.objects.count() - - # تعداد بوکمارک‌های کاربر در سرویس حدیث - from apps.bookmark.models.bookmark import Bookmark - bookmarks_count = Bookmark.objects.filter( - user=request.user, - service=Bookmark.ServiceChoices.HADITH, - status=True - ).count() - - # تعداد نویسنده‌ها - from apps.hadis.models import BookAuthor - authors_count = BookAuthor.objects.count() - - # 3. ساختن دیکشنری info - info = { - "categories_count": categories_count, - # "bookmarks_count": bookmarks_count, - "narrators_count": narrators_count, - "sources_count": sources_count, - "authors_count": authors_count, - } - - # 4. بازسازی دیتای نهایی مشابه اپ ویدیو - custom_data = { - "count": len(response.data) if isinstance(response.data, list) else response.data.get("count"), - "next": response.data.get("next") if isinstance(response.data, dict) else None, - "previous": response.data.get("previous") if isinstance(response.data, dict) else None, - "info": info, - "results": response.data if isinstance(response.data, list) else response.data.get("results") - } - - return Response(custom_data, status=status.HTTP_200_OK) - - -class HadisCollectionListView(ListAPIView): - """ - API view to list all hadis collections - """ - serializer_class = HadisCollectionListSerializer - pagination_class = NoPagination - - @hadis_collections_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - from django.db.models import Prefetch - from apps.hadis.models import HadisInCollection - return HadisCollection.objects.filter( - status=True, - display_position=HadisCollection.DisplayPosition.MIDDLE - ).prefetch_related( - Prefetch( - 'hadis_items', - queryset=HadisInCollection.objects.filter(hadis__status=True).order_by('order') - ) - ).order_by('order', '-created_at') - - -class HadisSyncView(ListAPIView): - """ - API view to sync all hadis data for offline mode - """ - serializer_class = HadisSyncSerializer - pagination_class = NoPagination - - def get_queryset(self): - qs = ( - Hadis.objects - .filter(status=True) - .select_related('category', 'hadis_status') - .prefetch_related( - 'tags', - Prefetch( - 'transmitters', - queryset=HadisTransmitter.objects.select_related( - 'transmitter', - 'transmitter__reliability', - 'narrator_layer' - ).prefetch_related( - 'uncertain_transmitters', - 'uncertain_transmitters__reliability' - ).order_by('order') - ), - Prefetch( - 'references', - queryset=HadisReference.objects - .select_related('book_reference') - .prefetch_related( - 'book_reference__author', - Prefetch( - 'images', - queryset=ReferenceImage.objects.order_by('priority') - ) - ) - ), - # 👇 اضافه شدن واکشی عمیق برای تصحیحات (بدون N+1) - Prefetch( - 'hadiscorrection_set', - queryset=HadisCorrection.objects.select_related('hadis').prefetch_related( - Prefetch( - 'references', - queryset=CorrectionReference.objects.select_related( - 'book_reference', - 'book_reference__author', - 'edition', - 'book_volume', - 'book_volume__edition' - ).prefetch_related( - 'images', - 'book_reference__editions', - 'book_reference__volumes' - ) - ) - ) - ), - # 👇 اضافه شدن واکشی عمیق برای تفاسیر (بدون N+1) - Prefetch( - 'category__interpretations', - queryset=HadisInterpretation.objects.select_related('category').prefetch_related( - Prefetch( - 'references', - queryset=InterpretationReference.objects.select_related( - 'book_reference', - 'book_reference__author', - 'edition', - 'book_volume', - 'book_volume__edition' - ).prefetch_related( - 'images', - 'book_reference__editions', - 'book_reference__volumes' - ) - ) - ) - ), - ) - .order_by('id') - ) - - # Incremental sync support to prevent transferring 25MB on subsequent syncs - req = getattr(self, 'request', None) - updated_after = (req.query_params.get('updated_after') or req.query_params.get('since')) if req and hasattr(req, 'query_params') else None - if updated_after: - from django.utils.dateparse import parse_datetime - try: - dt = parse_datetime(updated_after) - if dt: - qs = qs.filter(updated_at__gte=dt) - except Exception: - pass - - return qs - - @hadis_sync_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def list(self, request, *args, **kwargs): - queryset = self.get_queryset() - serializer = self.get_serializer( - queryset, - many=True, - context={'request': request}, - ) - return Response({ - 'count': len(serializer.data), - 'results': serializer.data - }) - -from ..serializers.category import CategoryQuranVerseSerializer -class HadisListView(ListAPIView): - """ - API view to list Hadis by category_id - """ - serializer_class = HadisListSerializer - pagination_class = StandardResultsSetPagination - authentication_classes = [TokenAuthentication] - - @hadis_list_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def list(self, request, *args, **kwargs): - # 1. Run the standard list logic (get pagination, filter, results) - response = super().list(request, *args, **kwargs) - - # 2. Find the "Parent" Category based on the URL slug - category_slug = self.kwargs.get('category_slug') - category_obj = get_object_or_404(HadisCategory, slug=category_slug) - - # 3. Serialize this single category for the Hero section - category_data = SimpleCategory(category_obj, context={'request': request}).data - - # 4. Check if it's a Quran Category and fetch verse data - quran_data = None - if category_obj.source_type == HadisCategory.SourceType.QURAN: - try: - from ..models.category import CategoryQuranVerse - verse_obj = CategoryQuranVerse.objects.prefetch_related( - 'secondary_translations' - ).get(category=category_obj) - - from ..serializers.category import CategoryQuranVerseSerializer - quran_data = CategoryQuranVerseSerializer( - verse_obj, - context={'request': request} - ).data - except CategoryQuranVerse.DoesNotExist: - pass - - # 👇 5. NEW LOGIC: Fetch all statuses for frontend filtering UI - lang = getattr(request, "LANGUAGE_CODE", None) or "fa" - statuses_list = [ - { - "id": status.id, - "slug": status.slug, - "title": get_localized_text(status.title, request=request, language_code=lang) or "", - "color": status.color, - "main_color_code": status.main_color_code, - } - for status in HadisStatus.objects.all().order_by('order') - ] - - # 6. Inject it into the response data - if isinstance(response.data, dict): - ordered_data = {} - for key in ['count', 'next', 'previous']: - if key in response.data: - ordered_data[key] = response.data[key] - - ordered_data['current_category'] = category_data - ordered_data['quran_data'] = quran_data - - # 👇 تزریق لیست استاتوس‌ها قبل از نتایج - ordered_data['statuses'] = statuses_list - - if 'results' in response.data: - ordered_data['results'] = response.data['results'] - response.data = ordered_data - - return response - - def get_queryset(self): - category_slug = self.kwargs.get('category_slug') - if not HadisCategory.objects.filter(slug=category_slug).exists(): - return Hadis.objects.none() - - queryset = Hadis.objects.filter( - category__slug=category_slug, - status=True - ).annotate( - layer_count=Count('transmitters__narrator_layer', distinct=True) - ).select_related('category').prefetch_related( - Prefetch( - 'references', - queryset=HadisReference.objects.prefetch_related( - Prefetch('images', queryset=ReferenceImage.objects.order_by('priority')) - ) - ) - ) - - # 👇 1. Apply Search Filter - search_query = self.request.query_params.get('search', None) - if search_query: - search_conditions = Q(text__icontains=search_query) - search_conditions |= Q(title__icontains=search_query) - search_conditions |= Q(title_narrator__icontains=search_query) - search_conditions |= Q(translation__icontains=search_query) - queryset = queryset.filter(search_conditions) - - # 👇 2. Apply Status Filter (Supports multiple comma-separated slugs) - status_filter = self.request.query_params.get('status', None) - if status_filter: - status_slugs = [s.strip() for s in status_filter.split(',')] - queryset = queryset.filter(hadis_status__slug__in=status_slugs) - - # 👇 3. Apply Transmitter Filter (Supports multiple comma-separated slugs) - transmitter_filter = self.request.query_params.get('transmitter', None) - if transmitter_filter: - transmitter_slugs = [t.strip() for t in transmitter_filter.split(',')] - queryset = queryset.filter(transmitters__transmitter__slug__in=transmitter_slugs) - - # 👇 4. Apply Source Filter (Supports multiple comma-separated slugs) - source_filter = self.request.query_params.get('source', None) - if source_filter: - source_slugs = [s.strip() for s in source_filter.split(',')] - queryset = queryset.filter(references__book_reference__slug__in=source_slugs) - - # 👇 5. Apply Source Author Filter (Supports author or source_author, multiple comma-separated slugs/IDs) - author_filter = self.request.query_params.get('author', None) or self.request.query_params.get('source_author', None) - if author_filter: - author_slugs = [a.strip() for a in author_filter.split(',') if a.strip()] - author_q = Q(references__book_reference__author__slug__in=author_slugs) - numeric_ids = [int(a) for a in author_slugs if a.isdigit()] - if numeric_ids: - author_q |= Q(references__book_reference__author_id__in=numeric_ids) - queryset = queryset.filter(author_q) - - # Filter by bookmarks if provided - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true' and self.request.user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - bookmarked_ids = Bookmark.objects.filter( - user=self.request.user, - service=Bookmark.ServiceChoices.HADITH, - status=True - ).values_list('content_id', flat=True) - queryset = queryset.filter(id__in=bookmarked_ids) - - # ❗️ distinct() برای جلوگیری از دابلیکیت شدن احادیث به خاطر ManyToMany ها الزامی است - return queryset.distinct() - - def get_serializer_context(self): - """Add user bookmarks to serializer context to avoid caching issues""" - context = super().get_serializer_context() - - # Add user's bookmarked hadis IDs to context - user = self.request.user - if user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - user_bookmarks = Bookmark.objects.filter( - user=user, - service=Bookmark.ServiceChoices.HADITH, - status=True - ).values_list('content_id', flat=True) - context['user_bookmarked_hadis_ids'] = set(user_bookmarks) - else: - context['user_bookmarked_hadis_ids'] = set() - - return context - - - - -class HadisMainListView(ListAPIView): - """ - API view to list Hadis by category_id - """ - serializer_class = HadisListSerializer - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] - pagination_class = StandardResultsSetPagination - - @hadis_main_list_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - queryset = Hadis.objects.select_related('category__sect', 'hadis_status').prefetch_related( - Prefetch( - 'references', - queryset=HadisReference.objects.select_related('book_reference__author').prefetch_related( - Prefetch('images', queryset=ReferenceImage.objects.order_by('priority')) - ) - ) - ) - - # Get search parameters - search_query = self.request.query_params.get('search', None) - status_filter = self.request.query_params.get('status', None) - category_filter = self.request.query_params.get('category', None) - - # Apply search filter - if search_query: - queryset = self.apply_search_filter(queryset, search_query) - - # Apply status filter - if status_filter: - queryset = queryset.filter(hadis_status__title__icontains=status_filter) - - # Apply category filter - if category_filter: - queryset = queryset.filter(category__title__icontains=category_filter) - - # Filter by bookmarks if provided - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true' and self.request.user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - bookmarked_ids = Bookmark.objects.filter( - user=self.request.user, - service=Bookmark.ServiceChoices.HADITH, - status=True - ).values_list('content_id', flat=True) - queryset = queryset.filter(id__in=bookmarked_ids) - - return queryset - - def list(self, request, *args, **kwargs): - queryset = self.get_queryset() - - # Apply pagination - page = self.paginate_queryset(queryset) - if page is not None: - serializer = self.get_serializer(page, many=True) - paginated_response = self.get_paginated_response(serializer.data) - - # Get category titles - category_titles = self.get_category_titles(request) - - # Get status titles - status_titles = self.get_status_titles(request) - - # Modify the paginated response to include our custom data - response_data = paginated_response.data - # response_data['category_titles'] = self.get_cached_category_titles(request) - # response_data['status_titles'] = self.get_cached_status_titles(request) - - return Response(response_data) - - # Fallback for when pagination is disabled - serializer = self.get_serializer(queryset, many=True) - return Response({ - 'count': queryset.count(), - 'results': serializer.data - }) - - return Response(response_data) - - def get_category_titles(self,request): - """Get list of category titles based on language""" - from ..models import HadisCategory - - categories = HadisCategory.objects.all() - category_titles = [] - - for category in categories: - title = get_localized_text(category.title,request) - category_titles.append(title) - - return category_titles - - def get_status_titles(self, request): - """Get list of status titles based on language""" - from ..models import HadisStatus - - statuses = HadisStatus.objects.all().order_by('order') - status_titles = [] - - for status in statuses: - title = get_localized_text(status.title,request) - status_titles.append(title) - return status_titles - - def apply_search_filter(self, queryset, search_query): - """ - Apply search filter across multiple fields including JSONFields. - Searches in: title, title_narrator, text, translation - """ - from django.db.models import Q - - # Basic search conditions - search_conditions = Q(text__icontains=search_query) - - # For JSONFields, search in the JSON string representation - # This will find matches in the "text" values within the JSON arrays - search_conditions |= Q(title__icontains=search_query) - search_conditions |= Q(title_narrator__icontains=search_query) - search_conditions |= Q(translation__icontains=search_query) - - return queryset.filter(search_conditions) - - #we add this later - - # def get_cached_category_titles(self, request): - # """Fetches categories, cached for 1 hour to reduce DB load""" - # lang = getattr(request, "LANGUAGE_CODE", "en") - # cache_key = f"hadis_meta_categories_{lang}" - - # data = cache.get(cache_key) - # if not data: - # # If not in cache, fetch from DB - # from ..models import HadisCategory - # categories = HadisCategory.objects.all().only('id', 'title') # Fetch only needed fields - - # # Build list - # data = [get_localized_text(c.title, request) for c in categories] - - # # Save to cache - # cache.set(cache_key, data, timeout=60 * 60) # 1 Hour - - # return data - - # def get_cached_status_titles(self, request): - # """Fetches statuses, cached for 1 hour""" - # lang = getattr(request, "LANGUAGE_CODE", "en") - # cache_key = f"hadis_meta_statuses_{lang}" - - # data = cache.get(cache_key) - # if not data: - # from ..models import HadisStatus - # statuses = HadisStatus.objects.all().order_by('order') - # data = [get_localized_text(s.title, request) for s in statuses] - # cache.set(cache_key, data, timeout=60 * 60) - - # return data - - -class HadisBasicView(RetrieveAPIView): - """ - API view to retrieve basic Hadis information by hadis_slug - """ - serializer_class = HadisBasicSerializer - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - authentication_classes = [TokenAuthentication] - - @hadis_basic_swagger - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - - def get_queryset(self): - return Hadis.objects.filter(status=True) - - def get_serializer_context(self): - """Add user bookmarks to serializer context to avoid caching issues""" - context = super().get_serializer_context() - - # Add user's bookmarked hadis IDs to context - user = self.request.user - if user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - user_bookmarks = Bookmark.objects.filter( - user=user, - service=Bookmark.ServiceChoices.HADITH, - status=True - ).values_list('content_id', flat=True) - context['user_bookmarked_hadis_ids'] = set(user_bookmarks) - else: - context['user_bookmarked_hadis_ids'] = set() - - return context - - -class HadisDetailView(Gone410ViewMixin, CanonicalSlugViewSetMixin, RetrieveAPIView): - """ - API view to retrieve detailed Hadis information by hadis_slug (excluding transmitters and corrections) - """ - serializer_class = HadisDetailSerializer - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - - active_field_name = 'status' - active_expected_value = True - - @hadis_detail_swagger - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - - def get_queryset(self): - return Hadis.objects.all().select_related( - 'category', 'hadis_status' - ).prefetch_related( - 'tags', - 'references__book_reference', - 'references__book_reference__author', - 'references__images', - ) - -class HadisTransmittersView(RetrieveAPIView): - """ - Fetches a single Hadis but filters the nested Transmitters list - if a ?layer=slug param is provided. - """ - serializer_class = HadisTransmitterListSerializer - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - pagination_class = NoPagination - @hadis_transmitters_swagger - def get(self, request, *args, **kwargs): - return self.retrieve(request, *args, **kwargs) - - def get_queryset(self): - # 1. Get the filter param - layer_slug = self.request.query_params.get('layer') - - # 2. Build the query for the "Child" (Transmitters) - # We start with the base optimization (select_related) - transmitter_qs = HadisTransmitter.objects.select_related( - 'transmitter', - 'narrator_layer', - 'status' - ).prefetch_related('uncertain_transmitters').order_by('order') - - # 3. Apply the filter to the Child Query (if param exists) - if layer_slug: - # Assumes 'NarratorLayer' has a 'slug' field. - # If not, use 'narrator_layer__name' or 'narrator_layer__id'. - transmitter_qs = transmitter_qs.filter(narrator_layer__slug=layer_slug) - - # 4. Use the Prefetch object to inject this filtered list into the Parent - return Hadis.objects.filter(status=True).prefetch_related( - Prefetch('transmitters', queryset=transmitter_qs) - ) - -class HadisCorrectionsView(ListAPIView): - """ - API view to retrieve corrections for a specific hadis - """ - serializer_class = HadisCorrectionSerializer - authentication_classes = [TokenAuthentication] - permission_classes = [IsAuthenticated] - pagination_class = StandardResultsSetPagination - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - - @hadis_corrections_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - # hadis = self.get_object() - # corrections_data = [] - - # for correction in hadis.hadiscorrection_set.all(): - # correction_info = { - # 'id': correction.id, - # 'title': correction.title, - # 'description': correction.description, - # 'translation': correction.translation - # } - # corrections_data.append(correction_info) - - # return Response({ - # 'hadis_id': hadis.id, - # 'corrections_count': len(corrections_data), - # 'corrections': corrections_data - # }) - - def get_queryset(self): - hadis_slug = self.kwargs.get('hadis_slug') - try: - hadis = Hadis.objects.get(slug=hadis_slug, status=True) - if not HadisCorrection.objects.filter(hadis=hadis).exists(): - return Hadis.objects.none() - queryset = HadisCorrection.objects.filter(hadis=hadis) - - # Filter by bookmarks if provided - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true' and self.request.user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - bookmarked_ids = Bookmark.objects.filter( - user=self.request.user, - service=Bookmark.ServiceChoices.HADITH_CORRECTION, - status=True - ).values_list('content_id', flat=True) - queryset = queryset.filter(id__in=bookmarked_ids) - - return queryset - except Hadis.DoesNotExist: - return HadisCorrection.objects.none() - - - -class CategoryHadisCorrectionsView(ListAPIView): - """ - API view to retrieve all corrections across all hadiths belonging to a specific category - """ - serializer_class = HadisCorrectionSerializer - authentication_classes = [TokenAuthentication] - permission_classes = [AllowAny] - pagination_class = StandardResultsSetPagination - - @swagger_auto_schema( - operation_summary="Get Category Hadis Corrections", - operation_description="Returns all text corrections across all hadiths belonging to a specific category, including hadis_info for each correction. Supports filtering by multiple hadiths and/or categories.", - tags=['Dobodbi - Hadis (V2)'], - manual_parameters=[ - openapi.Parameter('search', openapi.IN_QUERY, description="Search in text or narrator", type=openapi.TYPE_STRING), - openapi.Parameter('hadis_slug', openapi.IN_QUERY, description="Filter corrections by specific hadith slug (comma-separated for multiple)", type=openapi.TYPE_STRING), - openapi.Parameter('hadis_slugs', openapi.IN_QUERY, description="Filter corrections by multiple hadith slugs (comma-separated)", type=openapi.TYPE_STRING), - openapi.Parameter('category_slugs', openapi.IN_QUERY, description="Filter corrections by multiple category slugs (comma-separated)", type=openapi.TYPE_STRING), - openapi.Parameter('is_bookmark', openapi.IN_QUERY, description="Filter only bookmarked corrections (requires auth)", type=openapi.TYPE_BOOLEAN), - ] - ) - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - category_slug = self.kwargs.get('category_slug') - try: - category_ids = [] - if category_slug: - category = HadisCategory.objects.get(slug=category_slug) - category_ids = list(category.get_descendants(include_self=True).values_list('id', flat=True)) - - # Optional filter by multiple categories (selected subcategories or category slugs/IDs) - cat_param_keys = ['category_slugs', 'categories', 'category_slug'] - cat_slug_list = [] - for key in cat_param_keys: - vals = self.request.query_params.getlist(key) if hasattr(self.request.query_params, 'getlist') else [] - if not vals and key in self.request.query_params: - val = self.request.query_params.get(key) - if val: - vals = [val] if isinstance(val, str) else list(val) - for v in vals: - if isinstance(v, str): - cat_slug_list.extend([s.strip() for s in v.split(',') if s.strip()]) - elif isinstance(v, (list, tuple)): - cat_slug_list.extend(v) - elif v is not None: - cat_slug_list.append(str(v)) - - if cat_slug_list: - cat_filter = Q(slug__in=cat_slug_list) - id_list = [int(s) for s in cat_slug_list if str(s).isdigit()] - if id_list: - cat_filter |= Q(id__in=id_list) - - selected_categories = HadisCategory.objects.filter(cat_filter) - selected_cat_ids = set() - for sc in selected_categories: - selected_cat_ids.update(sc.get_descendants(include_self=True).values_list('id', flat=True)) - - if selected_cat_ids: - if category_ids: - category_ids = list(set(category_ids).intersection(selected_cat_ids)) - else: - category_ids = list(selected_cat_ids) - else: - return HadisCorrection.objects.none() - - hadis_filter_kwargs = {'status': True} - if category_ids: - hadis_filter_kwargs['category_id__in'] = category_ids - - hadis_ids = Hadis.objects.filter(**hadis_filter_kwargs).values_list('id', flat=True) - - queryset = HadisCorrection.objects.filter( - hadis_id__in=hadis_ids - ).select_related('hadis').prefetch_related( - 'references', - 'references__images' - ).order_by('priority', '-created_at') - - # Optional search query filter - search_query = self.request.query_params.get('search', None) - if search_query: - search_conditions = ( - Q(title__icontains=search_query) | - Q(text__icontains=search_query) | - Q(narrator__icontains=search_query) | - Q(translation__icontains=search_query) | - Q(hadis__title__icontains=search_query) | - Q(hadis__title_narrator__icontains=search_query) - ) - queryset = queryset.filter(search_conditions) - - # Optional filter by specific hadith slug(s) or ID(s) - hadis_param_keys = ['hadis_slug', 'hadis_slugs', 'hadiths'] - hadis_slug_list = [] - for key in hadis_param_keys: - vals = self.request.query_params.getlist(key) if hasattr(self.request.query_params, 'getlist') else [] - if not vals and key in self.request.query_params: - val = self.request.query_params.get(key) - if val: - vals = [val] if isinstance(val, str) else list(val) - for v in vals: - if isinstance(v, str): - hadis_slug_list.extend([s.strip() for s in v.split(',') if s.strip()]) - elif isinstance(v, (list, tuple)): - hadis_slug_list.extend(v) - elif v is not None: - hadis_slug_list.append(str(v)) - - if hadis_slug_list: - hadis_query = Q(hadis__slug__in=hadis_slug_list) - id_list = [int(s) for s in hadis_slug_list if str(s).isdigit()] - if id_list: - hadis_query |= Q(hadis__id__in=id_list) - - queryset = queryset.filter(hadis_query) - - # Filter by bookmarks if provided - is_bookmark = self.request.query_params.get('is_bookmark', '').lower() - if is_bookmark == 'true' and self.request.user.is_authenticated: - from apps.bookmark.models.bookmark import Bookmark - bookmarked_ids = Bookmark.objects.filter( - user=self.request.user, - service=Bookmark.ServiceChoices.HADITH_CORRECTION, - status=True - ).values_list('content_id', flat=True) - queryset = queryset.filter(id__in=bookmarked_ids) - - return queryset - except HadisCategory.DoesNotExist: - return HadisCorrection.objects.none() - - def list(self, request, *args, **kwargs): - response = super().list(request, *args, **kwargs) - category_slug = self.kwargs.get('category_slug') - category_obj = HadisCategory.objects.filter(slug=category_slug).first() if category_slug else None - category_data = SimpleCategory(category_obj, context={'request': request}).data if category_obj else None - - available_hadiths = [] - available_categories = [] - if category_obj: - cat_ids = category_obj.get_descendants(include_self=True).values_list('id', flat=True) - hadiths_in_cat = Hadis.objects.filter( - category_id__in=cat_ids, - status=True - ).order_by('number', 'id') - - for h in hadiths_in_cat: - h_title = get_localized_text(h.title, request) if hasattr(h, 'title') and h.title else f"Hadith {h.number}" - available_hadiths.append({ - "id": h.id, - "number": h.number, - "slug": h.slug, - "title": h_title, - }) - - subcats = category_obj.get_children() if hasattr(category_obj, 'get_children') else category_obj.children.all() - for sc in subcats: - sc_title = get_localized_text(sc.title, request) if hasattr(sc, 'title') and sc.title else sc.slug - available_categories.append({ - "id": sc.id, - "slug": sc.slug, - "title": sc_title, - }) - - if isinstance(response.data, dict): - ordered_data = { - 'count': response.data.get('count', 0), - 'next': response.data.get('next'), - 'previous': response.data.get('previous'), - 'current_category': category_data, - 'available_hadiths': available_hadiths, - 'available_categories': available_categories, - 'results': response.data.get('results', []), - } - response.data = ordered_data - - return response - - - -class HadisLayersView(ListAPIView): - """ - API view to retrieve all narrator layers for a specific hadis - """ - serializer_class = NarratorLayerSerializer - pagination_class = NoPagination - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - - @hadis_layers_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - from ..models import NarratorLayer - - hadis_slug = self.kwargs.get('hadis_slug') - - # Get the hadis object to ensure it exists - hadis = get_object_or_404(Hadis, slug=hadis_slug, status=True) - - # Return the layer objects for this hadis ordered by number - return NarratorLayer.objects.filter(hadis=hadis).order_by('number') - - -class HadisFiltersView(ListAPIView): - """ - API view to return filter data for hadis - Returns statuses and categories for filtering - """ - pagination_class = NoPagination - - @arguments_filters_swagger - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - # This view doesn't need a queryset, it returns computed data - return Hadis.objects.none() - - def list(self, request, *args, **kwargs): - # Get statuses from HadisStatus model - statuses = [] - for status in HadisStatus.objects.all().order_by('order'): - title_text = get_localized_text(status.title, request) - if title_text and status.slug: - statuses.append({ - 'text': title_text, - 'slug': status.slug - }) - - # Get categories from HadisCategory model (excluding empty leaf categories) - from django.db.models import Count, Q - categories = [] - for category in HadisCategory.objects.annotate( - active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), - children_total_count=Count('children') - ).filter( - Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) - ).order_by('order'): - title_text = get_localized_text(category.title, request) - if title_text and category.slug: - categories.append({ - 'text': title_text, - 'slug': category.slug - }) - - response_data = { - 'statuses': statuses, - 'categories': categories - } - - return Response(response_data) - -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework.permissions import AllowAny -from rest_framework.authentication import TokenAuthentication -from drf_yasg.utils import swagger_auto_schema -from django.utils.translation import get_language - -from apps.hadis.models import HadisStatus, HadisCategory, BookReference, Transmitters -from drf_yasg import openapi -class HadisFiltersSyncAPIView(APIView): - """ - API view to return all filter data for hadis (Sync API for Mobile App) - Returns statuses, categories, sources (books), and narrators. - Optimized for offline client-side searching and sorting. - """ - permission_classes = [AllowAny] - authentication_classes = [TokenAuthentication] - - @swagger_auto_schema( - operation_description=( - "Sync API for Flutter/mobile clients: returns flat hadis filter data for offline " - "searching and local filtering. Titles are localized from multilingual fields based " - "on the active request language." - ), - operation_summary="Sync Hadis Filters", - tags=['Dobodbi - Hadis'], - manual_parameters=[ - openapi.Parameter( - 'Accept-Language', - openapi.IN_HEADER, - description="Language code for localized titles, for example `en`, `fa`, `ar`, `ur`, or `ru`.", - type=openapi.TYPE_STRING, - required=False, - ) - ], - responses={ - status.HTTP_200_OK: openapi.Response( - description="Flat filter payload for hadis categories, statuses, sources, and narrators.", - examples={ - "application/json": { - "statuses": [ - { - "id": 133, - "slug": "sahih", - "title": "Authentic", - "order": 1, - "color": "green", - "main_color_code": "#1DAC43" - } - ], - "categories": [ - { - "id": 1036, - "slug": "general-hadiths", - "title": "General Hadiths", - "order": 1 - } - ], - "sources": [ - { - "id": 2538, - "slug": "man-la-yahduruhu-al-faqih", - "title": "Man La Yahduruhu al-Faqih", - "search_keywords": "Shaykh al-Saduq" - } - ], - "narrators": [ - { - "id": 501, - "slug": "muhammad-ibn-muslim", - "title": "Muhammad ibn Muslim", - "search_aliases": "Abu Ja'far" - } - ] - } - } - ) - } - ) - def get(self, request, *args, **kwargs): - # تشخیص زبان برای ارسال دیتای لوکالایز شده - lang = getattr(request, "LANGUAGE_CODE", None) or get_language() or "ru" - - # ۱. استتوس‌ها (مرتب‌شده بر اساس فیلد order) - statuses = [ - { - "id": status.id, - "slug": status.slug, - "title": get_localized_text(status.title, request=request, language_code=lang) or "", - "order": status.order, - "color": status.color, - "main_color_code": status.main_color_code, - } - for status in HadisStatus.objects.all().order_by('order') - ] - - # ۲. دسته‌بندی‌ها (مرتب‌شده بر اساس فیلد order، به استثنای دسته‌بندی‌های خالی) - from django.db.models import Count, Q - categories = [ - { - "id": cat.id, - "slug": cat.slug, - "title": get_localized_text(cat.title, request=request, language_code=lang) or "", - "order": cat.order, - } - for cat in HadisCategory.objects.annotate( - active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), - children_total_count=Count('children') - ).filter( - Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) - ).order_by('order') - ] - - # ۳. منابع / کتاب‌ها - sources = [] - # استفاده از select_related برای جلوگیری از مشکل N+1 Query هنگام گرفتن نویسنده‌ها - books = BookReference.objects.select_related('author').all().order_by('order', '-id') - - for book in books: - # استخراج نام نویسنده‌ها برای سرچ آفلاین فلاتر - authors_names = [ - get_localized_text(book.author.name, request=request, language_code=lang) or "" - ] if book.author else [] - - sources.append({ - "id": book.id, - "slug": book.slug, - "title": get_localized_text(book.title, request=request, language_code=lang) or "", - # تمام اسم‌های نویسنده‌ها را به هم می‌چسبانیم تا فلاتر بتواند روی آن‌ها سرچ بزند - # "search_keywords": " ".join(authors_names).strip() - }) - - # ۴. راویان (نریتورها) - narrators = [] - transmitters = Transmitters.objects.all().order_by('id') - - for narrator in transmitters: - # گرفتن نام‌های مستعار - known_as = get_localized_text(narrator.known_as, request=request, language_code=lang) or "" - nickname = get_localized_text(narrator.nickname, request=request, language_code=lang) or "" - kunya = get_localized_text(narrator.kunya, request=request, language_code=lang) or "" - - # تجمیع نام‌های جایگزین - search_aliases = f"{known_as} {nickname} {kunya}".strip() - - narrators.append({ - "id": narrator.id, - "slug": narrator.slug, - "title": get_localized_text(narrator.full_name, request=request, language_code=lang) or "", - # فلاتر روی این فیلد + title سرچ می‌زند - # "search_aliases": search_aliases - }) - - # خروجی نهایی بدون پجینیشن - return Response({ - "statuses": statuses, - "categories": categories, - "sources": sources, - "narrators": narrators - }) - -from ..serializers.hadis import HadisCorrectionDetailSerializer, HadisInterpretationDetailSerializer -from ..models import HadisInterpretation - -class HadisCorrectionDetailView(RetrieveAPIView): - """API View to get full details of a specific HadisCorrection by slug""" - serializer_class = HadisCorrectionDetailSerializer - lookup_field = 'slug' - lookup_url_kwarg = 'correction_slug' - permission_classes = [AllowAny] - - @swagger_auto_schema( - operation_summary="Get Hadis Correction Detail", - operation_description="Returns the full details of a specific hadis correction, including text, translations, and its book references.", - tags=['Dobodbi - Hadis (V2)'], - ) - def get(self, request, *args, **kwargs): - return super().get(request, *args, **kwargs) - - def get_queryset(self): - return HadisCorrection.objects.all().select_related( - 'hadis' - ).prefetch_related( - 'references__book_reference__author', - 'references__edition', - 'references__book_volume', - 'references__images' - ) - -class HadisInterpretationDetailView(RetrieveAPIView): - """API View to get full details of a specific HadisInterpretation by slug""" - serializer_class = HadisInterpretationDetailSerializer - lookup_field = 'slug' # 👈 تغییر به slug - lookup_url_kwarg = 'interpretation_slug' - permission_classes = [AllowAny] - - @swagger_auto_schema( - operation_summary="Get Hadis Interpretation Detail", - operation_description="Returns the full details of a specific hadis interpretation, including text, translations, and its book references.", - tags=['Dobodbi - Hadis (V2)'], - ) - def get(self, request, *args, **kwargs): - return super().get(request, *args, **kwargs) - - def get_queryset(self): - return HadisInterpretation.objects.all().select_related( - 'category' - ).prefetch_related( - 'references__book_reference__author', - 'references__edition', - 'references__book_volume', - 'references__images' - ) - - # 👈 هندل کردن حالت امن برای زمانی که چند تفسیر در یک کتگوری باشند - def get_object(self): - queryset = self.filter_queryset(self.get_queryset()) - lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field - filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} - - obj = queryset.filter(**filter_kwargs).first() # گرفتن اولین آبجکت - - if not obj: - raise Http404("No HadisInterpretation matches the given query.") - - self.check_object_permissions(self.request, obj) - return obj - - -class HadisInterpretsListView(ListAPIView): - """ - API View to list all interpretations for the category of a specific Hadis. - """ - serializer_class = HadisInterpretationDetailSerializer - authentication_classes = [TokenAuthentication] - permission_classes = [AllowAny] - pagination_class = StandardResultsSetPagination - lookup_field = 'slug' - lookup_url_kwarg = 'hadis_slug' - - @swagger_auto_schema( - operation_summary="Get Hadis Interpretations", - operation_description="Returns a paginated list of all interpretations registered for the category of this hadis, with full details and references.", - tags=['Dobodbi - Hadis (V2)'], - ) - def get(self, request, *args, **kwargs): - return self.list(request, *args, **kwargs) - - def get_queryset(self): - hadis_slug = self.kwargs.get('hadis_slug') - try: - hadis = Hadis.objects.select_related('category').get(slug=hadis_slug, status=True) - if not hadis.category_id: - return HadisInterpretation.objects.none() - - return HadisInterpretation.objects.filter( - category_id=hadis.category_id - ).select_related( - 'category' - ).prefetch_related( - 'references__book_reference__author', - 'references__edition', - 'references__book_volume', - 'references__images' - ).order_by('priority', 'id') - except Hadis.DoesNotExist: - 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 +from rest_framework.authentication import TokenAuthentication +from rest_framework.permissions import IsAuthenticated, AllowAny +from rest_framework.generics import ListAPIView, RetrieveAPIView +from drf_yasg.utils import swagger_auto_schema +from drf_yasg import openapi +from django.shortcuts import get_object_or_404 +from utils.pagination import NoPagination, StandardResultsSetPagination +from rest_framework.pagination import PageNumberPagination +from rest_framework.response import Response +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, CorrectionReference, HadisInterpretation, InterpretationReference +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 + +from rest_framework import status +from utils.mixins import CanonicalSlugViewSetMixin, Gone410ViewMixin + +class PinnedHadisCollectionListView(ListAPIView): + """ + API view to list pinned hadis collections with metadata info + """ + serializer_class = PinnedHadisCollectionSerializer + pagination_class = NoPagination + permission_classes = [IsAuthenticated] + authentication_classes = [TokenAuthentication] + + @hadis_collections_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + from django.db.models import Prefetch + from apps.hadis.models import HadisInCollection + return HadisCollection.objects.filter( + status=True, + display_position=HadisCollection.DisplayPosition.PINNED, + hadis_items__hadis__status=True + ).prefetch_related( + Prefetch( + 'hadis_items', + queryset=HadisInCollection.objects.filter(hadis__status=True).order_by('order') + ) + ).distinct().order_by('order', '-created_at') + + def list(self, request, *args, **kwargs): + # 1. گرفتن خروجی استاندارد لیست مجموعه‌ها + response = super().list(request, *args, **kwargs) + + # 2. محاسبه آمار (Info) + # تعداد کتگوری‌های فعال + categories_count = HadisCategory.objects.count() + + # تعداد نریتورها (راویان) + narrators_count = Transmitters.objects.count() + + # تعداد سورس‌ها (کتاب‌های منبع) + # فرض بر این است که BookReference مدل منابع شماست + from apps.hadis.models import BookReference + sources_count = BookReference.objects.count() + + # تعداد بوکمارک‌های کاربر در سرویس حدیث + from apps.bookmark.models.bookmark import Bookmark + bookmarks_count = Bookmark.objects.filter( + user=request.user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).count() + + # تعداد نویسنده‌ها + from apps.hadis.models import BookAuthor + authors_count = BookAuthor.objects.count() + + # 3. ساختن دیکشنری info + info = { + "categories_count": categories_count, + # "bookmarks_count": bookmarks_count, + "narrators_count": narrators_count, + "sources_count": sources_count, + "authors_count": authors_count, + } + + # 4. بازسازی دیتای نهایی مشابه اپ ویدیو + custom_data = { + "count": len(response.data) if isinstance(response.data, list) else response.data.get("count"), + "next": response.data.get("next") if isinstance(response.data, dict) else None, + "previous": response.data.get("previous") if isinstance(response.data, dict) else None, + "info": info, + "results": response.data if isinstance(response.data, list) else response.data.get("results") + } + + return Response(custom_data, status=status.HTTP_200_OK) + + +class HadisCollectionListView(ListAPIView): + """ + API view to list all hadis collections + """ + serializer_class = HadisCollectionListSerializer + pagination_class = NoPagination + + @hadis_collections_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + from django.db.models import Prefetch + from apps.hadis.models import HadisInCollection + return HadisCollection.objects.filter( + status=True, + display_position=HadisCollection.DisplayPosition.MIDDLE + ).prefetch_related( + Prefetch( + 'hadis_items', + queryset=HadisInCollection.objects.filter(hadis__status=True).order_by('order') + ) + ).order_by('order', '-created_at') + + +class HadisSyncView(ListAPIView): + """ + API view to sync all hadis data for offline mode + """ + serializer_class = HadisSyncSerializer + pagination_class = NoPagination + + def get_queryset(self): + qs = Hadis.objects.filter(status=True) + category_id = self.request.query_params.get('category_id') + category_slug = self.request.query_params.get('category_slug') + if category_id: + qs = qs.filter(category_id=category_id) + elif category_slug: + qs = qs.filter(category__slug=category_slug) + + return ( + qs + .select_related('category', 'hadis_status') + .prefetch_related( + 'tags', + Prefetch( + 'transmitters', + queryset=HadisTransmitter.objects.select_related( + 'transmitter', + 'transmitter__reliability', + 'narrator_layer' + ).prefetch_related( + 'uncertain_transmitters', + 'uncertain_transmitters__reliability' + ).order_by('order') + ), + Prefetch( + 'references', + queryset=HadisReference.objects + .select_related('book_reference') + .prefetch_related( + 'book_reference__author', + Prefetch( + 'images', + queryset=ReferenceImage.objects.order_by('priority') + ) + ) + ), + # 👇 اضافه شدن واکشی عمیق برای تصحیحات (بدون N+1) + Prefetch( + 'hadiscorrection_set', + queryset=HadisCorrection.objects.select_related('hadis').prefetch_related( + Prefetch( + 'references', + queryset=CorrectionReference.objects.select_related( + 'book_reference', + 'book_reference__author', + 'edition', + 'book_volume', + 'book_volume__edition' + ).prefetch_related( + 'images', + 'book_reference__editions', + 'book_reference__volumes' + ) + ) + ) + ), + # 👇 اضافه شدن واکشی عمیق برای تفاسیر (بدون N+1) + Prefetch( + 'category__interpretations', + queryset=HadisInterpretation.objects.select_related('category').prefetch_related( + Prefetch( + 'references', + queryset=InterpretationReference.objects.select_related( + 'book_reference', + 'book_reference__author', + 'edition', + 'book_volume', + 'book_volume__edition' + ).prefetch_related( + 'images', + 'book_reference__editions', + 'book_reference__volumes' + ) + ) + ) + ), + ) + .order_by('id') + ) + + # Incremental sync support to prevent transferring 25MB on subsequent syncs + req = getattr(self, 'request', None) + updated_after = (req.query_params.get('updated_after') or req.query_params.get('since')) if req and hasattr(req, 'query_params') else None + if updated_after: + # pyrefly: ignore [missing-import] + from django.utils.dateparse import parse_datetime + try: + dt = parse_datetime(updated_after) + if dt: + qs = qs.filter(updated_at__gte=dt) + except Exception: + pass + + return qs + + @hadis_sync_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset() + serializer = self.get_serializer( + queryset, + many=True, + context={'request': request}, + ) + return Response({ + 'count': len(serializer.data), + 'results': serializer.data + }) + +from ..serializers.category import CategoryQuranVerseSerializer +class HadisListView(ListAPIView): + """ + API view to list Hadis by category_id + """ + serializer_class = HadisListSerializer + pagination_class = StandardResultsSetPagination + authentication_classes = [TokenAuthentication] + + @hadis_list_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def list(self, request, *args, **kwargs): + # 1. Run the standard list logic (get pagination, filter, results) + response = super().list(request, *args, **kwargs) + + # 2. Find the "Parent" Category based on the URL slug + category_slug = self.kwargs.get('category_slug') + category_obj = get_object_or_404(HadisCategory, slug=category_slug) + + # 3. Serialize this single category for the Hero section + category_data = SimpleCategory(category_obj, context={'request': request}).data + + # 4. Check if it's a Quran Category and fetch verse data + quran_data = None + if category_obj.source_type == HadisCategory.SourceType.QURAN: + try: + from ..models.category import CategoryQuranVerse + verse_obj = CategoryQuranVerse.objects.prefetch_related( + 'secondary_translations' + ).get(category=category_obj) + + from ..serializers.category import CategoryQuranVerseSerializer + quran_data = CategoryQuranVerseSerializer( + verse_obj, + context={'request': request} + ).data + except CategoryQuranVerse.DoesNotExist: + pass + + # 👇 5. NEW LOGIC: Fetch all statuses for frontend filtering UI + lang = getattr(request, "LANGUAGE_CODE", None) or "fa" + statuses_list = [ + { + "id": status.id, + "slug": status.slug, + "title": get_localized_text(status.title, request=request, language_code=lang) or "", + "color": status.color, + "main_color_code": status.main_color_code, + } + for status in HadisStatus.objects.all().order_by('order') + ] + + # 6. Inject it into the response data + if isinstance(response.data, dict): + ordered_data = {} + for key in ['count', 'next', 'previous']: + if key in response.data: + ordered_data[key] = response.data[key] + + ordered_data['current_category'] = category_data + ordered_data['quran_data'] = quran_data + + # 👇 تزریق لیست استاتوس‌ها قبل از نتایج + ordered_data['statuses'] = statuses_list + + if 'results' in response.data: + ordered_data['results'] = response.data['results'] + response.data = ordered_data + + return response + + def get_queryset(self): + category_slug = self.kwargs.get('category_slug') + if not HadisCategory.objects.filter(slug=category_slug).exists(): + return Hadis.objects.none() + + queryset = Hadis.objects.filter( + category__slug=category_slug, + status=True + ).annotate( + layer_count=Count('transmitters__narrator_layer', distinct=True) + ).select_related('category').prefetch_related( + Prefetch( + 'references', + queryset=HadisReference.objects.prefetch_related( + Prefetch('images', queryset=ReferenceImage.objects.order_by('priority')) + ) + ) + ) + + # 👇 1. Apply Search Filter + search_query = self.request.query_params.get('search', None) + if search_query: + search_conditions = Q(text__icontains=search_query) + search_conditions |= Q(title__icontains=search_query) + search_conditions |= Q(title_narrator__icontains=search_query) + search_conditions |= Q(translation__icontains=search_query) + queryset = queryset.filter(search_conditions) + + # 👇 2. Apply Status Filter (Supports multiple comma-separated slugs) + status_filter = self.request.query_params.get('status', None) + if status_filter: + status_slugs = [s.strip() for s in status_filter.split(',')] + queryset = queryset.filter(hadis_status__slug__in=status_slugs) + + # 👇 3. Apply Transmitter Filter (Supports multiple comma-separated slugs) + transmitter_filter = self.request.query_params.get('transmitter', None) + if transmitter_filter: + transmitter_slugs = [t.strip() for t in transmitter_filter.split(',')] + queryset = queryset.filter(transmitters__transmitter__slug__in=transmitter_slugs) + + # 👇 4. Apply Source Filter (Supports multiple comma-separated slugs) + source_filter = self.request.query_params.get('source', None) + if source_filter: + source_slugs = [s.strip() for s in source_filter.split(',')] + queryset = queryset.filter(references__book_reference__slug__in=source_slugs) + + # 👇 5. Apply Source Author Filter (Supports author or source_author, multiple comma-separated slugs/IDs) + author_filter = self.request.query_params.get('author', None) or self.request.query_params.get('source_author', None) + if author_filter: + author_slugs = [a.strip() for a in author_filter.split(',') if a.strip()] + author_q = Q(references__book_reference__author__slug__in=author_slugs) + numeric_ids = [int(a) for a in author_slugs if a.isdigit()] + if numeric_ids: + author_q |= Q(references__book_reference__author_id__in=numeric_ids) + queryset = queryset.filter(author_q) + + # Filter by bookmarks if provided + is_bookmark = self.request.query_params.get('is_bookmark', '').lower() + if is_bookmark == 'true' and self.request.user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + bookmarked_ids = Bookmark.objects.filter( + user=self.request.user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).values_list('content_id', flat=True) + queryset = queryset.filter(id__in=bookmarked_ids) + + # ❗️ distinct() برای جلوگیری از دابلیکیت شدن احادیث به خاطر ManyToMany ها الزامی است + return queryset.distinct() + + def get_serializer_context(self): + """Add user bookmarks to serializer context to avoid caching issues""" + context = super().get_serializer_context() + + # Add user's bookmarked hadis IDs to context + user = self.request.user + if user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + user_bookmarks = Bookmark.objects.filter( + user=user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).values_list('content_id', flat=True) + context['user_bookmarked_hadis_ids'] = set(user_bookmarks) + else: + context['user_bookmarked_hadis_ids'] = set() + + return context + + + + +class HadisMainListView(ListAPIView): + """ + API view to list Hadis by category_id + """ + serializer_class = HadisListSerializer + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + pagination_class = StandardResultsSetPagination + + @hadis_main_list_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + queryset = Hadis.objects.select_related('category__sect', 'hadis_status').prefetch_related( + Prefetch( + 'references', + queryset=HadisReference.objects.select_related('book_reference__author').prefetch_related( + Prefetch('images', queryset=ReferenceImage.objects.order_by('priority')) + ) + ) + ) + + # Get search parameters + search_query = self.request.query_params.get('search', None) + status_filter = self.request.query_params.get('status', None) + category_filter = self.request.query_params.get('category', None) + + # Apply search filter + if search_query: + queryset = self.apply_search_filter(queryset, search_query) + + # Apply status filter + if status_filter: + queryset = queryset.filter(hadis_status__title__icontains=status_filter) + + # Apply category filter + if category_filter: + queryset = queryset.filter(category__title__icontains=category_filter) + + # Filter by bookmarks if provided + is_bookmark = self.request.query_params.get('is_bookmark', '').lower() + if is_bookmark == 'true' and self.request.user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + bookmarked_ids = Bookmark.objects.filter( + user=self.request.user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).values_list('content_id', flat=True) + queryset = queryset.filter(id__in=bookmarked_ids) + + return queryset + + def list(self, request, *args, **kwargs): + queryset = self.get_queryset() + + # Apply pagination + page = self.paginate_queryset(queryset) + if page is not None: + serializer = self.get_serializer(page, many=True) + paginated_response = self.get_paginated_response(serializer.data) + + # Get category titles + category_titles = self.get_category_titles(request) + + # Get status titles + status_titles = self.get_status_titles(request) + + # Modify the paginated response to include our custom data + response_data = paginated_response.data + # response_data['category_titles'] = self.get_cached_category_titles(request) + # response_data['status_titles'] = self.get_cached_status_titles(request) + + return Response(response_data) + + # Fallback for when pagination is disabled + serializer = self.get_serializer(queryset, many=True) + return Response({ + 'count': queryset.count(), + 'results': serializer.data + }) + + return Response(response_data) + + def get_category_titles(self,request): + """Get list of category titles based on language""" + from ..models import HadisCategory + + categories = HadisCategory.objects.all() + category_titles = [] + + for category in categories: + title = get_localized_text(category.title,request) + category_titles.append(title) + + return category_titles + + def get_status_titles(self, request): + """Get list of status titles based on language""" + from ..models import HadisStatus + + statuses = HadisStatus.objects.all().order_by('order') + status_titles = [] + + for status in statuses: + title = get_localized_text(status.title,request) + status_titles.append(title) + return status_titles + + def apply_search_filter(self, queryset, search_query): + """ + Apply search filter across multiple fields including JSONFields. + Searches in: title, title_narrator, text, translation + """ + from django.db.models import Q + + # Basic search conditions + search_conditions = Q(text__icontains=search_query) + + # For JSONFields, search in the JSON string representation + # This will find matches in the "text" values within the JSON arrays + search_conditions |= Q(title__icontains=search_query) + search_conditions |= Q(title_narrator__icontains=search_query) + search_conditions |= Q(translation__icontains=search_query) + + return queryset.filter(search_conditions) + + #we add this later + + # def get_cached_category_titles(self, request): + # """Fetches categories, cached for 1 hour to reduce DB load""" + # lang = getattr(request, "LANGUAGE_CODE", "en") + # cache_key = f"hadis_meta_categories_{lang}" + + # data = cache.get(cache_key) + # if not data: + # # If not in cache, fetch from DB + # from ..models import HadisCategory + # categories = HadisCategory.objects.all().only('id', 'title') # Fetch only needed fields + + # # Build list + # data = [get_localized_text(c.title, request) for c in categories] + + # # Save to cache + # cache.set(cache_key, data, timeout=60 * 60) # 1 Hour + + # return data + + # def get_cached_status_titles(self, request): + # """Fetches statuses, cached for 1 hour""" + # lang = getattr(request, "LANGUAGE_CODE", "en") + # cache_key = f"hadis_meta_statuses_{lang}" + + # data = cache.get(cache_key) + # if not data: + # from ..models import HadisStatus + # statuses = HadisStatus.objects.all().order_by('order') + # data = [get_localized_text(s.title, request) for s in statuses] + # cache.set(cache_key, data, timeout=60 * 60) + + # return data + + +class HadisBasicView(RetrieveAPIView): + """ + API view to retrieve basic Hadis information by hadis_slug + """ + serializer_class = HadisBasicSerializer + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + authentication_classes = [TokenAuthentication] + + @hadis_basic_swagger + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def get_queryset(self): + return Hadis.objects.filter(status=True) + + def get_serializer_context(self): + """Add user bookmarks to serializer context to avoid caching issues""" + context = super().get_serializer_context() + + # Add user's bookmarked hadis IDs to context + user = self.request.user + if user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + user_bookmarks = Bookmark.objects.filter( + user=user, + service=Bookmark.ServiceChoices.HADITH, + status=True + ).values_list('content_id', flat=True) + context['user_bookmarked_hadis_ids'] = set(user_bookmarks) + else: + context['user_bookmarked_hadis_ids'] = set() + + return context + + +class HadisDetailView(Gone410ViewMixin, CanonicalSlugViewSetMixin, RetrieveAPIView): + """ + API view to retrieve detailed Hadis information by hadis_slug (excluding transmitters and corrections) + """ + serializer_class = HadisDetailSerializer + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + + active_field_name = 'status' + active_expected_value = True + + @hadis_detail_swagger + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def get_queryset(self): + return Hadis.objects.all().select_related( + 'category', 'hadis_status' + ).prefetch_related( + 'tags', + 'references__book_reference', + 'references__book_reference__author', + 'references__images', + ) + +class HadisTransmittersView(RetrieveAPIView): + """ + Fetches a single Hadis but filters the nested Transmitters list + if a ?layer=slug param is provided. + """ + serializer_class = HadisTransmitterListSerializer + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + pagination_class = NoPagination + @hadis_transmitters_swagger + def get(self, request, *args, **kwargs): + return self.retrieve(request, *args, **kwargs) + + def get_queryset(self): + # 1. Get the filter param + layer_slug = self.request.query_params.get('layer') + + # 2. Build the query for the "Child" (Transmitters) + # We start with the base optimization (select_related) + transmitter_qs = HadisTransmitter.objects.select_related( + 'transmitter', + 'narrator_layer', + 'status' + ).prefetch_related('uncertain_transmitters').order_by('order') + + # 3. Apply the filter to the Child Query (if param exists) + if layer_slug: + # Assumes 'NarratorLayer' has a 'slug' field. + # If not, use 'narrator_layer__name' or 'narrator_layer__id'. + transmitter_qs = transmitter_qs.filter(narrator_layer__slug=layer_slug) + + # 4. Use the Prefetch object to inject this filtered list into the Parent + return Hadis.objects.filter(status=True).prefetch_related( + Prefetch('transmitters', queryset=transmitter_qs) + ) + +class HadisCorrectionsView(ListAPIView): + """ + API view to retrieve corrections for a specific hadis + """ + serializer_class = HadisCorrectionSerializer + authentication_classes = [TokenAuthentication] + permission_classes = [IsAuthenticated] + pagination_class = StandardResultsSetPagination + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + + @hadis_corrections_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + # hadis = self.get_object() + # corrections_data = [] + + # for correction in hadis.hadiscorrection_set.all(): + # correction_info = { + # 'id': correction.id, + # 'title': correction.title, + # 'description': correction.description, + # 'translation': correction.translation + # } + # corrections_data.append(correction_info) + + # return Response({ + # 'hadis_id': hadis.id, + # 'corrections_count': len(corrections_data), + # 'corrections': corrections_data + # }) + + def get_queryset(self): + hadis_slug = self.kwargs.get('hadis_slug') + try: + hadis = Hadis.objects.get(slug=hadis_slug, status=True) + if not HadisCorrection.objects.filter(hadis=hadis).exists(): + return Hadis.objects.none() + queryset = HadisCorrection.objects.filter(hadis=hadis) + + # Filter by bookmarks if provided + is_bookmark = self.request.query_params.get('is_bookmark', '').lower() + if is_bookmark == 'true' and self.request.user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + bookmarked_ids = Bookmark.objects.filter( + user=self.request.user, + service=Bookmark.ServiceChoices.HADITH_CORRECTION, + status=True + ).values_list('content_id', flat=True) + queryset = queryset.filter(id__in=bookmarked_ids) + + return queryset + except Hadis.DoesNotExist: + return HadisCorrection.objects.none() + + + +class CategoryHadisCorrectionsView(ListAPIView): + """ + API view to retrieve all corrections across all hadiths belonging to a specific category + """ + serializer_class = HadisCorrectionSerializer + authentication_classes = [TokenAuthentication] + permission_classes = [AllowAny] + pagination_class = StandardResultsSetPagination + + @swagger_auto_schema( + operation_summary="Get Category Hadis Corrections", + operation_description="Returns all text corrections across all hadiths belonging to a specific category, including hadis_info for each correction. Supports filtering by multiple hadiths and/or categories.", + tags=['Dobodbi - Hadis (V2)'], + manual_parameters=[ + openapi.Parameter('search', openapi.IN_QUERY, description="Search in text or narrator", type=openapi.TYPE_STRING), + openapi.Parameter('hadis_slug', openapi.IN_QUERY, description="Filter corrections by specific hadith slug (comma-separated for multiple)", type=openapi.TYPE_STRING), + openapi.Parameter('hadis_slugs', openapi.IN_QUERY, description="Filter corrections by multiple hadith slugs (comma-separated)", type=openapi.TYPE_STRING), + openapi.Parameter('category_slugs', openapi.IN_QUERY, description="Filter corrections by multiple category slugs (comma-separated)", type=openapi.TYPE_STRING), + openapi.Parameter('is_bookmark', openapi.IN_QUERY, description="Filter only bookmarked corrections (requires auth)", type=openapi.TYPE_BOOLEAN), + ] + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + category_slug = self.kwargs.get('category_slug') + try: + category_ids = [] + if category_slug: + category = HadisCategory.objects.get(slug=category_slug) + category_ids = list(category.get_descendants(include_self=True).values_list('id', flat=True)) + + # Optional filter by multiple categories (selected subcategories or category slugs/IDs) + cat_param_keys = ['category_slugs', 'categories', 'category_slug'] + cat_slug_list = [] + for key in cat_param_keys: + vals = self.request.query_params.getlist(key) if hasattr(self.request.query_params, 'getlist') else [] + if not vals and key in self.request.query_params: + val = self.request.query_params.get(key) + if val: + vals = [val] if isinstance(val, str) else list(val) + for v in vals: + if isinstance(v, str): + cat_slug_list.extend([s.strip() for s in v.split(',') if s.strip()]) + elif isinstance(v, (list, tuple)): + cat_slug_list.extend(v) + elif v is not None: + cat_slug_list.append(str(v)) + + if cat_slug_list: + cat_filter = Q(slug__in=cat_slug_list) + id_list = [int(s) for s in cat_slug_list if str(s).isdigit()] + if id_list: + cat_filter |= Q(id__in=id_list) + + selected_categories = HadisCategory.objects.filter(cat_filter) + selected_cat_ids = set() + for sc in selected_categories: + selected_cat_ids.update(sc.get_descendants(include_self=True).values_list('id', flat=True)) + + if selected_cat_ids: + if category_ids: + category_ids = list(set(category_ids).intersection(selected_cat_ids)) + else: + category_ids = list(selected_cat_ids) + else: + return HadisCorrection.objects.none() + + hadis_filter_kwargs = {'status': True} + if category_ids: + hadis_filter_kwargs['category_id__in'] = category_ids + + hadis_ids = Hadis.objects.filter(**hadis_filter_kwargs).values_list('id', flat=True) + + queryset = HadisCorrection.objects.filter( + hadis_id__in=hadis_ids + ).select_related('hadis').prefetch_related( + 'references', + 'references__images' + ).order_by('priority', '-created_at') + + # Optional search query filter + search_query = self.request.query_params.get('search', None) + if search_query: + search_conditions = ( + Q(title__icontains=search_query) | + Q(text__icontains=search_query) | + Q(narrator__icontains=search_query) | + Q(translation__icontains=search_query) | + Q(hadis__title__icontains=search_query) | + Q(hadis__title_narrator__icontains=search_query) + ) + queryset = queryset.filter(search_conditions) + + # Optional filter by specific hadith slug(s) or ID(s) + hadis_param_keys = ['hadis_slug', 'hadis_slugs', 'hadiths'] + hadis_slug_list = [] + for key in hadis_param_keys: + vals = self.request.query_params.getlist(key) if hasattr(self.request.query_params, 'getlist') else [] + if not vals and key in self.request.query_params: + val = self.request.query_params.get(key) + if val: + vals = [val] if isinstance(val, str) else list(val) + for v in vals: + if isinstance(v, str): + hadis_slug_list.extend([s.strip() for s in v.split(',') if s.strip()]) + elif isinstance(v, (list, tuple)): + hadis_slug_list.extend(v) + elif v is not None: + hadis_slug_list.append(str(v)) + + if hadis_slug_list: + hadis_query = Q(hadis__slug__in=hadis_slug_list) + id_list = [int(s) for s in hadis_slug_list if str(s).isdigit()] + if id_list: + hadis_query |= Q(hadis__id__in=id_list) + + queryset = queryset.filter(hadis_query) + + # Filter by bookmarks if provided + is_bookmark = self.request.query_params.get('is_bookmark', '').lower() + if is_bookmark == 'true' and self.request.user.is_authenticated: + from apps.bookmark.models.bookmark import Bookmark + bookmarked_ids = Bookmark.objects.filter( + user=self.request.user, + service=Bookmark.ServiceChoices.HADITH_CORRECTION, + status=True + ).values_list('content_id', flat=True) + queryset = queryset.filter(id__in=bookmarked_ids) + + return queryset + except HadisCategory.DoesNotExist: + return HadisCorrection.objects.none() + + def list(self, request, *args, **kwargs): + response = super().list(request, *args, **kwargs) + category_slug = self.kwargs.get('category_slug') + category_obj = HadisCategory.objects.filter(slug=category_slug).first() if category_slug else None + category_data = SimpleCategory(category_obj, context={'request': request}).data if category_obj else None + + available_hadiths = [] + available_categories = [] + if category_obj: + cat_ids = category_obj.get_descendants(include_self=True).values_list('id', flat=True) + hadiths_in_cat = Hadis.objects.filter( + category_id__in=cat_ids, + status=True + ).order_by('number', 'id') + + for h in hadiths_in_cat: + h_title = get_localized_text(h.title, request) if hasattr(h, 'title') and h.title else f"Hadith {h.number}" + available_hadiths.append({ + "id": h.id, + "number": h.number, + "slug": h.slug, + "title": h_title, + }) + + subcats = category_obj.get_children() if hasattr(category_obj, 'get_children') else category_obj.children.all() + for sc in subcats: + sc_title = get_localized_text(sc.title, request) if hasattr(sc, 'title') and sc.title else sc.slug + available_categories.append({ + "id": sc.id, + "slug": sc.slug, + "title": sc_title, + }) + + if isinstance(response.data, dict): + ordered_data = { + 'count': response.data.get('count', 0), + 'next': response.data.get('next'), + 'previous': response.data.get('previous'), + 'current_category': category_data, + 'available_hadiths': available_hadiths, + 'available_categories': available_categories, + 'results': response.data.get('results', []), + } + response.data = ordered_data + + return response + + + +class HadisLayersView(ListAPIView): + """ + API view to retrieve all narrator layers for a specific hadis + """ + serializer_class = NarratorLayerSerializer + pagination_class = NoPagination + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + + @hadis_layers_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + from ..models import NarratorLayer + + hadis_slug = self.kwargs.get('hadis_slug') + + # Get the hadis object to ensure it exists + hadis = get_object_or_404(Hadis, slug=hadis_slug, status=True) + + # Return the layer objects for this hadis ordered by number + return NarratorLayer.objects.filter(hadis=hadis).order_by('number') + + +class HadisFiltersView(ListAPIView): + """ + API view to return filter data for hadis + Returns statuses and categories for filtering + """ + pagination_class = NoPagination + + @arguments_filters_swagger + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + # This view doesn't need a queryset, it returns computed data + return Hadis.objects.none() + + def list(self, request, *args, **kwargs): + # Get statuses from HadisStatus model + statuses = [] + for status in HadisStatus.objects.all().order_by('order'): + title_text = get_localized_text(status.title, request) + if title_text and status.slug: + statuses.append({ + 'text': title_text, + 'slug': status.slug + }) + + # Get categories from HadisCategory model (excluding empty leaf categories) + from django.db.models import Count, Q + categories = [] + for category in HadisCategory.objects.annotate( + active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), + children_total_count=Count('children') + ).filter( + Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) + ).order_by('order'): + title_text = get_localized_text(category.title, request) + if title_text and category.slug: + categories.append({ + 'text': title_text, + 'slug': category.slug + }) + + response_data = { + 'statuses': statuses, + 'categories': categories + } + + return Response(response_data) + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework.permissions import AllowAny +from rest_framework.authentication import TokenAuthentication +from drf_yasg.utils import swagger_auto_schema +from django.utils.translation import get_language + +from apps.hadis.models import HadisStatus, HadisCategory, BookReference, Transmitters +from drf_yasg import openapi +class HadisFiltersSyncAPIView(APIView): + """ + API view to return all filter data for hadis (Sync API for Mobile App) + Returns statuses, categories, sources (books), and narrators. + Optimized for offline client-side searching and sorting. + """ + permission_classes = [AllowAny] + authentication_classes = [TokenAuthentication] + + @swagger_auto_schema( + operation_description=( + "Sync API for Flutter/mobile clients: returns flat hadis filter data for offline " + "searching and local filtering. Titles are localized from multilingual fields based " + "on the active request language." + ), + operation_summary="Sync Hadis Filters", + tags=['Dobodbi - Hadis'], + manual_parameters=[ + openapi.Parameter( + 'Accept-Language', + openapi.IN_HEADER, + description="Language code for localized titles, for example `en`, `fa`, `ar`, `ur`, or `ru`.", + type=openapi.TYPE_STRING, + required=False, + ) + ], + responses={ + status.HTTP_200_OK: openapi.Response( + description="Flat filter payload for hadis categories, statuses, sources, and narrators.", + examples={ + "application/json": { + "statuses": [ + { + "id": 133, + "slug": "sahih", + "title": "Authentic", + "order": 1, + "color": "green", + "main_color_code": "#1DAC43" + } + ], + "categories": [ + { + "id": 1036, + "slug": "general-hadiths", + "title": "General Hadiths", + "order": 1 + } + ], + "sources": [ + { + "id": 2538, + "slug": "man-la-yahduruhu-al-faqih", + "title": "Man La Yahduruhu al-Faqih", + "search_keywords": "Shaykh al-Saduq" + } + ], + "narrators": [ + { + "id": 501, + "slug": "muhammad-ibn-muslim", + "title": "Muhammad ibn Muslim", + "search_aliases": "Abu Ja'far" + } + ] + } + } + ) + } + ) + def get(self, request, *args, **kwargs): + # تشخیص زبان برای ارسال دیتای لوکالایز شده + lang = getattr(request, "LANGUAGE_CODE", None) or get_language() or "ru" + + # ۱. استتوس‌ها (مرتب‌شده بر اساس فیلد order) + statuses = [ + { + "id": status.id, + "slug": status.slug, + "title": get_localized_text(status.title, request=request, language_code=lang) or "", + "order": status.order, + "color": status.color, + "main_color_code": status.main_color_code, + } + for status in HadisStatus.objects.all().order_by('order') + ] + + # ۲. دسته‌بندی‌ها (مرتب‌شده بر اساس فیلد order، به استثنای دسته‌بندی‌های خالی) + from django.db.models import Count, Q + categories = [ + { + "id": cat.id, + "slug": cat.slug, + "title": get_localized_text(cat.title, request=request, language_code=lang) or "", + "order": cat.order, + } + for cat in HadisCategory.objects.annotate( + active_hadis_count=Count('hadis', filter=Q(hadis__status=True)), + children_total_count=Count('children') + ).filter( + Q(active_hadis_count__gt=0) | Q(children_total_count__gt=0) | Q(source_type=HadisCategory.SourceType.QURAN) + ).order_by('order') + ] + + # ۳. منابع / کتاب‌ها + sources = [] + # استفاده از select_related برای جلوگیری از مشکل N+1 Query هنگام گرفتن نویسنده‌ها + books = BookReference.objects.select_related('author').all().order_by('order', '-id') + + for book in books: + # استخراج نام نویسنده‌ها برای سرچ آفلاین فلاتر + authors_names = [ + get_localized_text(book.author.name, request=request, language_code=lang) or "" + ] if book.author else [] + + sources.append({ + "id": book.id, + "slug": book.slug, + "title": get_localized_text(book.title, request=request, language_code=lang) or "", + # تمام اسم‌های نویسنده‌ها را به هم می‌چسبانیم تا فلاتر بتواند روی آن‌ها سرچ بزند + # "search_keywords": " ".join(authors_names).strip() + }) + + # ۴. راویان (نریتورها) + narrators = [] + transmitters = Transmitters.objects.all().order_by('id') + + for narrator in transmitters: + # گرفتن نام‌های مستعار + known_as = get_localized_text(narrator.known_as, request=request, language_code=lang) or "" + nickname = get_localized_text(narrator.nickname, request=request, language_code=lang) or "" + kunya = get_localized_text(narrator.kunya, request=request, language_code=lang) or "" + + # تجمیع نام‌های جایگزین + search_aliases = f"{known_as} {nickname} {kunya}".strip() + + narrators.append({ + "id": narrator.id, + "slug": narrator.slug, + "title": get_localized_text(narrator.full_name, request=request, language_code=lang) or "", + # فلاتر روی این فیلد + title سرچ می‌زند + # "search_aliases": search_aliases + }) + + # خروجی نهایی بدون پجینیشن + return Response({ + "statuses": statuses, + "categories": categories, + "sources": sources, + "narrators": narrators + }) + +from ..serializers.hadis import HadisCorrectionDetailSerializer, HadisInterpretationDetailSerializer +from ..models import HadisInterpretation + +class HadisCorrectionDetailView(RetrieveAPIView): + """API View to get full details of a specific HadisCorrection by slug""" + serializer_class = HadisCorrectionDetailSerializer + lookup_field = 'slug' + lookup_url_kwarg = 'correction_slug' + permission_classes = [AllowAny] + + @swagger_auto_schema( + operation_summary="Get Hadis Correction Detail", + operation_description="Returns the full details of a specific hadis correction, including text, translations, and its book references.", + tags=['Dobodbi - Hadis (V2)'], + ) + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + + def get_queryset(self): + return HadisCorrection.objects.all().select_related( + 'hadis' + ).prefetch_related( + 'references__book_reference__author', + 'references__edition', + 'references__book_volume', + 'references__images' + ) + +class HadisInterpretationDetailView(RetrieveAPIView): + """API View to get full details of a specific HadisInterpretation by slug""" + serializer_class = HadisInterpretationDetailSerializer + lookup_field = 'slug' # 👈 تغییر به slug + lookup_url_kwarg = 'interpretation_slug' + permission_classes = [AllowAny] + + @swagger_auto_schema( + operation_summary="Get Hadis Interpretation Detail", + operation_description="Returns the full details of a specific hadis interpretation, including text, translations, and its book references.", + tags=['Dobodbi - Hadis (V2)'], + ) + def get(self, request, *args, **kwargs): + return super().get(request, *args, **kwargs) + + def get_queryset(self): + return HadisInterpretation.objects.all().select_related( + 'category' + ).prefetch_related( + 'references__book_reference__author', + 'references__edition', + 'references__book_volume', + 'references__images' + ) + + # 👈 هندل کردن حالت امن برای زمانی که چند تفسیر در یک کتگوری باشند + def get_object(self): + queryset = self.filter_queryset(self.get_queryset()) + lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field + filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]} + + obj = queryset.filter(**filter_kwargs).first() # گرفتن اولین آبجکت + + if not obj: + raise Http404("No HadisInterpretation matches the given query.") + + self.check_object_permissions(self.request, obj) + return obj + + +class HadisInterpretsListView(ListAPIView): + """ + API View to list all interpretations for the category of a specific Hadis. + """ + serializer_class = HadisInterpretationDetailSerializer + authentication_classes = [TokenAuthentication] + permission_classes = [AllowAny] + pagination_class = StandardResultsSetPagination + lookup_field = 'slug' + lookup_url_kwarg = 'hadis_slug' + + @swagger_auto_schema( + operation_summary="Get Hadis Interpretations", + operation_description="Returns a paginated list of all interpretations registered for the category of this hadis, with full details and references.", + tags=['Dobodbi - Hadis (V2)'], + ) + def get(self, request, *args, **kwargs): + return self.list(request, *args, **kwargs) + + def get_queryset(self): + hadis_slug = self.kwargs.get('hadis_slug') + try: + hadis = Hadis.objects.select_related('category').get(slug=hadis_slug, status=True) + if not hadis.category_id: + return HadisInterpretation.objects.none() + + return HadisInterpretation.objects.filter( + category_id=hadis.category_id + ).select_related( + 'category' + ).prefetch_related( + 'references__book_reference__author', + 'references__edition', + 'references__book_volume', + 'references__images' + ).order_by('priority', 'id') + except Hadis.DoesNotExist: + 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 +