from django.db import models from django.utils.translation import gettext_lazy as _ from django.utils.text import slugify from django.conf import settings from typing import Optional class BookSubjectArea(models.Model): title = models.JSONField(default = list , verbose_name=_('Title')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) 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 BookType(models.Model): title = models.JSONField(default = list , verbose_name=_('Title')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) 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 BookReference(models.Model): """ Model for hadis book references with detailed information This is different from library books - these are reference books for hadis """ legacy_id = models.CharField(max_length=255, unique=True, null=True, blank=True, db_index=True) source_url = models.URLField(max_length=1000, null=True, blank=True, verbose_name=_('Source URL')) title = models.JSONField(default = list , verbose_name=_('Title')) description = models.JSONField(default = list , verbose_name=_('Description')) language = models.JSONField(default = list , verbose_name=_('Language')) isbn = models.CharField(max_length=100, verbose_name=_('ISBN'), blank=True, null=True) volume = models.CharField(max_length=100, verbose_name=_('volume'), blank=True, null=True) number_of_volumes = models.PositiveIntegerField(verbose_name=_('Total Number of Volumes'), null=True, blank=True) year_of_publication = models.CharField(max_length=50, verbose_name=_('year of publication'), blank=True, null=True) number_page = models.PositiveIntegerField(verbose_name=_('number of pages'), blank=True, null=True) slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True) publisher = models.JSONField(default = list , verbose_name=_('Publisher')) subject_area = models.ManyToManyField(BookSubjectArea, related_name="book_subjects", verbose_name=_('subject area'), blank=True) type = models.ForeignKey(BookType, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('type')) order = models.IntegerField(default=0, verbose_name=_('Order')) researcher = models.JSONField(default=list, blank=True, verbose_name=_('Researcher')) city_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('City of Publication')) country_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('Country of Publication')) edition_number = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Edition Number')) rate = models.DecimalField( max_digits=3, decimal_places=2, verbose_name=_('rate'), blank=True, null=True, help_text=_('Rating from 0 to 5') ) 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 = [ models.Index(fields=['id']), # Already indexed (PK) ] verbose_name = _('Book Reference') verbose_name_plural = _('Book References') ordering = ('-created_at',) def __str__(self): return self.title[0]['text'] if self.title else "Untitled Reference" 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 @property def share_link(self): if self.slug: return f"{settings.DOVODI_DOMAIN}/arguments/sources/{self.slug}" return None def get_title(self,lang): return self._get_json_field("title" , lang) def get_description(self, lang): return self._get_json_field("description" , lang) def get_publisher(self, lang): return self._get_json_field("publisher" , lang) def get_language(self, lang): return self._get_json_field("language" , lang) 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): title_text = first_item.get('text', '').strip() if title_text: base_slug = slugify(title_text, allow_unicode=True) slug = base_slug counter = 1 while BookReference.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"book-{suffix}" counter = 1 while BookReference.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"book-{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"book-{suffix}" counter = 1 while BookReference.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"book-{suffix}-{counter}" counter += 1 self.slug = base_slug else: # Fallback if title is empty or invalid import time suffix = int(time.time() * 1000) % 1000000 base_slug = f"book-{suffix}" counter = 1 while BookReference.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"book-{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"book-{suffix}" counter = 1 while BookReference.objects.filter(slug=base_slug).exclude(pk=self.pk).exists(): base_slug = f"book-{suffix}-{counter}" counter += 1 self.slug = base_slug super().save(*args, **kwargs) class BookReferenceImage(models.Model): """ Model for book reference images - multiple images per book reference """ book_reference = models.ForeignKey( BookReference, on_delete=models.CASCADE, related_name='images', verbose_name=_('book reference') ) image = models.ImageField(upload_to='hadis/book_reference_images/', verbose_name=_('image')) volume = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('Volume')) order = models.PositiveIntegerField(default=0, verbose_name=_('order')) description = models.JSONField(default = list , verbose_name=_('Description')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) class Meta: verbose_name = _('Book Reference Image') verbose_name_plural = _('Book Reference Images') ordering = ['order', '-created_at'] 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 __str__(self): return f"{self.book_reference} - Image {self.order}" class BookReferenceDocument(models.Model): """ Model for handling multiple PDF files per book (e.g., different volumes). """ book_reference = models.ForeignKey( BookReference, on_delete=models.CASCADE, related_name='documents', verbose_name=_('book reference') ) file = models.FileField(upload_to='hadis/book_reference_documents/', verbose_name=_('Document File')) volume = models.CharField(max_length=50, null=True, blank=True, verbose_name=_('Volume')) title = models.CharField(max_length=255, blank=True, null=True, help_text=_('e.g., Volume 1, 01.pdf')) order = models.PositiveIntegerField(default=0, verbose_name=_('order')) created_at = models.DateTimeField(auto_now_add=True) class Meta: verbose_name = _('Book Reference Document') verbose_name_plural = _('Book Reference Documents') ordering = ['order', '-created_at'] def __str__(self): return f"{self.book_reference} - {self.title or 'Doc ' + str(self.id)}" class BookAuthor(models.Model): """ Model for book reference authors """ name = models.JSONField(default = list , verbose_name=_('Name')) 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) birth_year_miladi = models.IntegerField(verbose_name=_("Birth Year (Miladi)"), null=True, blank=True) death_year_miladi = models.IntegerField(verbose_name=_("Death Year (Miladi)"), null=True, blank=True) book_references = models.ManyToManyField( BookReference, related_name='authors', verbose_name=_('book references'), 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 = _('Book Author') verbose_name_plural = _('Book Authors') ordering = ['name'] def __str__(self): return self.name[0]['text'] if self.name else "Unknown Author" 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 class BookAttribute(models.Model): """ Model for book reference attributes - custom key-value pairs """ title = models.JSONField(default = list , verbose_name=_('Title')) value = models.JSONField(default = list , verbose_name=_('Value')) book_reference = models.ForeignKey( BookReference, on_delete=models.CASCADE, related_name='attributes', verbose_name=_('book attribute'), default = None, 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: verbose_name = _('Book Attribute') verbose_name_plural = _('Book Attributes') ordering = ['title'] def __str__(self): title = self.title[0]['text'] if self.title else "No Title" value = self.value[0]['text'] if self.value else "No Value" return f"{title}: {value}" 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_value(self,lang): """ Get title for a specific language """ if not self.value or not isinstance(self.value, list): return None for tr in self.value: if isinstance(tr, dict) and tr.get('language_code') == lang: return tr.get('text', '') for tr in self.value: if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '') return None