from tabnanny import verbose from django.db import models from django.utils.translation import gettext_lazy as _ from django.utils.text import slugify from typing import Optional from utils.slug import generate_smart_slug from django.conf import settings from utils.mixins import ColorPaletteMixin, LowercaseSlugMixin class NarratorLayer(LowercaseSlugMixin, models.Model): slug_source_field = 'name' """ Model for narrator layers/classes (Tabaqat) Represents the classification level of narrators in hadis chains """ name = models.JSONField(default = list , verbose_name=_('Name')) number = models.PositiveIntegerField(verbose_name=_('layer number'), unique=True) description = models.JSONField(default = list , verbose_name=_('Description')) slug = models.SlugField(max_length=255,unique=True, verbose_name=_('slug'), blank=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: verbose_name = _('Narrator Layer') verbose_name_plural = _('Narrator Layers') ordering = ['number'] def __str__(self): name = self.name[0]['text'] if self.name else f"Layer {self.number}" return f"{_('Layer')} {self.number} - {name}" def get_description(self,lang): """ Get title for a specific language """ if not self.description or not isinstance(self.description, list): return None for tr in self.description: if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('text', '') for tr in self.description: if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '') return None def get_name(self,lang): """ Get title for a specific language """ if not self.name or not isinstance(self.name, list): return None for tr in self.name: if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('text', '') for tr in self.name: if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '') return None def save(self, *args, **kwargs): if not self.slug or (isinstance(self.slug, str) and self.slug.strip() == ''): # Try to get text from name field with robust error handling try: if self.name and isinstance(self.name, list) and len(self.name) > 0: first_item = self.name[0] if isinstance(first_item, dict): text = first_item.get('text', '').strip() if text: slug = slugify(text) # Ensure uniqueness counter = 1 base_slug = slug while NarratorLayer.objects.filter(slug=slug).exclude(pk=self.pk).exists(): slug = f"{base_slug}-{counter}" counter += 1 self.slug = slug else: # Fallback to layer number if text is empty base_slug = f"layer-{self.number}" slug = base_slug counter = 1 while NarratorLayer.objects.filter(slug=slug).exclude(pk=self.pk).exists(): slug = f"{base_slug}-{counter}" counter += 1 self.slug = slug else: # Fallback to layer number if name structure is invalid base_slug = f"layer-{self.number}" slug = base_slug counter = 1 while NarratorLayer.objects.filter(slug=slug).exclude(pk=self.pk).exists(): slug = f"{base_slug}-{counter}" counter += 1 self.slug = slug else: # Fallback to layer number if name structure is invalid base_slug = f"layer-{self.number}" slug = base_slug counter = 1 while NarratorLayer.objects.filter(slug=slug).exclude(pk=self.pk).exists(): slug = f"{base_slug}-{counter}" counter += 1 self.slug = slug except (IndexError, KeyError, AttributeError, TypeError): # Fallback to layer number on any error base_slug = f"layer-{self.number}" slug = base_slug counter = 1 while NarratorLayer.objects.filter(slug=slug).exclude(pk=self.pk).exists(): slug = f"{base_slug}-{counter}" counter += 1 self.slug = slug super().save(*args, **kwargs) class TransmitterReliability(ColorPaletteMixin, 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) def __str__(self): return self.title[0]['text'] if self.title else str(self.id) def get_title(self,lang): """ Get title for a specific language """ 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', '') for tr in self.title: if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '') return None class Meta: verbose_name = _('Transmitter Reliability') verbose_name_plural = _('Transmitter Reliabilities') class Transmitters(LowercaseSlugMixin, models.Model): slug_source_field = 'full_name' # class ReliabilityLevel(models.TextChoices): # VERY_RELIABLE = 'very_reliable', _('Very Reliable') # RELIABLE = 'reliable', _('Reliable') # ACCEPTABLE = 'acceptable', _('Acceptable') # WEAK = 'weak', _('Weak') # VERY_WEAK = 'very_weak', _('Very Weak') # UNKNOWN = 'unknown', _('Unknown') class MadhhabChoices(models.TextChoices): SHIA = 'shia', _('Shia') SUNNI = 'sunni', _('Sunni') # HANAFI = 'hanafi', _('Hanafi') # MALIKI = 'maliki', _('Maliki') # SHAFII = 'shafii', _('Shafi\'i') # HANBALI = 'hanbali', _('Hanbali') OTHER = 'other', _('Other') UNKNOWN = 'unknown', _('Unknown') # Basic Information full_name = models.JSONField(default = list , verbose_name=_('Full Name')) kunya = models.JSONField(default = list , verbose_name=_('Kunya')) known_as = models.JSONField(default = list , verbose_name=_('Known as')) nickname = models.JSONField(default = list , verbose_name=_('Nick Name')) slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True) legacy_number = models.PositiveIntegerField(unique=True, null=True, blank=True, db_index=True) legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True) relatives_raw = models.JSONField(default=dict, blank=True, verbose_name=_('Raw Relatives Data')) father = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children_as_father', verbose_name=_('Father')) mother = models.ForeignKey('self', on_delete=models.SET_NULL, null=True, blank=True, related_name='children_as_mother', verbose_name=_('Mother')) # ManyToMany for siblings allows bi-directional sibling relationships siblings = models.ManyToManyField('self', blank=True, verbose_name=_('Siblings')) freed_slave_of = models.JSONField(default=list, blank=True, verbose_name=_('Freed Slave Of')) occupation = models.JSONField(default=list, blank=True, verbose_name=_('Occupation')) features = models.JSONField(default=list, blank=True, verbose_name=_('Features')) tags = models.JSONField(default=list, blank=True, verbose_name=_('Tags')) # Geographic Information origin = models.JSONField(default = list , verbose_name=_('Origin')) lived_in = models.JSONField(default = list , verbose_name=_('Lived in')) died_in = models.JSONField(default = list , verbose_name=_('Died in')) # Date Information birth_year_hijri = models.CharField(max_length=50, verbose_name=_("Birth Year (Hijri)"), null=True, blank=True) death_year_hijri = models.CharField(max_length=50, verbose_name=_("Death Year (Hijri)"), null=True, blank=True) age_at_death = models.PositiveIntegerField(verbose_name=_('Age at Death'), blank=True, null=True) generation = models.PositiveIntegerField(verbose_name=_('Generation'), blank=True, null=True) # New fields tadlis = models.BooleanField(default=False, verbose_name=_('Tadlis')) ikhtilat = models.BooleanField(default=False, verbose_name=_('Ikhtilat')) companion_type = models.CharField(max_length=255, null=True, blank=True, verbose_name=_('Companion Type')) # Religious & Academic Information reliability = models.ForeignKey( TransmitterReliability, on_delete=models.CASCADE, verbose_name=_('reliability'), related_name='transmitters', default=12 # ID of 'Unknown' reliability ) madhhab = models.CharField( max_length=20, choices=MadhhabChoices.choices, default=MadhhabChoices.UNKNOWN, verbose_name=_('Madhhab/School of Thought') ) # Presence in Famous Collections in_sahih_muslim = models.BooleanField( default=False, verbose_name=_('In Sahih Muslim'), help_text=_('Is this narrator present in Sahih Muslim?') ) in_sahih_bukhari = models.BooleanField( default=False, verbose_name=_('In Sahih Bukhari'), help_text=_('Is this narrator present in Sahih Bukhari?') ) # Additional Information description = models.JSONField(default = list , verbose_name=_('Description')) thumbnail = models.ImageField(upload_to='hadis/transmitter_thumbnails/', null=True, blank=True, help_text=_('image allowed')) 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: indexes = [ # For ordering in sync API models.Index(fields=['id']), ] verbose_name = _('Transmitter') verbose_name_plural = _('Transmitters') ordering = ('full_name',) @property def share_link(self): if self.slug: return f"{settings.DOVODI_DOMAIN}/arguments/narrators/{self.slug}" return None def _get_json_field(self, field_name: str, lang: Optional[str]=None , fallback: str = "en"): """ Generic getter for JSONField in our [{text, language_code}] format. Usage: self._get_json_field('title', 'fa') """ if lang is None: lang = fallback value = getattr(self, field_name, None) if not value or not isinstance(value, list): return None # 1) exact language for item in value: if isinstance(item, dict) and item.get("language_code") == lang: return item.get("text", "") # 2) fallback language if fallback and fallback != lang: for item in value: if isinstance(item, dict) and item.get("language_code") == fallback: return item.get("text", "") # 3) first available item = value[0] print(item) return item.get("text", "") if isinstance(item, dict) else None def get_full_name(self, lang): return self._get_json_field("full_name" , lang) def get_kunya(self, lang): return self._get_json_field("kunya" , lang) def get_nickname(self, lang): return self._get_json_field("nickname" , lang) def get_origin(self, lang): return self._get_json_field("origin" , lang) def get_lived_in(self,lang): return self._get_json_field("lived_in" , lang) def get_died_in(self, lang): return self._get_json_field("died_in" , lang) def get_description(self, lang): return self._get_json_field("description" , lang) def __str__(self): if self.full_name and len(self.full_name) > 0: return self.full_name[0].get('text', str(self.id)) return str(self.id) class HadisTransmitter(models.Model): hadis = models.ForeignKey( "hadis.Hadis", on_delete=models.CASCADE, verbose_name=_('hadis'), related_name='transmitters' ) transmitter = models.ForeignKey( Transmitters, on_delete=models.CASCADE, verbose_name=_('transmitter'), related_name='hadises' ) chain_index = models.PositiveIntegerField( default=0, verbose_name=_('Chain Index'), help_text=_('Which chain/branch this belongs to (0, 1, 2...)') ) narrator_layer = models.ForeignKey( NarratorLayer, on_delete=models.SET_NULL, verbose_name=_('narrator layer'), related_name='transmitters', null=True, blank=True, help_text=_('The layer/class (Tabaqah) this narrator belongs to') ) status = models.ForeignKey( TransmitterReliability, on_delete=models.SET_NULL, verbose_name=_('reliability status'), related_name='hadis_transmitters', null=True, blank=True, help_text=_('Reliability status of the narrator') ) order = models.PositiveIntegerField( default=0, verbose_name=_('Order'), help_text=_('Order in the chain of transmission') ) is_gap = models.BooleanField(default=False, verbose_name=_('is gap')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) class Meta: indexes = [ # Speeds up fetching transmitters for a specific hadis in order models.Index(fields=['hadis', 'order']), ] verbose_name = _('Hadis Transmitter') verbose_name_plural = _('Hadis Transmitters') ordering = ('hadis', 'chain_index', 'order') # Update ordering # 2. UPDATE UNIQUE TOGETHER # A specific position in a specific chain of a specific hadith can only have one narrator unique_together = ('hadis', 'chain_index', 'order') def __str__(self): return f'{self.hadis.number} - Chain {self.chain_index} - {self.transmitter} (Pos: {self.order})' class OpinionStatus(ColorPaletteMixin, 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) def __str__(self): return self.title[0]['text'] if self.title else str(self.id) def get_title(self,lang): """ Get title for a specific language """ 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', '') for tr in self.title: if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '') return None class Meta: verbose_name = _('Opinion Status') verbose_name_plural = _('Opinion Statuses') class TransmitterOpinion(models.Model): """ Model for scholarly opinions about transmitters """ # class OpinionStatus(models.TextChoices): # CONFIRMED = 'confirmed', _('Confirmed') # MIXED = 'mixed', _('Mixed') # REJECTED = 'rejected', _('Rejected') transmitter = models.ForeignKey( Transmitters, on_delete=models.CASCADE, verbose_name=_('transmitter'), related_name='opinions' ) scholar = models.ForeignKey( 'hadis.BookAuthor', on_delete=models.SET_NULL, verbose_name=_('Scholar'), related_name='transmitter_opinions', null=True, blank=True ) scholar_name = models.JSONField(default = list , verbose_name=_('Scholar Name')) opinion_text = models.JSONField(default = list , verbose_name=_('Opinion Text')) status = models.ForeignKey( OpinionStatus, on_delete=models.CASCADE, verbose_name=_('opinion status'), related_name='opinions', # default=1, blank = True, null=True ) order = models.IntegerField(default=0, verbose_name=_('order')) 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: indexes = [ # For filtering by transmitter + ordering models.Index(fields=['transmitter']), ] verbose_name = _('Transmitter Opinion') verbose_name_plural = _('Transmitter Opinions') ordering = ('order', 'id') def __str__(self): scholar = self.scholar_name[0]['text'] if self.scholar_name else "Unknown Scholar" return f"{scholar}'s opinion on {self.transmitter} ({self.status})" def get_scholar_name(self,lang): """ Get title for a specific language """ if not self.scholar_name or not isinstance(self.scholar_name, list): return None for tr in self.scholar_name: if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('text', '') for tr in self.scholar_name: if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '') return None def get_opinion_tex(self,lang): """ Get title for a specific language """ if not self.opinion_text or not isinstance(self.opinion_text, list): return None for tr in self.opinion_text: if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('text', '') for tr in self.opinion_text: if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '') return None class TransmitterOriginalText(models.Model): transmitter = models.ForeignKey( Transmitters, on_delete=models.CASCADE, verbose_name=_('transmitter'), related_name='originaltexts' ) slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True) title = models.JSONField(default = list , verbose_name=_('Title')) text = models.JSONField(default = list , verbose_name=_('Text')) translation = models.JSONField(verbose_name=_('translation'), default=list) share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True) embedded_in = models.JSONField(default=list, blank=True) order = models.IntegerField(default=0, verbose_name=_('order')) class Meta: indexes = [ # For filtering by transmitter + ordering models.Index(fields=['transmitter']), ] verbose_name = _('Transmitter Original Text') verbose_name_plural = _('Transmitter Original Text') ordering = ('order', 'id') def __str__(self): title = self.title[0]['text'] if self.title else "Untitled" return f"{title} by {self.transmitter}" def save(self, *args, **kwargs): """ Override save to automatically generate smart slugs. """ from utils.slug import build_slug_for_instance build_slug_for_instance(self, source_field='title') # Generate/update share_link before saving if self.slug and self.transmitter and self.transmitter.slug: self.share_link = f"{settings.DOVODI_DOMAIN}/arguments/narrators/{self.transmitter.slug}/original-texts/{self.slug}" # Reset embedded_in if text or translation changes if self.pk: old_instance = TransmitterOriginalText.objects.get(pk=self.pk) if (old_instance.text != self.text or old_instance.translation != self.translation): self.embedded_in = [] # Reset! super().save(*args, **kwargs) def get_title(self,lang): """ Get title for a specific language """ 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', '') for tr in self.title: if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '') return None def get_opinion_tex(self,lang): """ Get title for a specific language """ if not self.opinion_text or not isinstance(self.opinion_text, list): return None for tr in self.opinion_text: if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('text', '') for tr in self.opinion_text: if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '') return None from .reference import BookReference class OriginalTextReference(models.Model): """Links an Original Text (Excerpt) to a specific Book Edition""" original_text = models.ForeignKey( TransmitterOriginalText, on_delete=models.CASCADE, related_name='references' ) book_reference = models.ForeignKey('hadis.BookReference', on_delete=models.SET_NULL, null=True, blank=True) # 👇 فیلدهای جدید برای هماهنگی با سیستم Edition و Volume edition = models.ForeignKey('hadis.BookEdition', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Edition')) book_volume = models.ForeignKey('hadis.BookVolume', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Volume Record')) # فیلدهای متنی قدیمی برای سادگی یا مواردی که هنوز ولوم ساخته نشده است volume = models.CharField(max_length=100, null=True, blank=True, verbose_name=_('Volume Text')) pages = models.CharField(max_length=100, null=True, blank=True) # 👈 تغییر page به pages address = models.TextField(null=True, blank=True, verbose_name=_('Address')) url = models.URLField(max_length=1000, null=True, blank=True) # 👈 مچ کردن طول با بقیه مدل‌ها (1000) class OriginalTextReferenceImage(models.Model): """Stores the screenshots for a specific Original Text Book Reference""" reference = models.ForeignKey(OriginalTextReference, related_name='images', on_delete=models.CASCADE) image = models.ImageField(upload_to='hadis/original_text_images/') priority = models.IntegerField(default=0) RELATION_TYPE_MAP = { 'stepfather': [{'language_code': 'ru', 'text': 'Отчим'}, {'language_code': 'ar', 'text': 'زوج الأم'}, {'language_code': 'en', 'text': 'Stepfather'}], 'stepmother': [{'language_code': 'ru', 'text': 'Мачеха'}, {'language_code': 'ar', 'text': 'زوجة الأب'}, {'language_code': 'en', 'text': 'Stepmother'}], 'uncles_and_aunts': [{'language_code': 'ru', 'text': 'Дяди и тёти'}, {'language_code': 'ar', 'text': 'الأعمام والعمات والأخوال والخالات'}, {'language_code': 'en', 'text': 'Uncles and aunts'}], 'siblings': [{'language_code': 'ru', 'text': 'Сиблинги'}, {'language_code': 'ar', 'text': 'الإخوة'}, {'language_code': 'en', 'text': 'Siblings'}], 'parents': [{'language_code': 'ru', 'text': 'Родители'}, {'language_code': 'ar', 'text': 'الوالدان'}, {'language_code': 'en', 'text': 'Parents'}], 'nephews_and_nieces': [{'language_code': 'ru', 'text': 'Племянники'}, {'language_code': 'ar', 'text': 'أبناء الإخوة'}, {'language_code': 'en', 'text': 'Nephews and nieces'}], 'children': [{'language_code': 'ru', 'text': 'Дети'}, {'language_code': 'ar', 'text': 'الأولاد'}, {'language_code': 'en', 'text': 'Children'}], 'stepchildren': [{'language_code': 'ru', 'text': 'Пасынки и падчерицы'}, {'language_code': 'ar', 'text': 'الربائب'}, {'language_code': 'en', 'text': 'Stepchildren'}], 'spouses': [{'language_code': 'ru', 'text': 'Супруги'}, {'language_code': 'ar', 'text': 'الأزواج'}, {'language_code': 'en', 'text': 'Spouses'}], 'cousins': [{'language_code': 'ru', 'text': 'Двоюродные братья и сестры'}, {'language_code': 'ar', 'text': 'أبناء العمومة والخؤولة'}, {'language_code': 'en', 'text': 'Cousins'}], 'granduncles_and_grandaunts': [{'language_code': 'ru', 'text': 'Великие дяди и тети'}, {'language_code': 'ar', 'text': 'إخوة الأجداد'}, {'language_code': 'en', 'text': 'Granduncles and grandaunts'}], 'grandnephews_and_grandnieces': [{'language_code': 'ru', 'text': 'Внучатые племянники'}, {'language_code': 'ar', 'text': 'أحفاد الإخوة'}, {'language_code': 'en', 'text': 'Grandnephews and grandnieces'}], 'grandchildren_and_great_grandchildren': [{'language_code': 'ru', 'text': 'Внуки и правнуки'}, {'language_code': 'ar', 'text': 'الأحفاد وأبناء الأحفاد'}, {'language_code': 'en', 'text': 'Grandchildren and great-grandchildren'}], 'grandparents': [{'language_code': 'ru', 'text': 'Прародители'}, {'language_code': 'ar', 'text': 'الأجداد'}, {'language_code': 'en', 'text': 'Grandparents'}], 'foster_mother': [{'language_code': 'ru', 'text': 'Молочная мать'}, {'language_code': 'ar', 'text': 'المرضعة'}, {'language_code': 'en', 'text': 'Foster mother'}], 'tribe': [{'language_code': 'ru', 'text': 'Племя'}, {'language_code': 'ar', 'text': 'القبيلة'}, {'language_code': 'en', 'text': 'Tribe'}], 'foster_children': [{'language_code': 'ru', 'text': 'Молочные дети'}, {'language_code': 'ar', 'text': 'الرضاء'}, {'language_code': 'en', 'text': 'Foster children'}], 'in_laws': [{'language_code': 'ru', 'text': 'Свойственники'}, {'language_code': 'ar', 'text': 'الأصهار والأختان'}, {'language_code': 'en', 'text': 'In-laws'}], 'students': [{'language_code': 'ru', 'text': 'Ученики'}, {'language_code': 'ar', 'text': 'التلاميذ'}, {'language_code': 'en', 'text': 'Students'}], 'owners_and_masters': [{'language_code': 'ru', 'text': 'Хозяева и владельцы'}, {'language_code': 'ar', 'text': 'الملاك'}, {'language_code': 'en', 'text': 'Owners and masters'}], 'other_relatives': [{'language_code': 'ru', 'text': 'Другие родственники'}, {'language_code': 'ar', 'text': 'الأقارب'}, {'language_code': 'en', 'text': 'Other relatives'}], 'friends_and_companions': [{'language_code': 'ru', 'text': 'Друзья и сподвижники'}, {'language_code': 'ar', 'text': 'الأصحاب'}, {'language_code': 'en', 'text': 'Friends and companions'}], 'allies': [{'language_code': 'ru', 'text': 'Союзники'}, {'language_code': 'ar', 'text': 'الحلفاء'}, {'language_code': 'en', 'text': 'Allies'}], 'neighbors': [{'language_code': 'ru', 'text': 'Соседи'}, {'language_code': 'ar', 'text': 'الجيران'}, {'language_code': 'en', 'text': 'Neighbors'}], 'wards_and_proteges': [{'language_code': 'ru', 'text': 'Подопечные и воспитанники'}, {'language_code': 'ar', 'text': 'المحضونون'}, {'language_code': 'en', 'text': 'Wards and proteges'}], 'cousins_of_parents': [{'language_code': 'ru', 'text': 'Двоюродные дяди и тети'}, {'language_code': 'ar', 'text': 'أبناء عمومة الآباء'}, {'language_code': 'en', 'text': 'Cousins of parents'}], 'cousins_of_grandparents': [{'language_code': 'ru', 'text': 'Двоюродные дедушки и бабушки'}, {'language_code': 'ar', 'text': 'أبناء عمومة الأجداد'}, {'language_code': 'en', 'text': 'Cousins of grandparents'}], 'cousins_children': [{'language_code': 'ru', 'text': 'Двоюродные племянники'}, {'language_code': 'ar', 'text': 'أبناء أبناء العمومة'}, {'language_code': 'en', 'text': 'Cousins children'}], 'cousins_grandchildren_and_great_grandchildren': [{'language_code': 'ru', 'text': 'Двоюродные внуки и правнуки'}, {'language_code': 'ar', 'text': 'أحفاد أبناء العمومة'}, {'language_code': 'en', 'text': 'Cousins grandchildren and great-grandchildren'}], } class TransmitterRelative(models.Model): class RelationTypeChoices(models.TextChoices): STEPFATHER = 'stepfather', _('Stepfather') STEPMOTHER = 'stepmother', _('Stepmother') UNCLES_AND_AUNTS = 'uncles_and_aunts', _('Uncles and aunts') SIBLINGS = 'siblings', _('Siblings') PARENTS = 'parents', _('Parents') NEPHEWS_AND_NIECES = 'nephews_and_nieces', _('Nephews and nieces') CHILDREN = 'children', _('Children') STEPCHILDREN = 'stepchildren', _('Stepchildren') SPOUSES = 'spouses', _('Spouses') COUSINS = 'cousins', _('Cousins') GRANDUNCLES_AND_GRANDAUNTS = 'granduncles_and_grandaunts', _('Granduncles and grandaunts') GRANDNEPHEWS_AND_GRANDNIECES = 'grandnephews_and_grandnieces', _('Grandnephews and grandnieces') GRANDCHILDREN_AND_GREAT_GRANDCHILDREN = 'grandchildren_and_great_grandchildren', _('Grandchildren and great-grandchildren') GRANDPARENTS = 'grandparents', _('Grandparents') FOSTER_MOTHER = 'foster_mother', _('Foster mother') TRIBE = 'tribe', _('Tribe') FOSTER_CHILDREN = 'foster_children', _('Foster children') IN_LAWS = 'in_laws', _('In-laws') STUDENTS = 'students', _('Students') OWNERS_AND_MASTERS = 'owners_and_masters', _('Owners and masters') OTHER_RELATIVES = 'other_relatives', _('Other relatives') FRIENDS_AND_COMPANIONS = 'friends_and_companions', _('Friends and companions') ALLIES = 'allies', _('Allies') NEIGHBORS = 'neighbors', _('Neighbors') WARDS_AND_PROTEGES = 'wards_and_proteges', _('Wards and proteges') COUSINS_OF_PARENTS = 'cousins_of_parents', _('Cousins of parents') COUSINS_OF_GRANDPARENTS = 'cousins_of_grandparents', _('Cousins of grandparents') COUSINS_CHILDREN = 'cousins_children', _('Cousins children') COUSINS_GRANDCHILDREN_AND_GREAT_GRANDCHILDREN = 'cousins_grandchildren_and_great_grandchildren', _('Cousins grandchildren and great-grandchildren') related_to = models.ForeignKey( Transmitters, on_delete=models.CASCADE, verbose_name=_('Related To'), related_name='relatives' ) relation_type = models.CharField( max_length=50, choices=RelationTypeChoices.choices, verbose_name=_('Relation Type') ) relation_value = models.JSONField(default=list, verbose_name=_('Relation Value')) name = models.CharField(max_length=255, verbose_name=_('Name')) narrator = models.ForeignKey( Transmitters, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Narrator'), related_name='relative_of' ) 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 = _('Transmitter Relative') verbose_name_plural = _('Transmitter Relatives') ordering = ['relation_type', 'name'] def save(self, *args, **kwargs): if self.relation_type and not self.relation_value: self.relation_value = RELATION_TYPE_MAP.get(self.relation_type, []) super().save(*args, **kwargs) def __str__(self): return f"{self.name} ({self.get_relation_type_display()} of {self.related_to})"