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 class NarratorLayer(models.Model): """ 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, models.Model): title = models.JSONField(default = list , verbose_name=_('Title')) slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,null=True) def save(self, *args, **kwargs): if not self.slug or (isinstance(self.slug, str) and self.slug.strip() == ''): # Try to get text from title field with robust error handling try: if self.title and isinstance(self.title, list) and len(self.title) > 0: first_item = self.title[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 TransmitterReliability.objects.filter(slug=slug).exclude(pk=self.pk).exists(): slug = f"{base_slug}-{counter}" counter += 1 self.slug = slug else: # Fallback to a timestamp-based slug import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"reliability-{suffix}" counter = 1 while TransmitterReliability.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"reliability-{suffix}-{counter}" counter += 1 self.slug = base_slug else: import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"reliability-{suffix}" counter = 1 while TransmitterReliability.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"reliability-{suffix}-{counter}" counter += 1 self.slug = base_slug else: import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"reliability-{suffix}" counter = 1 while TransmitterReliability.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"reliability-{suffix}-{counter}" counter += 1 self.slug = base_slug except (IndexError, KeyError, AttributeError, TypeError): import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"reliability-{suffix}" counter = 1 while TransmitterReliability.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"reliability-{suffix}-{counter}" counter += 1 self.slug = base_slug super().save(*args, **kwargs) 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(models.Model): # 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) # 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.IntegerField(verbose_name=_("Birth Year (Hijri)"), null=True, blank=True) death_year_hijri = models.IntegerField(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) # 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 save(self, *args, **kwargs): if not self.slug or (isinstance(self.slug, str) and self.slug.strip() == ''): # Try to get text from full_name field with robust error handling try: if self.full_name and isinstance(self.full_name, list) and len(self.full_name) > 0: first_item = self.full_name[0] if isinstance(first_item, dict): name_text = first_item.get('text', '').strip() if name_text: base_slug = slugify(name_text, allow_unicode=True) slug = base_slug counter = 1 while Transmitters.objects.filter(slug=slug).exclude(pk=self.pk).exists(): slug = f"{base_slug}-{counter}" counter += 1 self.slug = slug else: # Fallback if text is empty import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"transmitter-{suffix}" counter = 1 while Transmitters.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"transmitter-{suffix}-{counter}" counter += 1 self.slug = base_slug else: # Fallback if structure is invalid import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"transmitter-{suffix}" counter = 1 while Transmitters.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"transmitter-{suffix}-{counter}" counter += 1 self.slug = base_slug else: # Fallback if full_name is empty or invalid import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"transmitter-{suffix}" counter = 1 while Transmitters.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"transmitter-{suffix}-{counter}" counter += 1 self.slug = base_slug except (IndexError, KeyError, AttributeError, TypeError): # Fallback on any error import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"transmitter-{suffix}" counter = 1 while Transmitters.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"transmitter-{suffix}-{counter}" counter += 1 self.slug = base_slug super().save(*args, **kwargs) 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' ) 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', 'order') unique_together = ('hadis', 'transmitter', 'order') def __str__(self): layer_info = f" - {self.narrator_layer}" if self.narrator_layer else "" return f'{self.hadis.number} - {self.transmitter} ({self.order}){layer_info}' class OpinionStatus(ColorPaletteMixin, models.Model): title = models.JSONField(default = list , verbose_name=_('Title')) slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,null=True) def save(self, *args, **kwargs): if not self.slug or (isinstance(self.slug, str) and self.slug.strip() == ''): # Try to get text from title field with robust error handling try: if self.title and isinstance(self.title, list) and len(self.title) > 0: first_item = self.title[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 OpinionStatus.objects.filter(slug=slug).exclude(pk=self.pk).exists(): slug = f"{base_slug}-{counter}" counter += 1 self.slug = slug else: # Fallback to a timestamp-based slug import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"opinion-{suffix}" counter = 1 while OpinionStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"opinion-{suffix}-{counter}" counter += 1 self.slug = base_slug else: import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"opinion-{suffix}" counter = 1 while OpinionStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"opinion-{suffix}-{counter}" counter += 1 self.slug = base_slug else: import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"opinion-{suffix}" counter = 1 while OpinionStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"opinion-{suffix}-{counter}" counter += 1 self.slug = base_slug except (IndexError, KeyError, AttributeError, TypeError): import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"opinion-{suffix}" counter = 1 while OpinionStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"opinion-{suffix}-{counter}" counter += 1 self.slug = base_slug super().save(*args, **kwargs) 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_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 ) 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 = ('-created_at',) 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) class Meta: indexes = [ # For filtering by transmitter + ordering models.Index(fields=['transmitter']), ] verbose_name = _('Transmitter Original Text') verbose_name_plural = _('Transmitter Original Text') 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. """ # Generate slug if not already set if not self.slug and self.title: # Extract title text title_text = None if isinstance(self.title, list) and self.title: first_item = self.title[0] if isinstance(first_item, dict): title_text = first_item.get("text") # Generate smart slug if title_text: self.slug = generate_smart_slug( text=title_text, model_class=TransmitterOriginalText, max_length=100, # ← Adjust max length here keep_words=8, # ← Limit to 8 words (your requirement) instance=self, ) else: # Fallback if title is empty - use timestamp for uniqueness import time suffix = int(time.time() * 1000) % 1000000 transmitter_slug = self.transmitter.slug if self.transmitter and self.transmitter.slug else 'unknown' base_slug = f"original-text-{transmitter_slug}-{suffix}" # Ensure uniqueness counter = 1 while TransmitterOriginalText.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"original-text-{transmitter_slug}-{suffix}-{counter}" counter += 1 self.slug = base_slug # 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