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 from utils.mixins import ColorPaletteMixin, LowercaseSlugMixin 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')) class Meta: ordering = ('-created_at', '-id') verbose_name = _('Subject Area') verbose_name_plural = _('Subject Areas') 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): """ 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', '') 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', '') return None class BookTag(models.Model): """ Model for book reference tags """ 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')) class Meta: verbose_name = _('Book Tag') verbose_name_plural = _('Book Tags') 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 f"Tag {self.id}" return f"Tag {self.id}" class BookType(models.Model): class Sect(models.TextChoices): SHIA = 'shia', _('Shia') SUNNI = 'sunni', _('Sunni') title = models.JSONField(default = list , verbose_name=_('Title')) sect = models.CharField( max_length=10, choices=Sect.choices, default=Sect.SHIA, verbose_name=_('Sect') ) 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): 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): """ 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', '') 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', '') return None class BookReference(LowercaseSlugMixin, models.Model): slug_source_field = 'title' """ 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) title = models.JSONField(default = list , verbose_name=_('Title')) description = models.JSONField(default = list , verbose_name=_('Description')) language = models.JSONField(default = list , verbose_name=_('Language')) 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, allow_unicode=True) subject_area = models.ManyToManyField( 'BookSubjectArea', verbose_name=_('Subject Area'), blank=True ) tag = models.ForeignKey( 'BookTag', on_delete=models.SET_NULL, null=True, blank=True, related_name='book_references', verbose_name=_('Tag') ) type = models.ForeignKey('BookType', on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('Type')) author = models.ForeignKey( 'BookAuthor', on_delete=models.SET_NULL, null=True, blank=True, related_name='book_references', verbose_name=_('Author') ) order = models.IntegerField(default=0, verbose_name=_('Order')) priority = models.PositiveIntegerField( default=0, db_index=True, verbose_name=_('Priority'), help_text=_('Display order priority. Lower numbers appear first.') ) rate = models.DecimalField( max_digits=3, decimal_places=2, verbose_name=_('rate'), blank=True, null=True, help_text=_('Rating from 0 to 5') ) has_editions = models.BooleanField( default=False, verbose_name=_('Has Editions Structure'), help_text=_('True if this reference has one or more editions, False for direct volumes without edition structure') ) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at')) normalized_text = models.TextField(blank=True, null=True, verbose_name=_('normalized text for search')) class Meta: indexes = [ models.Index(fields=['id']), # Already indexed (PK) models.Index(fields=['priority']), ] verbose_name = _('Book Reference') verbose_name_plural = _('Book References') ordering = ('priority', 'id') 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 f"Reference {self.id}" return f"Reference {self.id}" def save(self, *args, **kwargs): from utils.text_normalizer import build_search_blob author_name = self.author.name if self.author else "" self.normalized_text = build_search_blob( self.title, self.description, author_name ) 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 @property def share_link(self): if self.slug: return f"{settings.DOVODI_DOMAIN}/arguments/sources/{self.slug}" return None @property def authors(self): """ Backward compatibility helper returning a queryset/list of authors. Allows serializers calling obj.authors.all() to function correctly without hitting DB if author is already cached. """ if hasattr(self, '_state') and hasattr(self._state, 'fields_cache') and 'author' in self._state.fields_cache: author_obj = self._state.fields_cache['author'] class InMemAuthorList(list): def all(self): return self def first(self): return self[0] if self else None def exists(self): return bool(self) def count(self): return len(self) def filter(self, *args, **kwargs): return self return InMemAuthorList([author_obj] if author_obj else []) if self.author_id: return BookAuthor.objects.filter(id=self.author_id) return BookAuthor.objects.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_language(self, lang): return self._get_json_field("language" , lang) class BookPublisher(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) city = models.ForeignKey( 'hadis.City', on_delete=models.SET_NULL, null=True, blank=True, related_name='publishers', verbose_name=_('City') ) country = models.ForeignKey( 'hadis.Country', on_delete=models.SET_NULL, null=True, blank=True, related_name='publishers', verbose_name=_('Country') ) 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 = _('Book Publisher') verbose_name_plural = _('Book Publishers') 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): super().save(*args, **kwargs) self.editions.all().update(publisher=self.title or []) class BookEdition(models.Model): """ Model representing a specific edition or print of a book reference. """ title = models.JSONField(default=list, blank=True, verbose_name=_('Edition Title')) book_reference = models.ForeignKey(BookReference, on_delete=models.CASCADE, related_name='editions', verbose_name=_('book reference'), null=True, blank=True) books = models.ManyToManyField( BookReference, related_name='editions_list', blank=True, verbose_name=_('Connected Books/Works') ) publisher = models.JSONField(default=list, blank=True, verbose_name=_('Publisher')) publisher_item = models.ForeignKey( 'hadis.BookPublisher', on_delete=models.SET_NULL, null=True, blank=True, related_name='editions', verbose_name=_('Publisher Entity') ) city_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('City of Publication')) city_of_publication_city = models.ForeignKey( 'hadis.City', on_delete=models.SET_NULL, null=True, blank=True, related_name='publication_editions', verbose_name=_('City of Publication (City)'), ) country_of_publication = models.JSONField(default=list, blank=True, verbose_name=_('Country of Publication')) country_of_publication_country = models.ForeignKey( 'hadis.Country', on_delete=models.SET_NULL, null=True, blank=True, related_name='publication_editions', verbose_name=_('Country of Publication (Country)'), ) edition_number = models.CharField(max_length=100, blank=True, null=True, verbose_name=_('Edition Number')) year_of_publication = models.CharField(max_length=50, blank=True, null=True, verbose_name=_('year of publication')) number_of_volumes = models.PositiveIntegerField(null=True, blank=True, verbose_name=_('Total Number of Volumes')) source_url = models.URLField(max_length=1000, null=True, blank=True, verbose_name=_('Online Source URL')) isbn = models.CharField(max_length=100, verbose_name=_('ISBN'), blank=True, null=True) notes = models.JSONField(default=list, blank=True, verbose_name=_('Notes')) tag = models.ForeignKey( 'BookTag', on_delete=models.SET_NULL, null=True, blank=True, related_name='book_editions', verbose_name=_('Tag') ) 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 Edition') verbose_name_plural = _('Book Editions') ordering = ['-created_at'] def __str__(self): t_str = "" if self.title and isinstance(self.title, list): for item in self.title: if isinstance(item, dict) and (item.get('text') or item.get('title')): t_str = (item.get('text') or item.get('title')).strip() break if t_str: return f"{t_str} ({self.edition_number or 'Ed. ' + str(self.id)})" return f"{self.book_reference} - {self.edition_number or 'Edition ' + str(self.id)}" def save(self, *args, **kwargs): if self.publisher_item_id and self.publisher_item: self.publisher = self.publisher_item.title or [] if self.city_of_publication_city_id and self.city_of_publication_city: self.city_of_publication = self.city_of_publication_city.title or [] if self.country_of_publication_country_id and self.country_of_publication_country: self.country_of_publication = self.country_of_publication_country.title or [] super().save(*args, **kwargs) if self.book_reference and not self.books.filter(id=self.book_reference_id).exists(): self.books.add(self.book_reference) class BookVolume(models.Model): """ Model representing a single volume, which can belong to an edition or directly to the book. """ book_reference = models.ForeignKey(BookReference, on_delete=models.CASCADE, related_name='volumes', verbose_name=_('book reference')) edition = models.ForeignKey(BookEdition, on_delete=models.CASCADE, related_name='volumes', verbose_name=_('edition'), null=True, blank=True) title = models.CharField(max_length=255, verbose_name=_('Title'), help_text=_('e.g., Volume 1'), blank=True, null=True) image = models.ImageField( upload_to='hadis/volume_images/', null=True, blank=True, verbose_name=_('Image') ) file = models.FileField( upload_to='hadis/volume_files/', null=True, blank=True, verbose_name=_('File') ) 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 Volume') verbose_name_plural = _('Book Volumes') ordering = ['title'] def __str__(self): return f"{self.book_reference} - {self.title or 'Vol ' + str(self.id)}" 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') ) book_volume = models.ForeignKey( BookVolume, on_delete=models.CASCADE, related_name='images', verbose_name=_('book volume'), null=True, blank=True ) image = models.ImageField(upload_to='hadis/book_reference_images/', verbose_name=_('image')) 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 save(self, *args, **kwargs): from utils.watermark import apply_watermark_to_field_file apply_watermark_to_field_file(self.image, position="bottom_left") super().save(*args, **kwargs) 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') ) book_volume = models.ForeignKey( BookVolume, on_delete=models.CASCADE, related_name='documents', verbose_name=_('book volume'), null=True, blank=True ) file = models.FileField(upload_to='hadis/book_reference_documents/', verbose_name=_('Document File')) 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 BookAuthorRutba(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, allow_unicode=True) description = models.JSONField(default=list, verbose_name=_('Description')) 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): 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', '') return None def get_description(self, lang): 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', '') or tr.get('title', '') for tr in self.description: if isinstance(tr, dict) and tr.get('language_code') == 'en': return tr.get('text', '') or tr.get('title', '') return None class Meta: verbose_name = _('Author Rutba') verbose_name_plural = _('Author Rutbas') def save(self, *args, **kwargs): super().save(*args, **kwargs) self.authors.all().update(rutba=self.title or []) class BookAuthor(LowercaseSlugMixin, models.Model): slug_source_field = 'name' """ Model for book reference authors """ name = models.JSONField(default=list, verbose_name=_('Name')) slug = models.SlugField(max_length=255, verbose_name=_('slug'), null=True, blank=True, unique=True, allow_unicode=True) related_narrator = models.OneToOneField( 'hadis.Transmitters', on_delete=models.SET_NULL, null=True, blank=True, related_name='book_author_profile', verbose_name=_('Related Narrator Profile') ) # 👇 فیلدهای جدید اضافه شده known_as = models.JSONField(default=list, blank=True, verbose_name=_('Known as')) scholarity = models.JSONField(default=list, blank=True, verbose_name=_('Scholarity')) tabaqa = models.JSONField(default=list, blank=True, verbose_name=_('Tabaqa')) rutba = models.JSONField(default=list, blank=True, verbose_name=_('Rutba')) rutba_status = models.ForeignKey( 'BookAuthorRutba', on_delete=models.SET_NULL, null=True, blank=True, related_name='authors', verbose_name=_('Rutba Status') ) features = models.JSONField(default=list, blank=True, verbose_name=_('Features')) features_list = models.ManyToManyField( 'hadis.TransmitterFeature', blank=True, related_name='authors', verbose_name=_('Features (M2M)') ) # Geographic Information origin = models.JSONField(default=list, blank=True, verbose_name=_('Origin')) lived_in = models.JSONField(default=list, blank=True, verbose_name=_('Lived in')) died_in = models.JSONField(default=list, blank=True, verbose_name=_('Died in')) origin_city = models.ForeignKey( 'hadis.City', on_delete=models.SET_NULL, null=True, blank=True, related_name='origin_authors', verbose_name=_('Origin City'), ) lived_in_city = models.ForeignKey( 'hadis.City', on_delete=models.SET_NULL, null=True, blank=True, related_name='lived_in_authors', verbose_name=_('Lived in City'), ) died_in_city = models.ForeignKey( 'hadis.City', on_delete=models.SET_NULL, null=True, blank=True, related_name='died_in_authors', verbose_name=_('Died in City'), ) birth_year_hijri = models.CharField(max_length=100, verbose_name=_("Birth Year (Hijri)"), null=True, blank=True) death_year_hijri = models.CharField(max_length=100, verbose_name=_("Death Year (Hijri)"), null=True, blank=True) age_at_death = models.CharField(max_length=100, verbose_name=_("Age at Death"), 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) image = models.ImageField( upload_to='hadis/author_images/', null=True, blank=True, help_text=_('image allowed'), verbose_name=_('Image') ) 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 def save(self, *args, **kwargs): if self.rutba_status_id and self.rutba_status: self.rutba = self.rutba_status.title or [] if self.origin_city_id and self.origin_city: self.origin = self.origin_city.title or [] if self.lived_in_city_id and self.lived_in_city: self.lived_in = self.lived_in_city.title or [] if self.died_in_city_id and self.died_in_city: self.died_in = self.died_in_city.title or [] for date_field in ('birth_year_hijri', 'death_year_hijri', 'age_at_death'): val = getattr(self, date_field, None) if val is not None: val_str = str(val).strip() setattr(self, date_field, val_str if val_str else None) super().save(*args, **kwargs) self.researcher_profiles.all().update(name=self.name, slug=self.slug) try: self.sync_author_dates() except Exception: pass def sync_author_dates(self): """ Synchronizes birth_year_hijri, death_year_hijri, and age_at_death with YearValue and BookAuthorDate tables. """ if not self.pk: return from .transmitter import YearValue date_mapping = { 'birth': self.birth_year_hijri, 'death': self.death_year_hijri, 'age': self.age_at_death, } for date_type, raw_val in date_mapping.items(): if not raw_val or not str(raw_val).strip(): BookAuthorDate.objects.filter(author=self, date_type=date_type).delete() continue raw_str = str(raw_val).strip() yv = YearValue.get_or_create_from_canonical(category=date_type, raw_value=raw_str) BookAuthorDate.objects.update_or_create( author=self, date_type=date_type, defaults={ 'year_value': yv, 'raw_value': yv.raw_value, } ) def get_formatted_date(self, date_type: str, lang: str = 'ru') -> Optional[str]: field_map = { 'birth': 'birth_year_hijri', 'death': 'death_year_hijri', 'age': 'age_at_death', } field_name = field_map.get(date_type) if not field_name: return None raw_val = getattr(self, field_name, None) if raw_val is None or str(raw_val).strip() == '': return None raw_str = str(raw_val).strip() target_lang = (lang or 'ru').split('-')[0].lower() # Check prefetched author_dates first if available if hasattr(self, '_prefetched_objects_cache') and 'author_dates' in self._prefetched_objects_cache: for ad in self._prefetched_objects_cache['author_dates']: if ad.date_type == date_type and ad.year_value_id and ad.year_value: txt = ad.year_value.get_translation(target_lang) if txt: return txt # If context tag (#...), check YearValue in DB for custom translations if raw_str.startswith('#'): from .transmitter import YearValue yv = YearValue.objects.filter(raw_value=raw_str).first() if yv: txt = yv.get_translation(target_lang) if txt: return txt from ..utils.date_engine import format_canonical_date engine_lang = target_lang if target_lang in ('ru', 'en', 'ar') else 'en' return format_canonical_date(raw_str, category=date_type, lang=engine_lang) def get_formatted_birth_year(self, lang: str = 'ru') -> Optional[str]: return self.get_formatted_date('birth', lang=lang) def get_formatted_death_year(self, lang: str = 'ru') -> Optional[str]: return self.get_formatted_date('death', lang=lang) def get_formatted_age(self, lang: str = 'ru') -> Optional[str]: return self.get_formatted_date('age', lang=lang) class BookAuthorDate(models.Model): """ Links a BookAuthor to a parsed YearValue for birth, death, or age. """ class DateTypeChoices(models.TextChoices): BIRTH = 'birth', _('Birth') DEATH = 'death', _('Death') AGE = 'age', _('Age') author = models.ForeignKey( BookAuthor, on_delete=models.CASCADE, related_name='author_dates', verbose_name=_('Book Author') ) date_type = models.CharField( max_length=20, choices=DateTypeChoices.choices, db_index=True, verbose_name=_('Date Type') ) year_value = models.ForeignKey( 'hadis.YearValue', on_delete=models.CASCADE, related_name='author_dates', verbose_name=_('Year Value') ) raw_value = models.CharField( max_length=100, verbose_name=_('Canonical Raw Value') ) 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 Date') verbose_name_plural = _('Book Author Dates') unique_together = ('author', 'date_type') indexes = [ models.Index(fields=['date_type', 'year_value']), models.Index(fields=['author', 'date_type']), ] ordering = ('author_id', 'date_type') def __str__(self): return f"{self.author_id} - {self.date_type}: {self.raw_value}" 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 class BookResearcher(LowercaseSlugMixin, models.Model): slug_source_field = 'name' book_edition = models.ForeignKey( 'BookEdition', on_delete=models.CASCADE, related_name='researchers', verbose_name=_('Book Edition') ) author = models.ForeignKey( 'BookAuthor', on_delete=models.SET_NULL, null=True, blank=True, related_name='researcher_profiles', verbose_name=_('Author') ) name = models.JSONField(default=list, blank=True, verbose_name=_('Name')) slug = models.SlugField(max_length=255, verbose_name=_('slug'), null=True, blank=True, allow_unicode=True) class Meta: verbose_name = _('Book Researcher') verbose_name_plural = _('Book Researchers') def save(self, *args, **kwargs): if self.author: self.name = self.author.name self.slug = self.author.slug super().save(*args, **kwargs) def __str__(self): try: return self.name[0]['text'] except: return self.slug or f"Researcher {self.id}" class BookEditor(LowercaseSlugMixin, models.Model): slug_source_field = 'name' book_edition = models.ForeignKey( 'BookEdition', on_delete=models.CASCADE, related_name='editors', verbose_name=_('Book Edition') ) author = models.ForeignKey( 'BookAuthor', on_delete=models.SET_NULL, null=True, blank=True, related_name='editor_profiles', verbose_name=_('Author') ) name = models.JSONField(default=list, blank=True, verbose_name=_('Name')) slug = models.SlugField(max_length=255, verbose_name=_('slug'), null=True, blank=True, allow_unicode=True) class Meta: verbose_name = _('Book Editor') verbose_name_plural = _('Book Editors') def save(self, *args, **kwargs): if self.author: self.name = self.author.name self.slug = self.author.slug super().save(*args, **kwargs) def __str__(self): try: return self.name[0]['text'] except: return self.slug or f"Editor {self.id}"