Browse Source

feat(hadis): implement relational models for relation types and professions

- Add RelationType and TransmitterProfession models with two-way sync
- Add migrations 0063 and 0064 for relation types and professions seeding and data migration
- Register RelationType admin serializer, viewset and URL routes
- Update relational migration roadmap documentation
master
Mohsen Taba 1 week ago
parent
commit
aab232bb14
  1. 29
      RELATIONAL_MIGRATION_ROADMAP.md
  2. 822
      apps/hadis/migrations/0063_relation_type_and_relations.py
  3. 109
      apps/hadis/migrations/0064_transmitter_profession_and_relations.py
  4. 89
      apps/hadis/models/transmitter.py
  5. 67
      apps/hadis/serializers/serializers_admin.py
  6. 8
      apps/hadis/urls.py
  7. 40
      apps/hadis/views_admin.py

29
RELATIONAL_MIGRATION_ROADMAP.md

@ -11,7 +11,7 @@
- [x] **فاز ۳: وضعیت صحابی بودن (`SahabiStatus` / `sahabi_status`)** — رابطه تک‌به‌چند (ForeignKey / M2O) *(تکمیل شده)* - [x] **فاز ۳: وضعیت صحابی بودن (`SahabiStatus` / `sahabi_status`)** — رابطه تک‌به‌چند (ForeignKey / M2O) *(تکمیل شده)*
- [—] **فاز ۴: تفکیک و اتصال رتبه‌بندی و وثاقت راوی و مؤلف (`NarratorRating` و `reliability_levels`)** — *(صرف‌نظر شد / اسکیپ طبق تصمیم: سیستم فعلی نظرات رجالی پاسخگو است)* - [—] **فاز ۴: تفکیک و اتصال رتبه‌بندی و وثاقت راوی و مؤلف (`NarratorRating` و `reliability_levels`)** — *(صرف‌نظر شد / اسکیپ طبق تصمیم: سیستم فعلی نظرات رجالی پاسخگو است)*
- [x] **فاز ۵: مذاهب راویان (`Madhhab` / `madhabs`)** — رابطه چندبه‌چند (M2M) *(تکمیل شده)* - [x] **فاز ۵: مذاهب راویان (`Madhhab` / `madhabs`)** — رابطه چندبه‌چند (M2M) *(تکمیل شده)*
- [ ] **فاز ۶: داینامیک‌سازی انواع نسبت‌های خویشاوندی (`RelationType` / `relatives`)** — رابطه ForeignKey در `TransmitterRelative`
- [x] **فاز ۶: داینامیک‌سازی انواع نسبت‌های خویشاوندی (`RelationType` / `relatives`)** — رابطه ForeignKey در `TransmitterRelative` *(تکمیل شده)*
- [ ] **فاز ۷: مکان‌های چندگانه راوی (`narrators_locations`)** — اتصال چندبه‌چند با ترتیب (`sort_order`) و نوع (`residence` / `death`) - [ ] **فاز ۷: مکان‌های چندگانه راوی (`narrators_locations`)** — اتصال چندبه‌چند با ترتیب (`sort_order`) و نوع (`residence` / `death`)
--- ---
@ -135,16 +135,25 @@
--- ---
### فاز ۶: داینامیک‌سازی انواع نسبت‌های خویشاوندی (`RelationType` / `relatives`) 🔲
* **وضعیت:** `در انتظار اجرا`
### فاز ۶: داینامیک‌سازی انواع نسبت‌های خویشاوندی (`RelationType` / `relatives`) ✅
* **وضعیت:** `تکمیل شده`
* **نوع رابطه طبق داکیومنت کارفرما:** **ForeignKey در جدول خویشاوندان راوی** * **نوع رابطه طبق داکیومنت کارفرما:** **ForeignKey در جدول خویشاوندان راوی**
* **مراحل پیاده‌سازی بک‌اند:**
1. ساخت مدل `RelationType` شامل فیلد کلیدی و `title` چندزبانه (برای جایگزینی دیکشنری هاردکد شده `RELATION_TYPE_MAP`).
2. متصل کردن `TransmitterRelative.relation_type_ref = models.ForeignKey(RelationType, ...)` به مدل جدید.
3. دیتامایگریشن برای انتقال ۲۹ نسبت خویشاوندی موجود به رکوردهای جدول جدید.
4. اندپوینت‌های مدیریت نسبت‌ها در پنل ادمین.
* **مراحل پیاده‌سازی داشبورد:**
1. دریافت داینامیک لیست نسبت‌های خویشاوندی از API به جای گزینه‌های استاتیک در فرم خویشاوندان راوی.
> انواع نسبت‌های خویشاوندی به عنوان یک کالکشن مستقل چندزبانه با کلید شناسایی انگلیسی (`code`) و عناوین به ۴ زبان (`fa`, `ar`, `en`, `ru`) پیاده‌سازی شدند.
* **کارهای انجام‌شده:**
1. **مدل `RelationType`:** ایجاد در `transmitter.py` با فیلدهای `code` (یکتا و کلید تطبیق انگلیسی)، `title` (چندزبانه به ۴ زبان: fa, ar, en, ru)، `slug` (یکتا و خودکار)، متد هلپر `get_title()` و تایم‌استمپ‌ها.
2. **اتصال رابطه رابطه‌ای به `TransmitterRelative`:** افزودن کلید خارجی `relation_type_ref = models.ForeignKey('hadis.RelationType', on_delete=models.SET_NULL, null=True, blank=True, related_name='relative_links')` با حفظ سازگاری کامل به عقب با فیلد متنی قدیمی `relation_type` و همگام‌سازی خودکار در متد `save()`.
3. **مایگریشن و Seeding دیتابیس (۰۶۳):**
- ایجاد جدول `RelationType` و Seed کردن تمام ۲۹ نسبت خویشاوندی موجود در پروژه به همراه عناوین کامل عربی، فارسی، انگلیسی و روسی.
- مهاجرت و اتصال خودکار هر ۹۱۱ رکورد موجود در جدول `TransmitterRelative` به رکوردهای متناظر در جدول جدید بر اساس کد نسبت.
4. **سریالایزرها و APIها:**
- ساخت `AdminRelationTypeSerializer` و `DovodiRelationTypeViewSet`.
- ثبت روت‌های `/api/hadis/admin/dovodi-relation-types/` و `/api/hadis/admin/relation-types/`.
- پشتیبانی از جستجو بر اساس `title`, `slug`, `code`.
5. **داشبورد (فرانت‌اند):**
- افزودن اینترفیس `RelationTypeItem` و متدهای کامل API (`getRelationTypesArchive`, `createRelationTypeArchive`, `updateRelationTypeArchive`, `deleteRelationTypeArchive`) در `transmitters.ts`.
- طراحی و ساخت مودال کامل `RelationTypeDialog` با هدر استاندارد چندزبانه، فیلد عنوان چندزبانه، فیلد شناسه انگلیسی (`code`)، اسلاگ و فوتر چسبان با تأیید حذف.
- افزودن تب «انواع نسبت‌ها»، ستون‌های شناسه، عنوان، کد انگلیسی و اسلاگ به همراه فیلتر سرچ و حذف تکی و دسته‌جمعی در صفحه آرشیو راویان (`TransmittersArchivePage.tsx`).
- بیلد نهایی پروژه فرانت‌اند با موفقیت و بدون هیچ خطایی انجام گرفت.
--- ---

822
apps/hadis/migrations/0063_relation_type_and_relations.py

@ -0,0 +1,822 @@
# Generated by Django 4.2.30 on 2026-10-20 16:15
from django.db import migrations, models
import django.db.models.deletion
RELATION_TYPE_DATA = [
{
"code": "stepfather",
"slug": "stepfather",
"title": [
{
"language_code": "ar",
"text": "زوج الأم",
"title": "زوج الأم"
},
{
"language_code": "fa",
"text": "ناپدری",
"title": "ناپدری"
},
{
"language_code": "en",
"text": "Stepfather",
"title": "Stepfather"
},
{
"language_code": "ru",
"text": "Отчим",
"title": "Отчим"
}
]
},
{
"code": "stepmother",
"slug": "stepmother",
"title": [
{
"language_code": "ar",
"text": "زوجة الأب",
"title": "زوجة الأب"
},
{
"language_code": "fa",
"text": "نامادری",
"title": "نامادری"
},
{
"language_code": "en",
"text": "Stepmother",
"title": "Stepmother"
},
{
"language_code": "ru",
"text": "Мачеха",
"title": "Мачеха"
}
]
},
{
"code": "uncles_and_aunts",
"slug": "uncles-and-aunts",
"title": [
{
"language_code": "ar",
"text": "الأعمام والعمات والأخوال والخالات",
"title": "الأعمام والعمات والأخوال والخالات"
},
{
"language_code": "fa",
"text": "عموها، عمه‌ها، دایی‌ها و خاله‌ها",
"title": "عموها، عمه‌ها، دایی‌ها و خاله‌ها"
},
{
"language_code": "en",
"text": "Uncles and aunts",
"title": "Uncles and aunts"
},
{
"language_code": "ru",
"text": "Дяди и тёти",
"title": "Дяди и тёти"
}
]
},
{
"code": "siblings",
"slug": "siblings",
"title": [
{
"language_code": "ar",
"text": "الإخوة",
"title": "الإخوة"
},
{
"language_code": "fa",
"text": "برادران و خواهران",
"title": "برادران و خواهران"
},
{
"language_code": "en",
"text": "Siblings",
"title": "Siblings"
},
{
"language_code": "ru",
"text": "Сиблинги",
"title": "Сиблинги"
}
]
},
{
"code": "parents",
"slug": "parents",
"title": [
{
"language_code": "ar",
"text": "الوالدان",
"title": "الوالدان"
},
{
"language_code": "fa",
"text": "والدین",
"title": "والدین"
},
{
"language_code": "en",
"text": "Parents",
"title": "Parents"
},
{
"language_code": "ru",
"text": "Родители",
"title": "Родители"
}
]
},
{
"code": "nephews_and_nieces",
"slug": "nephews-and-nieces",
"title": [
{
"language_code": "ar",
"text": "أبناء الإخوة",
"title": "أبناء الإخوة"
},
{
"language_code": "fa",
"text": "فرزندان برادر و خواهر",
"title": "فرزندان برادر و خواهر"
},
{
"language_code": "en",
"text": "Nephews and nieces",
"title": "Nephews and nieces"
},
{
"language_code": "ru",
"text": "Племянники",
"title": "Племянники"
}
]
},
{
"code": "children",
"slug": "children",
"title": [
{
"language_code": "ar",
"text": "الأولاد",
"title": "الأولاد"
},
{
"language_code": "fa",
"text": "فرزندان",
"title": "فرزندان"
},
{
"language_code": "en",
"text": "Children",
"title": "Children"
},
{
"language_code": "ru",
"text": "Дети",
"title": "Дети"
}
]
},
{
"code": "stepchildren",
"slug": "stepchildren",
"title": [
{
"language_code": "ar",
"text": "الربائب",
"title": "الربائب"
},
{
"language_code": "fa",
"text": "فرزندان همسر (ربائب)",
"title": "فرزندان همسر (ربائب)"
},
{
"language_code": "en",
"text": "Stepchildren",
"title": "Stepchildren"
},
{
"language_code": "ru",
"text": "Пасынки и падчерицы",
"title": "Пасынки и падчерицы"
}
]
},
{
"code": "spouses",
"slug": "spouses",
"title": [
{
"language_code": "ar",
"text": "الأزواج",
"title": "الأزواج"
},
{
"language_code": "fa",
"text": "همسران",
"title": "همسران"
},
{
"language_code": "en",
"text": "Spouses",
"title": "Spouses"
},
{
"language_code": "ru",
"text": "Супруги",
"title": "Супруги"
}
]
},
{
"code": "cousins",
"slug": "cousins",
"title": [
{
"language_code": "ar",
"text": "أبناء العمومة والخؤولة",
"title": "أبناء العمومة والخؤولة"
},
{
"language_code": "fa",
"text": "فرزندان عمو، عمه، دایی و خاله",
"title": "فرزندان عمو، عمه، دایی و خاله"
},
{
"language_code": "en",
"text": "Cousins",
"title": "Cousins"
},
{
"language_code": "ru",
"text": "Двоюродные братья и сестры",
"title": "Двоюродные братья и сестры"
}
]
},
{
"code": "granduncles_and_grandaunts",
"slug": "granduncles-and-grandaunts",
"title": [
{
"language_code": "ar",
"text": "إخوة الأجداد",
"title": "إخوة الأجداد"
},
{
"language_code": "fa",
"text": "عموها و عمه‌های والدین",
"title": "عموها و عمه‌های والدین"
},
{
"language_code": "en",
"text": "Granduncles and grandaunts",
"title": "Granduncles and grandaunts"
},
{
"language_code": "ru",
"text": "Великие дяди и тети",
"title": "Великие дяди и тети"
}
]
},
{
"code": "grandnephews_and_grandnieces",
"slug": "grandnephews-and-grandnieces",
"title": [
{
"language_code": "ar",
"text": "أحفاد الإخوة",
"title": "أحفاد الإخوة"
},
{
"language_code": "fa",
"text": "نوه‌های برادر و خواهر",
"title": "نوه‌های برادر و خواهر"
},
{
"language_code": "en",
"text": "Grandnephews and grandnieces",
"title": "Grandnephews and grandnieces"
},
{
"language_code": "ru",
"text": "Внучатые племянники",
"title": "Внучатые племянники"
}
]
},
{
"code": "grandchildren_and_great_grandchildren",
"slug": "grandchildren-and-great-grandchildren",
"title": [
{
"language_code": "ar",
"text": "الأحفاد وأبناء الأحفاد",
"title": "الأحفاد وأبناء الأحفاد"
},
{
"language_code": "fa",
"text": "نوه‌ها و نبیره‌ها",
"title": "نوه‌ها و نبیره‌ها"
},
{
"language_code": "en",
"text": "Grandchildren and great-grandchildren",
"title": "Grandchildren and great-grandchildren"
},
{
"language_code": "ru",
"text": "Внуки и правнуки",
"title": "Внуки и правнуки"
}
]
},
{
"code": "grandparents",
"slug": "grandparents",
"title": [
{
"language_code": "ar",
"text": "الأجداد",
"title": "الأجداد"
},
{
"language_code": "fa",
"text": "پدربزرگ و مادربزرگ",
"title": "پدربزرگ و مادربزرگ"
},
{
"language_code": "en",
"text": "Grandparents",
"title": "Grandparents"
},
{
"language_code": "ru",
"text": "Прародители",
"title": "Прародители"
}
]
},
{
"code": "foster_mother",
"slug": "foster-mother",
"title": [
{
"language_code": "ar",
"text": "المرضعة",
"title": "المرضعة"
},
{
"language_code": "fa",
"text": "مادر رضاعی (دایه)",
"title": "مادر رضاعی (دایه)"
},
{
"language_code": "en",
"text": "Foster mother",
"title": "Foster mother"
},
{
"language_code": "ru",
"text": "Молочная мать",
"title": "Молочная мать"
}
]
},
{
"code": "tribe",
"slug": "tribe",
"title": [
{
"language_code": "ar",
"text": "القبيلة",
"title": "القبيلة"
},
{
"language_code": "fa",
"text": "طایفه و قبیله",
"title": "طایفه و قبیله"
},
{
"language_code": "en",
"text": "Tribe",
"title": "Tribe"
},
{
"language_code": "ru",
"text": "Племя",
"title": "Племя"
}
]
},
{
"code": "foster_children",
"slug": "foster-children",
"title": [
{
"language_code": "ar",
"text": "الرضاء",
"title": "الرضاء"
},
{
"language_code": "fa",
"text": "فرزندان رضاعی",
"title": "فرزندان رضاعی"
},
{
"language_code": "en",
"text": "Foster children",
"title": "Foster children"
},
{
"language_code": "ru",
"text": "Молочные дети",
"title": "Молочные дети"
}
]
},
{
"code": "in_laws",
"slug": "in-laws",
"title": [
{
"language_code": "ar",
"text": "الأصهار والأختان",
"title": "الأصهار والأختان"
},
{
"language_code": "fa",
"text": "بستگان سببی (خانواده همسر)",
"title": "بستگان سببی (خانواده همسر)"
},
{
"language_code": "en",
"text": "In-laws",
"title": "In-laws"
},
{
"language_code": "ru",
"text": "Свойственники",
"title": "Свойственники"
}
]
},
{
"code": "students",
"slug": "students",
"title": [
{
"language_code": "ar",
"text": "التلاميذ",
"title": "التلاميذ"
},
{
"language_code": "fa",
"text": "شاگردان",
"title": "شاگردان"
},
{
"language_code": "en",
"text": "Students",
"title": "Students"
},
{
"language_code": "ru",
"text": "Ученики",
"title": "Ученики"
}
]
},
{
"code": "owners_and_masters",
"slug": "owners-and-masters",
"title": [
{
"language_code": "ar",
"text": "الملاك",
"title": "الملاك"
},
{
"language_code": "fa",
"text": "مالکان و اربابان",
"title": "مالکان و اربابان"
},
{
"language_code": "en",
"text": "Owners and masters",
"title": "Owners and masters"
},
{
"language_code": "ru",
"text": "Хозяева и владельцы",
"title": "Хозяева и владельцы"
}
]
},
{
"code": "other_relatives",
"slug": "other-relatives",
"title": [
{
"language_code": "ar",
"text": "الأقارب",
"title": "الأقارب"
},
{
"language_code": "fa",
"text": "سایر خویشاوندان",
"title": "سایر خویشاوندان"
},
{
"language_code": "en",
"text": "Other relatives",
"title": "Other relatives"
},
{
"language_code": "ru",
"text": "Другие родственники",
"title": "Другие родственники"
}
]
},
{
"code": "friends_and_companions",
"slug": "friends-and-companions",
"title": [
{
"language_code": "ar",
"text": "الأصحاب",
"title": "الأصحاب"
},
{
"language_code": "fa",
"text": "دوستان و همراهان",
"title": "دوستان و همراهان"
},
{
"language_code": "en",
"text": "Friends and companions",
"title": "Friends and companions"
},
{
"language_code": "ru",
"text": "Друзья и сподвижники",
"title": "Друзья и сподвижники"
}
]
},
{
"code": "allies",
"slug": "allies",
"title": [
{
"language_code": "ar",
"text": "الحلفاء",
"title": "الحلفاء"
},
{
"language_code": "fa",
"text": "هم‌پیمانان (حلفاء)",
"title": "هم‌پیمانان (حلفاء)"
},
{
"language_code": "en",
"text": "Allies",
"title": "Allies"
},
{
"language_code": "ru",
"text": "Союзники",
"title": "Союзники"
}
]
},
{
"code": "neighbors",
"slug": "neighbors",
"title": [
{
"language_code": "ar",
"text": "الجيران",
"title": "الجيران"
},
{
"language_code": "fa",
"text": "همسایگان",
"title": "همسایگان"
},
{
"language_code": "en",
"text": "Neighbors",
"title": "Neighbors"
},
{
"language_code": "ru",
"text": "Соседи",
"title": "Соседи"
}
]
},
{
"code": "wards_and_proteges",
"slug": "wards-and-proteges",
"title": [
{
"language_code": "ar",
"text": "المحضونون",
"title": "المحضونون"
},
{
"language_code": "fa",
"text": "تحت تکفل و پرورش‌یافتگان",
"title": "تحت تکفل و پرورش‌یافتگان"
},
{
"language_code": "en",
"text": "Wards and proteges",
"title": "Wards and proteges"
},
{
"language_code": "ru",
"text": "Подопечные и воспитанники",
"title": "Подопечные и воспитанники"
}
]
},
{
"code": "cousins_of_parents",
"slug": "cousins-of-parents",
"title": [
{
"language_code": "ar",
"text": "أبناء عمومة الآباء",
"title": "أبناء عمومة الآباء"
},
{
"language_code": "fa",
"text": "فرزندان عمو، عمه، دایی و خاله والدین",
"title": "فرزندان عمو، عمه، دایی و خاله والدین"
},
{
"language_code": "en",
"text": "Cousins of parents",
"title": "Cousins of parents"
},
{
"language_code": "ru",
"text": "Двоюродные дяди и тети",
"title": "Двоюродные дяди и тети"
}
]
},
{
"code": "cousins_of_grandparents",
"slug": "cousins-of-grandparents",
"title": [
{
"language_code": "ar",
"text": "أبناء عمومة الأجداد",
"title": "أبناء عمومة الأجداد"
},
{
"language_code": "fa",
"text": "فرزندان عمو، عمه، دایی و خاله اجداد",
"title": "فرزندان عمو، عمه، دایی و خاله اجداد"
},
{
"language_code": "en",
"text": "Cousins of grandparents",
"title": "Cousins of grandparents"
},
{
"language_code": "ru",
"text": "Двоюродные дедушки и бабушки",
"title": "Двоюродные дедушки и бабушки"
}
]
},
{
"code": "cousins_children",
"slug": "cousins-children",
"title": [
{
"language_code": "ar",
"text": "أبناء أبناء العمومة",
"title": "أبناء أبناء العمومة"
},
{
"language_code": "fa",
"text": "فرزندان عموزاده‌ها و دایی‌زاده‌ها",
"title": "فرزندان عموزاده‌ها و دایی‌زاده‌ها"
},
{
"language_code": "en",
"text": "Cousins children",
"title": "Cousins children"
},
{
"language_code": "ru",
"text": "Двоюродные племянники",
"title": "Двоюродные племянники"
}
]
},
{
"code": "cousins_grandchildren_and_great_grandchildren",
"slug": "cousins-grandchildren-and-great-grandchildren",
"title": [
{
"language_code": "ar",
"text": "أحفاد أبناء العمومة",
"title": "أحفاد أبناء العمومة"
},
{
"language_code": "fa",
"text": "نوه‌های عموزاده‌ها و دایی‌زاده‌ها",
"title": "نوه‌های عموزاده‌ها و دایی‌زاده‌ها"
},
{
"language_code": "en",
"text": "Cousins grandchildren and great-grandchildren",
"title": "Cousins grandchildren and great-grandchildren"
},
{
"language_code": "ru",
"text": "Двоюродные внуки и правнуки",
"title": "Двоюродные внуки и правнуки"
}
]
}
]
def seed_and_migrate_relation_types(apps, schema_editor):
RelationType = apps.get_model('hadis', 'RelationType')
TransmitterRelative = apps.get_model('hadis', 'TransmitterRelative')
rel_type_map = {}
for item in RELATION_TYPE_DATA:
obj, _ = RelationType.objects.get_or_create(
code=item["code"],
defaults={
"slug": item["slug"],
"title": item["title"],
},
)
rel_type_map[item["code"]] = obj
# Link existing TransmitterRelative records
updated_count = 0
for code, rel_obj in rel_type_map.items():
count = TransmitterRelative.objects.filter(relation_type=code).update(relation_type_ref=rel_obj)
updated_count += count
print(f"Linked {updated_count} TransmitterRelative records to RelationType.")
def reverse_seed(apps, schema_editor):
pass
class Migration(migrations.Migration):
dependencies = [
('hadis', '0062_madhhab_and_relations'),
]
operations = [
migrations.CreateModel(
name='RelationType',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('code', models.CharField(blank=True, max_length=100, null=True, unique=True, verbose_name='Code')),
('title', models.JSONField(default=list, verbose_name='Title')),
('slug', models.SlugField(allow_unicode=True, blank=True, max_length=255, unique=True, verbose_name='slug')),
('description', models.JSONField(blank=True, default=list, verbose_name='Description')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated At')),
],
options={
'verbose_name': 'Relation Type',
'verbose_name_plural': 'Relation Types',
'ordering': ['slug', 'id'],
},
),
migrations.AddField(
model_name='transmitterrelative',
name='relation_type_ref',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='relative_links', to='hadis.relationtype', verbose_name='Relation Type Ref'),
),
migrations.RunPython(seed_and_migrate_relation_types, reverse_seed),
]

109
apps/hadis/migrations/0064_transmitter_profession_and_relations.py

@ -0,0 +1,109 @@
# Generated by Django 4.2.30 on 2026-10-21 10:21
from django.db import migrations, models
from django.utils.text import slugify
def populate_professions_from_existing_records(apps, schema_editor):
TransmitterProfession = apps.get_model('hadis', 'TransmitterProfession')
Transmitters = apps.get_model('hadis', 'Transmitters')
profession_by_text = {}
for p in TransmitterProfession.objects.all():
if isinstance(p.title, list):
for entry in p.title:
if isinstance(entry, dict):
txt = (entry.get('text') or entry.get('title') or '').strip().lower()
if txt:
profession_by_text[txt] = p
def get_or_create_profession(json_list):
if not json_list or not isinstance(json_list, list):
return None
cleaned = []
lookup_keys = []
for entry in json_list:
if isinstance(entry, dict):
lang = (entry.get('language_code') or '').strip()
txt = (entry.get('text') or entry.get('title') or '').strip()
if lang and txt:
cleaned.append({'language_code': lang, 'text': txt, 'title': txt})
lookup_keys.append(txt.lower())
if not cleaned:
return None
for key in lookup_keys:
if key in profession_by_text:
existing = profession_by_text[key]
existing_langs = {
item.get('language_code'): item
for item in (existing.title or [])
if isinstance(item, dict) and item.get('language_code')
}
updated = False
for item in cleaned:
if item['language_code'] not in existing_langs:
existing_langs[item['language_code']] = item
updated = True
if updated:
existing.title = list(existing_langs.values())
existing.save(update_fields=['title'])
for k in lookup_keys:
profession_by_text[k] = existing
return existing
first_text = cleaned[0]['text']
slug_val = slugify(first_text, allow_unicode=True)[:240] or 'profession'
prof_obj = TransmitterProfession.objects.create(title=cleaned, slug=slug_val)
for k in lookup_keys:
profession_by_text[k] = prof_obj
return prof_obj
for tr in Transmitters.objects.all().iterator():
source_data = getattr(tr, 'occupation', None) or getattr(tr, 'profession', None)
if source_data and isinstance(source_data, list):
p_obj = get_or_create_profession(source_data)
if p_obj:
tr.professions_list.add(p_obj)
if not tr.profession:
tr.profession = p_obj.title
tr.save(update_fields=['profession'])
class Migration(migrations.Migration):
dependencies = [
('hadis', '0063_relation_type_and_relations'),
]
operations = [
migrations.CreateModel(
name='TransmitterProfession',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('title', models.JSONField(default=list, verbose_name='Title')),
('slug', models.SlugField(allow_unicode=True, blank=True, max_length=255, null=True, verbose_name='slug')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='updated at')),
],
options={
'verbose_name': 'Transmitter Profession',
'verbose_name_plural': 'Transmitter Professions',
'ordering': ('-created_at', '-id'),
},
),
migrations.AddField(
model_name='transmitters',
name='profession',
field=models.JSONField(blank=True, default=list, verbose_name='Profession'),
),
migrations.AddField(
model_name='transmitters',
name='professions_list',
field=models.ManyToManyField(blank=True, related_name='transmitters', to='hadis.transmitterprofession', verbose_name='Professions (M2M)'),
),
migrations.RunPython(
populate_professions_from_existing_records,
reverse_code=migrations.RunPython.noop,
),
]

89
apps/hadis/models/transmitter.py

@ -167,6 +167,38 @@ class City(LowercaseSlugMixin, models.Model):
return None return None
class TransmitterProfession(LowercaseSlugMixin, models.Model):
slug_source_field = 'title'
title = models.JSONField(default=list, verbose_name=_('Title'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True, null=True, allow_unicode=True)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
class Meta:
ordering = ('-created_at', '-id')
verbose_name = _('Transmitter Profession')
verbose_name_plural = _('Transmitter Professions')
def __str__(self):
if self.title and isinstance(self.title, list) and len(self.title) > 0 and isinstance(self.title[0], dict):
return self.title[0].get('text') or self.title[0].get('title') or str(self.id)
return str(self.id)
def get_title(self, lang: str = 'en') -> Optional[str]:
if not self.title or not isinstance(self.title, list):
return None
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '') or tr.get('title', '')
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '') or tr.get('title', '')
if isinstance(self.title[0], dict):
return self.title[0].get('text', '') or self.title[0].get('title', '')
return None
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
super().save(*args, **kwargs) super().save(*args, **kwargs)
try: try:
@ -330,6 +362,41 @@ class Madhhab(LowercaseSlugMixin, models.Model):
return None return None
class RelationType(LowercaseSlugMixin, models.Model):
slug_source_field = 'title'
code = models.CharField(max_length=100, blank=True, null=True, unique=True, verbose_name=_('Code'))
title = models.JSONField(default=list, verbose_name=_('Title'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True, unique=True, allow_unicode=True)
description = models.JSONField(default=list, blank=True, verbose_name=_('Description'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('Created At'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('Updated At'))
class Meta:
verbose_name = _('Relation Type')
verbose_name_plural = _('Relation Types')
ordering = ['slug', 'id']
def __str__(self):
if self.code:
return self.code
if not self.title or not isinstance(self.title, list):
return str(self.id)
for tr in self.title:
if isinstance(tr, dict) and tr.get('language_code') in ('ar', 'fa', 'en'):
return tr.get('text', '') or tr.get('title', '')
return str(self.id)
def get_title(self, lang: str = 'fa') -> Optional[str]:
if not self.title or not isinstance(self.title, list):
return None
for item in self.title:
if isinstance(item, dict) and item.get('language_code') == lang:
return item.get('text', '') or item.get('title', '')
return None
class Transmitters(LowercaseSlugMixin, models.Model): class Transmitters(LowercaseSlugMixin, models.Model):
slug_source_field = 'full_name' slug_source_field = 'full_name'
# class ReliabilityLevel(models.TextChoices): # class ReliabilityLevel(models.TextChoices):
@ -383,6 +450,13 @@ class Transmitters(LowercaseSlugMixin, models.Model):
related_name='transmitters', related_name='transmitters',
verbose_name=_('Features (M2M)') verbose_name=_('Features (M2M)')
) )
profession = models.JSONField(default=list, blank=True, verbose_name=_('Profession'))
professions_list = models.ManyToManyField(
'hadis.TransmitterProfession',
blank=True,
related_name='transmitters',
verbose_name=_('Professions (M2M)')
)
tags = models.JSONField(default=list, blank=True, verbose_name=_('Tags')) tags = models.JSONField(default=list, blank=True, verbose_name=_('Tags'))
# Geographic Information # Geographic Information
@ -1127,6 +1201,14 @@ class TransmitterRelative(models.Model):
choices=RelationTypeChoices.choices, choices=RelationTypeChoices.choices,
verbose_name=_('Relation Type') verbose_name=_('Relation Type')
) )
relation_type_ref = models.ForeignKey(
'hadis.RelationType',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='relative_links',
verbose_name=_('Relation Type Ref')
)
relation_value = models.JSONField(default=list, verbose_name=_('Relation Value')) relation_value = models.JSONField(default=list, verbose_name=_('Relation Value'))
name = models.CharField(max_length=255, verbose_name=_('Name')) name = models.CharField(max_length=255, verbose_name=_('Name'))
narrator = models.ForeignKey( narrator = models.ForeignKey(
@ -1147,6 +1229,13 @@ class TransmitterRelative(models.Model):
ordering = ['relation_type', 'name'] ordering = ['relation_type', 'name']
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.relation_type_ref and not self.relation_type:
self.relation_type = self.relation_type_ref.code or ''
elif self.relation_type and not self.relation_type_ref:
from .transmitter import RelationType
rel_ref = RelationType.objects.filter(code=self.relation_type).first()
if rel_ref:
self.relation_type_ref = rel_ref
if self.relation_type and not self.relation_value: if self.relation_type and not self.relation_value:
self.relation_value = RELATION_TYPE_MAP.get(self.relation_type, []) self.relation_value = RELATION_TYPE_MAP.get(self.relation_type, [])
super().save(*args, **kwargs) super().save(*args, **kwargs)

67
apps/hadis/serializers/serializers_admin.py

@ -10,7 +10,7 @@ from ..models import (
Hadis, HadisCategory, HadisStatus, HadisTag, HadisCorrection, HadisInterpretation, HadisCollection, HadisSect, HadisInCollection, HadisTransmitter, HadisReference, ReferenceImage, Hadis, HadisCategory, HadisStatus, HadisTag, HadisCorrection, HadisInterpretation, HadisCollection, HadisSect, HadisInCollection, HadisTransmitter, HadisReference, ReferenceImage,
BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAuthorRutba, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookResearcher, BookEditor, BookReference, BookReferenceImage, BookReferenceDocument, BookAuthor, BookAuthorRutba, BookAttribute, BookSubjectArea, BookType, BookTag, BookEdition, BookVolume, BookResearcher, BookEditor,
Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, ScholarlyOpinion, TransmitterOpinion, TransmitterOriginalText, Transmitters, TransmitterReliability, NarratorLayer, OpinionStatus, ScholarlyOpinion, TransmitterOpinion, TransmitterOriginalText,
City, Kunya, TransmitterFeature, SahabiStatus, Madhhab, YearValue, NarratorDate,
City, Kunya, TransmitterFeature, TransmitterProfession, SahabiStatus, Madhhab, RelationType, YearValue, NarratorDate,
CorrectionReference, CorrectionReferenceImage, InterpretationReference, InterpretationReferenceImage, CorrectionReference, CorrectionReferenceImage, InterpretationReference, InterpretationReferenceImage,
OriginalTextReference, OriginalTextReferenceImage, OriginalTextReference, OriginalTextReferenceImage,
ContentRelease ContentRelease
@ -2046,6 +2046,15 @@ class AdminTransmitterFeatureSerializer(serializers.ModelSerializer):
read_only_fields = ["id", "slug", "created_at", "updated_at"] read_only_fields = ["id", "slug", "created_at", "updated_at"]
class AdminTransmitterProfessionSerializer(serializers.ModelSerializer):
title = serializers.JSONField()
class Meta:
model = TransmitterProfession
fields = ["id", "title", "slug", "created_at", "updated_at"]
read_only_fields = ["id", "slug", "created_at", "updated_at"]
class AdminSahabiStatusSerializer(serializers.ModelSerializer): class AdminSahabiStatusSerializer(serializers.ModelSerializer):
@ -2060,6 +2069,17 @@ class AdminSahabiStatusSerializer(serializers.ModelSerializer):
class AdminRelationTypeSerializer(serializers.ModelSerializer):
code = serializers.CharField(required=False, allow_blank=True)
title = serializers.JSONField()
description = serializers.JSONField(required=False, default=list)
class Meta:
model = RelationType
fields = ["id", "code", "title", "slug", "description", "created_at", "updated_at"]
read_only_fields = ["id", "slug", "created_at", "updated_at"]
class AdminMadhhabSerializer(serializers.ModelSerializer): class AdminMadhhabSerializer(serializers.ModelSerializer):
title = serializers.JSONField() title = serializers.JSONField()
description = serializers.JSONField(required=False, default=list) description = serializers.JSONField(required=False, default=list)
@ -2084,6 +2104,7 @@ class AdminTransmitterListSerializer(serializers.ModelSerializer):
reliability_detail = AdminTransmitterReliabilitySerializer(source="reliability", read_only=True) reliability_detail = AdminTransmitterReliabilitySerializer(source="reliability", read_only=True)
kunyas_detail = AdminKunyaSerializer(source="kunyas", many=True, read_only=True) kunyas_detail = AdminKunyaSerializer(source="kunyas", many=True, read_only=True)
features_detail = AdminTransmitterFeatureSerializer(source="features_list", many=True, read_only=True) features_detail = AdminTransmitterFeatureSerializer(source="features_list", many=True, read_only=True)
professions_detail = AdminTransmitterProfessionSerializer(source="professions_list", many=True, read_only=True)
sahabi_status_detail = AdminSahabiStatusSerializer(source="sahabi_status", read_only=True) sahabi_status_detail = AdminSahabiStatusSerializer(source="sahabi_status", read_only=True)
madhhabs_detail = AdminMadhhabSerializer(source="madhhabs", many=True, read_only=True) madhhabs_detail = AdminMadhhabSerializer(source="madhhabs", many=True, read_only=True)
full_name = serializers.JSONField() full_name = serializers.JSONField()
@ -2112,6 +2133,7 @@ class AdminTransmitterListSerializer(serializers.ModelSerializer):
"nickname", "nickname",
"kunyas_detail", "kunyas_detail",
"features_detail", "features_detail",
"professions_detail",
"sahabi_status", "sahabi_status",
"sahabi_status_detail", "sahabi_status_detail",
"reliability", "reliability",
@ -2153,6 +2175,13 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
required=False, required=False,
) )
features_detail = AdminTransmitterFeatureSerializer(source="features_list", many=True, read_only=True) features_detail = AdminTransmitterFeatureSerializer(source="features_list", many=True, read_only=True)
profession = serializers.JSONField(required=False, default=list)
professions_list = serializers.PrimaryKeyRelatedField(
queryset=TransmitterProfession.objects.all(),
many=True,
required=False,
)
professions_detail = AdminTransmitterProfessionSerializer(source="professions_list", many=True, read_only=True)
sahabi_status = serializers.PrimaryKeyRelatedField( sahabi_status = serializers.PrimaryKeyRelatedField(
queryset=SahabiStatus.objects.all(), queryset=SahabiStatus.objects.all(),
required=False, required=False,
@ -2223,7 +2252,10 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
"kunyas_detail", "kunyas_detail",
"features", "features",
"features_list", "features_list",
"features_detail", "sahabi_status_detail", "madhhabs_detail",
"features_detail",
"profession",
"professions_list",
"professions_detail", "sahabi_status_detail", "madhhabs_detail",
"sahabi_status", "sahabi_status",
"sahabi_status_detail", "sahabi_status_detail",
"companion_type", "companion_type",
@ -2273,7 +2305,7 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
"reliability_detail", "father_detail", "sons", "reliability_detail", "father_detail", "sons",
"birth_year_preview", "death_year_preview", "age_at_death_preview", "birth_year_preview", "death_year_preview", "age_at_death_preview",
"origin_city_detail", "lived_in_city_detail", "died_in_city_detail", "origin_city_detail", "lived_in_city_detail", "died_in_city_detail",
"kunyas_detail", "features_detail",
"kunyas_detail", "features_detail", "professions_detail",
] ]
def get_birth_year_preview(self, obj): def get_birth_year_preview(self, obj):
@ -2323,6 +2355,7 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
"full_name", "full_name",
"kunya", "kunya",
"features", "features",
"profession",
"known_as", "known_as",
"nickname", "nickname",
"origin", "origin",
@ -2380,6 +2413,14 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
except ValueError: except ValueError:
pass pass
if "professions_list" in data:
val = data["professions_list"]
if isinstance(val, str):
try:
data["professions_list"] = json.loads(val)
except ValueError:
pass
if "madhhabs" in data: if "madhhabs" in data:
val = data["madhhabs"] val = data["madhhabs"]
if isinstance(val, str): if isinstance(val, str):
@ -2416,6 +2457,7 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
sons_ids = validated_data.pop("sons_ids", None) sons_ids = validated_data.pop("sons_ids", None)
kunyas_objs = validated_data.pop("kunyas", None) kunyas_objs = validated_data.pop("kunyas", None)
features_objs = validated_data.pop("features_list", None) features_objs = validated_data.pop("features_list", None)
professions_objs = validated_data.pop("professions_list", None)
madhhabs_objs = validated_data.pop("madhhabs", None) madhhabs_objs = validated_data.pop("madhhabs", None)
instance = super().create(validated_data) instance = super().create(validated_data)
if sons_ids is not None: if sons_ids is not None:
@ -2438,6 +2480,15 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
else: else:
instance.features = [] instance.features = []
instance.save(update_fields=['features']) instance.save(update_fields=['features'])
if professions_objs is not None:
instance.professions_list.set(professions_objs)
if professions_objs:
first_prof = professions_objs[0]
instance.profession = first_prof.title or []
instance.save(update_fields=['profession'])
else:
instance.profession = []
instance.save(update_fields=['profession'])
if "sahabi_status" in validated_data: if "sahabi_status" in validated_data:
st = validated_data.get("sahabi_status") st = validated_data.get("sahabi_status")
instance.companion_type = st.title if st else [] instance.companion_type = st.title if st else []
@ -2460,6 +2511,7 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
sons_ids = validated_data.pop("sons_ids", None) sons_ids = validated_data.pop("sons_ids", None)
kunyas_objs = validated_data.pop("kunyas", None) kunyas_objs = validated_data.pop("kunyas", None)
features_objs = validated_data.pop("features_list", None) features_objs = validated_data.pop("features_list", None)
professions_objs = validated_data.pop("professions_list", None)
madhhabs_objs = validated_data.pop("madhhabs", None) madhhabs_objs = validated_data.pop("madhhabs", None)
if remove_thumbnail and instance.thumbnail: if remove_thumbnail and instance.thumbnail:
instance.thumbnail.delete(save=False) instance.thumbnail.delete(save=False)
@ -2486,6 +2538,15 @@ class AdminTransmitterDetailSerializer(serializers.ModelSerializer):
else: else:
instance.features = [] instance.features = []
instance.save(update_fields=['features']) instance.save(update_fields=['features'])
if professions_objs is not None:
instance.professions_list.set(professions_objs)
if professions_objs:
first_prof = professions_objs[0]
instance.profession = first_prof.title or []
instance.save(update_fields=['profession'])
else:
instance.profession = []
instance.save(update_fields=['profession'])
if "sahabi_status" in validated_data: if "sahabi_status" in validated_data:
st = validated_data.get("sahabi_status") st = validated_data.get("sahabi_status")
instance.companion_type = st.title if st else [] instance.companion_type = st.title if st else []

8
apps/hadis/urls.py

@ -47,8 +47,10 @@ from .views_admin import (
DovodiCityViewSet, DovodiCityViewSet,
DovodiKunyaViewSet, DovodiKunyaViewSet,
DovodiTransmitterFeatureViewSet, DovodiTransmitterFeatureViewSet,
DovodiTransmitterProfessionViewSet,
DovodiSahabiStatusViewSet, DovodiSahabiStatusViewSet,
DovodiMadhhabViewSet
DovodiMadhhabViewSet,
DovodiRelationTypeViewSet
) )
@ -151,10 +153,14 @@ admin_router.register(r'dovodi-kunyas', DovodiKunyaViewSet, basename='dovodi-kun
admin_router.register(r'kunyas', DovodiKunyaViewSet, basename='admin-kunyas') admin_router.register(r'kunyas', DovodiKunyaViewSet, basename='admin-kunyas')
admin_router.register(r'dovodi-transmitter-features', DovodiTransmitterFeatureViewSet, basename='dovodi-transmitter-features') admin_router.register(r'dovodi-transmitter-features', DovodiTransmitterFeatureViewSet, basename='dovodi-transmitter-features')
admin_router.register(r'transmitter-features', DovodiTransmitterFeatureViewSet, basename='admin-transmitter-features') admin_router.register(r'transmitter-features', DovodiTransmitterFeatureViewSet, basename='admin-transmitter-features')
admin_router.register(r'dovodi-transmitter-professions', DovodiTransmitterProfessionViewSet, basename='dovodi-transmitter-professions')
admin_router.register(r'transmitter-professions', DovodiTransmitterProfessionViewSet, basename='admin-transmitter-professions')
admin_router.register(r'dovodi-sahabi-statuses', DovodiSahabiStatusViewSet, basename='dovodi-sahabi-statuses') admin_router.register(r'dovodi-sahabi-statuses', DovodiSahabiStatusViewSet, basename='dovodi-sahabi-statuses')
admin_router.register(r'sahabi-statuses', DovodiSahabiStatusViewSet, basename='admin-sahabi-statuses') admin_router.register(r'sahabi-statuses', DovodiSahabiStatusViewSet, basename='admin-sahabi-statuses')
admin_router.register(r'dovodi-madhhabs', DovodiMadhhabViewSet, basename='dovodi-madhhabs') admin_router.register(r'dovodi-madhhabs', DovodiMadhhabViewSet, basename='dovodi-madhhabs')
admin_router.register(r'madhhabs', DovodiMadhhabViewSet, basename='admin-madhhabs') admin_router.register(r'madhhabs', DovodiMadhhabViewSet, basename='admin-madhhabs')
admin_router.register(r'dovodi-relation-types', DovodiRelationTypeViewSet, basename='dovodi-relation-types')
admin_router.register(r'relation-types', DovodiRelationTypeViewSet, basename='admin-relation-types')

40
apps/hadis/views_admin.py

@ -40,8 +40,10 @@ from .models import (
Kunya, Kunya,
TransmitterFeature, TransmitterFeature,
TransmitterProfession,
SahabiStatus, SahabiStatus,
Madhhab, Madhhab,
RelationType,
) )
@ -131,8 +133,10 @@ from .serializers.serializers_admin import (
AdminKunyaSerializer, AdminKunyaSerializer,
AdminTransmitterFeatureSerializer, AdminTransmitterFeatureSerializer,
AdminTransmitterProfessionSerializer,
AdminSahabiStatusSerializer, AdminSahabiStatusSerializer,
AdminMadhhabSerializer, AdminMadhhabSerializer,
AdminRelationTypeSerializer,
) )
@ -2943,6 +2947,24 @@ class DovodiTransmitterFeatureViewSet(ModelViewSet):
return queryset return queryset
class DovodiTransmitterProfessionViewSet(ModelViewSet):
serializer_class = AdminTransmitterProfessionSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = OptionalPagination
parser_classes = (JSONParser, MultiPartParser, FormParser)
def get_queryset(self):
queryset = TransmitterProfession.objects.all().order_by("slug")
search_query = self.request.query_params.get("search")
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query)
| Q(slug__icontains=search_query)
)
return queryset
class DovodiSahabiStatusViewSet(ModelViewSet): class DovodiSahabiStatusViewSet(ModelViewSet):
serializer_class = AdminSahabiStatusSerializer serializer_class = AdminSahabiStatusSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin] permission_classes = [IsAuthenticated, IsSuperAdmin]
@ -2977,3 +2999,21 @@ class DovodiMadhhabViewSet(ModelViewSet):
| Q(slug__icontains=search_query) | Q(slug__icontains=search_query)
) )
return queryset return queryset
class DovodiRelationTypeViewSet(ModelViewSet):
serializer_class = AdminRelationTypeSerializer
permission_classes = [IsAuthenticated, IsSuperAdmin]
authentication_classes = [TokenAuthentication]
pagination_class = OptionalPagination
parser_classes = (JSONParser, MultiPartParser, FormParser)
def get_queryset(self):
queryset = RelationType.objects.all().order_by("slug", "id")
search_query = self.request.query_params.get("search")
if search_query:
queryset = queryset.filter(
Q(title__icontains=search_query)
| Q(slug__icontains=search_query)
| Q(code__icontains=search_query)
)
return queryset
Loading…
Cancel
Save