You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

600 lines
25 KiB

from enum import unique
from typing import Optional
from django.db import models
from django.db.models import F, ForeignKey
from django.utils.translation import gettext_lazy as _
from django.conf import settings
from django.utils.text import slugify
from .reference import BookReference
from utils.slug import generate_smart_slug
from utils.mixins import ColorPaletteMixin
class HadisCollection(models.Model):
title = models.JSONField(default = list , verbose_name=_('Title'))
slug = models.SlugField(max_length=255, unique=True, verbose_name=_('slug'), blank=True)
summary = models.JSONField(default = list , verbose_name=_('Summary'))
status = models.BooleanField(default=True, verbose_name=_('status'))
order = models.IntegerField(default=0, verbose_name=_('order'))
thumbnail = models.ImageField(upload_to='hadis/collection_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'))
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 HadisCollection.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"collection-{suffix}"
counter = 1
while HadisCollection.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"collection-{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"collection-{suffix}"
counter = 1
while HadisCollection.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"collection-{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"collection-{suffix}"
counter = 1
while HadisCollection.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"collection-{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"collection-{suffix}"
counter = 1
while HadisCollection.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"collection-{suffix}-{counter}"
counter += 1
self.slug = base_slug
super().save(*args, **kwargs)
def __str__(self):
return self.title[0]['text']
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_summary(self,lang):
"""
Get translation for a specific language
"""
if not self.summary or not isinstance(self.summary, list):
return None
for tr in self.summary:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
for tr in self.summary:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return None
class Meta:
verbose_name = _('hadis collection')
verbose_name_plural = _('hadis collections')
ordering = ('order',)
class HadisInCollection(models.Model):
hadis = models.ForeignKey('Hadis', on_delete=models.CASCADE, verbose_name=_('hadis'), related_name='collection_items')
collection = models.ForeignKey(HadisCollection, on_delete=models.CASCADE, verbose_name=_('collection'), related_name='hadis_items')
order = models.IntegerField(default=0, verbose_name=_('order'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
class Meta:
verbose_name = _('hadis in collection')
verbose_name_plural = _('hadis in collections')
ordering = ('order',)
unique_together = ('hadis', 'collection')
def __str__(self):
return f"{self.collection.title[0]['text']} - {self.hadis.number}"
class HadisTag(models.Model):
title = models.JSONField(default = list , verbose_name=_('Title'))
status = models.BooleanField(default=True, verbose_name=_('status'))
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 f"{self.title[0]['text']}"
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 HadisStatus(ColorPaletteMixin,models.Model):
title = models.JSONField(default = list , verbose_name=_('Title'))
slug= models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique = True)
# color = models.CharField(max_length=20, choices=ColorChoices.choices, verbose_name=_('color'))
order = models.IntegerField(default=0, verbose_name=_('order'))
description = models.JSONField(default = list , verbose_name=_('Description'))
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:
slug = slugify(title_text)
# Ensure uniqueness
counter = 1
base_slug = slug
while HadisStatus.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"status-{suffix}"
counter = 1
while HadisStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"status-{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"status-{suffix}"
counter = 1
while HadisStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"status-{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"status-{suffix}"
counter = 1
while HadisStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"status-{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"status-{suffix}"
counter = 1
while HadisStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"status-{suffix}-{counter}"
counter += 1
self.slug = base_slug
super().save(*args, **kwargs)
def __str__(self):
return self.title[0]['text']
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 = _('hadis status')
verbose_name_plural = _('hadis statuses')
ordering = ('order',)
class Hadis(models.Model):
category = models.ForeignKey("hadis.HadisCategory", on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('category'))
number = models.PositiveIntegerField(verbose_name=_('number'), default=1)
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
title_narrator = models.JSONField(default = list , verbose_name=_('Title Narrator'))
title = models.JSONField(default = list , verbose_name=_('Title'))
description = models.JSONField(default = list , verbose_name=_('Description'))
text = models.TextField(verbose_name=_('text'))
translation = models.JSONField(verbose_name=_('translation'), default=list)
status = models.BooleanField(default=True, verbose_name=_('visibility'))
hadis_status = models.ForeignKey(HadisStatus, on_delete=models.SET_NULL, null=True, blank=True, verbose_name=_('hadis status'))
hadis_status_text = models.JSONField(default = list , verbose_name=_('Status text'))
address = models.JSONField(default = list , verbose_name=_('Address'))
links = models.JSONField(verbose_name=_('links'), null=True, blank=True, default=dict)
tags = models.ManyToManyField("HadisTag", related_name="hadis_overview", verbose_name=_('tags'), blank=True)
share_link = models.CharField(max_length=255, verbose_name=_('share link'), null=True, blank=True)
explanation = models.JSONField(default = list , verbose_name=_('Explanation'))
# explanations = models.JSONField(default = list , verbose_name=_('Explanations'))
# address_details = models.JSONField(default = list , verbose_name=_('Address Details'))
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated at'))
embedded_in = models.JSONField(default=list, blank=True)
def __str__(self):
return f"{self.number} - {self.title[0]['text']}" if self.title else f"Hadis {self.number}"
def save(self, *args, **kwargs):
# Generate slug if not already set or if slug is empty
if not self.slug or (isinstance(self.slug, str) and self.slug.strip() == ''):
title_text = None
# 1. Try to extract text if title exists
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")
# 2. If we found text, use smart slug
if title_text:
self.slug = generate_smart_slug(
text=title_text,
model_class=Hadis,
max_length=100,
keep_words=8,
instance=self,
)
# 3. CRITICAL FALLBACK: If no title text, use the number with unique suffix
else:
# We use a random suffix or timestamp to ensure uniqueness if multiple untitled hadiths exist
from datetime import datetime
import random
import time
# Use timestamp + random to ensure uniqueness
suffix = int(time.time() * 1000) % 1000000 # Use milliseconds for better uniqueness
base_slug = f"hadis-{self.number or 'unknown'}-{suffix}"
# Ensure uniqueness by checking database
counter = 1
while Hadis.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"hadis-{self.number or 'unknown'}-{suffix}-{counter}"
counter += 1
self.slug = base_slug
# Generate/update share_link before saving
if self.slug:
category_slug = self.category.slug if self.category and self.category.slug else 'uncategorized'
self.share_link = f"{settings.DOVODI_DOMAIN}/arguments/hadith/{category_slug}/{self.slug}"
# Reset embedded_in if text or translation changes
if self.pk:
old_instance = Hadis.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_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_translation(self, lang):
return self._get_json_field("translation" , lang)
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_hadis_status_text(self, lang):
return self._get_json_field("hadis_status_text" , lang)
def get_address(self, lang):
return self._get_json_field("address" , lang)
def get_explanation(self, lang):
return self._get_json_field("explanation" , lang)
def get_explanations(self, lang):
return self._get_json_field("explanations" , lang)
def get_address_details(self, lang):
"""Get address details (returns full list, not localized)"""
return self.address_details if self.address_details else []
class Meta:
indexes = [
# Optimizes: Hadis.objects.filter(status=True).order_by('id')
models.Index(fields=['status', 'id']),
]
verbose_name = _('hadis')
verbose_name_plural = _('hadises')
ordering = ('category', 'number')
class HadisReference(models.Model):
hadis = models.ForeignKey(
Hadis,
on_delete=models.CASCADE,
verbose_name=_('hadis'),
related_name='references'
)
book_reference = models.ForeignKey(
BookReference,
on_delete=models.SET_NULL,
null=True,
blank=True,
verbose_name=_('book reference'),
related_name='hadis_references'
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
description = models.JSONField(default = list , verbose_name=_('Description'))
class Meta:
indexes = [
# For fetching hadises by book
models.Index(fields=['book_reference']),
]
verbose_name = _('Hadis Reference')
verbose_name_plural = _('Hadis References')
# unique_together = ('hadis', 'book_reference')
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.hadis.number}-{self.book_reference.title[0]["text"] if self.book_reference else "No Book Reference"}'
class ReferenceImage(models.Model):
reference = models.ForeignKey(HadisReference,related_name = 'images', verbose_name="Hadis Reference", on_delete=models.CASCADE)
thumbnail = models.ImageField(upload_to='hadis/reference_images/', null=True, blank=True, verbose_name=_('thumbnail'))
priority = models.IntegerField(
default=0,
verbose_name=_("Priority"),
help_text=_("Priority of the image, lower values mean higher priority.")
)
class Meta:
indexes = [
# Speeds up fetching images for a reference in priority order
models.Index(fields=['reference', 'priority']),
]
verbose_name = _('Reference Image')
verbose_name_plural = _('Reference Images')
def __str__(self):
return f'{self.reference.book_reference.title[0]["text"]}-{self.id}'
def save(self, *args, **kwargs):
if ReferenceImage.objects.filter(reference=self.reference, priority=self.priority).exists():
ReferenceImage.objects.filter(
reference=self.reference,
priority__gte=self.priority
).update(priority=F('priority') + 1)
super().save(*args, **kwargs)
class HadisCorrection(models.Model):
hadis = models.ForeignKey(Hadis, verbose_name=_("hadis correction"), on_delete=models.CASCADE)
title = models.JSONField(default = list , verbose_name=_('Title'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True)
description =models.JSONField(default = list , verbose_name=_('Description'))
translation = models.JSONField(verbose_name=_("translation"), default=list)
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("created at"))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_("updated at"))
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:
verbose_name = _("Hadis Correction")
verbose_name_plural = _("Hadis Corrections")
ordering = ("-created_at",)
def __str__(self):
return f"{self.hadis.number} - {self.title[0]['text']}"
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=HadisCorrection,
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
base_slug = f"correction-{self.hadis.slug if self.hadis and self.hadis.slug else 'unknown'}-{suffix}"
# Ensure uniqueness
counter = 1
while HadisCorrection.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"correction-{self.hadis.slug if self.hadis and self.hadis.slug else 'unknown'}-{suffix}-{counter}"
counter += 1
self.slug = base_slug
# Generate/update share_link before saving
if self.slug and self.hadis and self.hadis.slug:
category_slug = self.hadis.category.slug if self.hadis.category and self.hadis.category.slug else 'uncategorized'
self.share_link = f"{settings.DOVODI_DOMAIN}/arguments/hadith/{category_slug}/{self.hadis.slug}/corrections/{self.slug}"
# Reset embedded_in if text or translation changes
if self.pk:
old_instance = HadisCorrection.objects.get(pk=self.pk)
if (old_instance.description != self.description 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_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_translation(self,lang):
"""
Get title for a specific language
"""
if not self.translation or not isinstance(self.translation, list):
return None
for tr in self.translation:
if isinstance(tr, dict) and tr.get('language_code') == lang:
return tr.get('text', '')
for tr in self.translation:
if isinstance(tr, dict) and tr.get('language_code') == 'en':
return tr.get('text', '')
return None