Browse Source

slug generator mixin upgraded with update and save auto slug create

master
Mohsen Taba 1 week ago
parent
commit
d5ae8d22c4
  1. 14
      apps/article/models.py
  2. 60
      apps/hadis/models/category.py
  3. 146
      apps/hadis/models/hadis.py
  4. 94
      apps/hadis/models/reference.py
  5. 217
      apps/hadis/models/transmitter.py
  6. 22
      apps/library/models.py
  7. 14
      apps/podcast/models.py
  8. 22
      apps/video/models.py
  9. 35
      utils/admin.py
  10. 32
      utils/mixins.py
  11. 61
      utils/slug.py

14
apps/article/models.py

@ -6,7 +6,7 @@ from utils import generate_slug_for_model
from utils.mixins import LowercaseSlugMixin from utils.mixins import LowercaseSlugMixin
class ArticleCategory(models.Model):
class ArticleCategory(LowercaseSlugMixin, models.Model):
title = models.CharField(max_length=255, verbose_name=_('title')) title = models.CharField(max_length=255, verbose_name=_('title'))
slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug')) slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug'))
@ -18,18 +18,13 @@ class ArticleCategory(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug_for_model(ArticleCategory, self.title)
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _('Article Category') verbose_name = _('Article Category')
verbose_name_plural = _('Article Categories') verbose_name_plural = _('Article Categories')
ordering = ['order'] ordering = ['order']
class ArticleCollection(models.Model):
class ArticleCollection(LowercaseSlugMixin, models.Model):
class DisplayPosition(models.TextChoices): class DisplayPosition(models.TextChoices):
PINNED = 'pinned', _('Pinned') PINNED = 'pinned', _('Pinned')
MIDDLE = 'middle', _('Middle Section') MIDDLE = 'middle', _('Middle Section')
@ -60,11 +55,6 @@ class ArticleCollection(models.Model):
def __str__(self): def __str__(self):
return f'Collection #{self.id}/{self.title}' return f'Collection #{self.id}/{self.title}'
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug_for_model(ArticleCollection, self.title)
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _('Article Collection') verbose_name = _('Article Collection')
verbose_name_plural = _('Articles Collections') verbose_name_plural = _('Articles Collections')

60
apps/hadis/models/category.py

@ -4,6 +4,7 @@ from django.core.exceptions import ValidationError
from mptt.models import MPTTModel, TreeForeignKey from mptt.models import MPTTModel, TreeForeignKey
from django.utils.text import slugify from django.utils.text import slugify
from django.conf import settings from django.conf import settings
from utils.mixins import LowercaseSlugMixin
class HadisSect(models.Model): class HadisSect(models.Model):
@ -63,7 +64,7 @@ class HadisSect(models.Model):
ordering = ('order',) ordering = ('order',)
class HadisCategory(MPTTModel):
class HadisCategory(LowercaseSlugMixin, MPTTModel):
class SourceType(models.TextChoices): class SourceType(models.TextChoices):
QURAN = 'quran', _('Quran') QURAN = 'quran', _('Quran')
HADITH = 'hadith', _('Hadith') HADITH = 'hadith', _('Hadith')
@ -92,63 +93,10 @@ class HadisCategory(MPTTModel):
f'Your sect: {self.sect.sect_type}') f'Your sect: {self.sect.sect_type}')
) )
slug_source_field = 'title'
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.full_clean() self.full_clean()
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 HadisCategory.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"category-{suffix}"
counter = 1
while HadisCategory.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"category-{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"category-{suffix}"
counter = 1
while HadisCategory.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"category-{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"category-{suffix}"
counter = 1
while HadisCategory.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"category-{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"category-{suffix}"
counter = 1
while HadisCategory.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"category-{suffix}-{counter}"
counter += 1
self.slug = base_slug
super().save(*args, **kwargs) super().save(*args, **kwargs)
@property @property

146
apps/hadis/models/hadis.py

@ -9,7 +9,8 @@ from .reference import BookReference
from utils.slug import generate_smart_slug from utils.slug import generate_smart_slug
from utils.mixins import ColorPaletteMixin, LowercaseSlugMixin from utils.mixins import ColorPaletteMixin, LowercaseSlugMixin
class HadisCollection(models.Model):
class HadisCollection(LowercaseSlugMixin, models.Model):
slug_source_field = 'title'
title = models.JSONField(default = list , verbose_name=_('Title')) title = models.JSONField(default = list , verbose_name=_('Title'))
slug = models.SlugField(max_length=255, unique=True, verbose_name=_('slug'), blank=True) slug = models.SlugField(max_length=255, unique=True, verbose_name=_('slug'), blank=True)
summary = models.JSONField(default = list , verbose_name=_('Summary')) summary = models.JSONField(default = list , verbose_name=_('Summary'))
@ -20,64 +21,6 @@ class HadisCollection(models.Model):
created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at')) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_('created at'))
updated_at = models.DateTimeField(auto_now=True, verbose_name=_('updated 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): def __str__(self):
return self.title[0]['text'] if self.title else "Untitled Collection" return self.title[0]['text'] if self.title else "Untitled Collection"
@ -164,7 +107,8 @@ class HadisTag(models.Model):
return None return None
class HadisStatus(ColorPaletteMixin,models.Model):
class HadisStatus(ColorPaletteMixin, LowercaseSlugMixin, models.Model):
slug_source_field = 'title'
title = models.JSONField(default = list , verbose_name=_('Title')) title = models.JSONField(default = list , verbose_name=_('Title'))
slug= models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique = True) slug= models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique = True)
@ -172,65 +116,6 @@ class HadisStatus(ColorPaletteMixin,models.Model):
order = models.IntegerField(default=0, verbose_name=_('order')) order = models.IntegerField(default=0, verbose_name=_('order'))
description = models.JSONField(default = list , verbose_name=_('Description')) 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): def __str__(self):
return self.title[0]['text'] if self.title else str(self.id) return self.title[0]['text'] if self.title else str(self.id)
@ -517,27 +402,8 @@ class HadisCorrection(models.Model):
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# Generate smart slug # Generate smart slug
if not self.slug and self.title:
title_text = None
if 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")
if title_text:
self.slug = generate_smart_slug(
text=title_text, model_class=HadisCorrection,
max_length=100, keep_words=8, instance=self,
)
else:
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}"
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
from utils.slug import build_slug_for_instance
build_slug_for_instance(self, source_field='title')
if self.slug and self.hadis and self.hadis.slug: if self.slug and self.hadis and self.hadis.slug:
self.share_link = f"{settings.DOVODI_DOMAIN}/arguments/hadith/{self.hadis.slug}/corrections/{self.slug}" self.share_link = f"{settings.DOVODI_DOMAIN}/arguments/hadith/{self.hadis.slug}/corrections/{self.slug}"

94
apps/hadis/models/reference.py

@ -3,6 +3,7 @@ from django.utils.translation import gettext_lazy as _
from django.utils.text import slugify from django.utils.text import slugify
from django.conf import settings from django.conf import settings
from typing import Optional from typing import Optional
from utils.mixins import LowercaseSlugMixin
class BookSubjectArea(models.Model): class BookSubjectArea(models.Model):
title = models.JSONField(default = list , verbose_name=_('Title')) title = models.JSONField(default = list , verbose_name=_('Title'))
@ -83,7 +84,8 @@ class BookType(models.Model):
return tr.get('text', '') return tr.get('text', '')
return None return None
class BookReference(models.Model):
class BookReference(LowercaseSlugMixin, models.Model):
slug_source_field = 'title'
""" """
Model for hadis book references with detailed information Model for hadis book references with detailed information
This is different from library books - these are reference books for hadis This is different from library books - these are reference books for hadis
@ -192,63 +194,7 @@ class BookReference(models.Model):
return self._get_json_field("language" , 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 BookEdition(models.Model): class BookEdition(models.Model):
""" """
@ -377,7 +323,8 @@ class BookReferenceDocument(models.Model):
return f"{self.book_reference} - {self.title or 'Doc ' + str(self.id)}" return f"{self.book_reference} - {self.title or 'Doc ' + str(self.id)}"
class BookAuthor(models.Model):
class BookAuthor(LowercaseSlugMixin, models.Model):
slug_source_field = 'name'
""" """
Model for book reference authors Model for book reference authors
""" """
@ -436,32 +383,6 @@ class BookAuthor(models.Model):
return None return None
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if not self.slug or (isinstance(self.slug, str) and self.slug.strip() == ''):
try:
if self.name and isinstance(self.name, list) and len(self.name) > 0:
first_item = self.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 BookAuthor.objects.filter(slug=slug).exclude(pk=self.pk).exists():
slug = f"{base_slug}-{counter}"
counter += 1
self.slug = slug
else:
import time
self.slug = f"author-{int(time.time() * 1000) % 1000000}"
else:
import time
self.slug = f"author-{int(time.time() * 1000) % 1000000}"
else:
import time
self.slug = f"author-{int(time.time() * 1000) % 1000000}"
except Exception:
import time
self.slug = f"author-{int(time.time() * 1000) % 1000000}"
super().save(*args, **kwargs) super().save(*args, **kwargs)
self.researcher_profiles.all().update(name=self.name, slug=self.slug) self.researcher_profiles.all().update(name=self.name, slug=self.slug)
@ -528,7 +449,8 @@ class BookAttribute(models.Model):
return None return None
class BookResearcher(models.Model):
class BookResearcher(LowercaseSlugMixin, models.Model):
slug_source_field = 'name'
book_edition = models.ForeignKey( book_edition = models.ForeignKey(
'BookEdition', 'BookEdition',
on_delete=models.CASCADE, on_delete=models.CASCADE,

217
apps/hadis/models/transmitter.py

@ -7,11 +7,12 @@ from django.utils.text import slugify
from typing import Optional from typing import Optional
from utils.slug import generate_smart_slug from utils.slug import generate_smart_slug
from django.conf import settings from django.conf import settings
from utils.mixins import ColorPaletteMixin
from utils.mixins import ColorPaletteMixin, LowercaseSlugMixin
class NarratorLayer(models.Model):
class NarratorLayer(LowercaseSlugMixin, models.Model):
slug_source_field = 'name'
""" """
Model for narrator layers/classes (Tabaqat) Model for narrator layers/classes (Tabaqat)
Represents the classification level of narrators in hadis chains Represents the classification level of narrators in hadis chains
@ -122,67 +123,14 @@ class NarratorLayer(models.Model):
self.slug = slug self.slug = slug
super().save(*args, **kwargs) super().save(*args, **kwargs)
class TransmitterReliability(ColorPaletteMixin, models.Model):
class TransmitterReliability(ColorPaletteMixin, LowercaseSlugMixin, models.Model):
slug_source_field = 'title'
title = models.JSONField(default = list , verbose_name=_('Title')) title = models.JSONField(default = list , verbose_name=_('Title'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,null=True) 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): def __str__(self):
return self.title[0]['text'] if self.title else str(self.id) return self.title[0]['text'] if self.title else str(self.id)
@ -210,7 +158,8 @@ class TransmitterReliability(ColorPaletteMixin, models.Model):
class Transmitters(models.Model):
class Transmitters(LowercaseSlugMixin, models.Model):
slug_source_field = 'full_name'
# class ReliabilityLevel(models.TextChoices): # class ReliabilityLevel(models.TextChoices):
# VERY_RELIABLE = 'very_reliable', _('Very Reliable') # VERY_RELIABLE = 'very_reliable', _('Very Reliable')
# RELIABLE = 'reliable', _('Reliable') # RELIABLE = 'reliable', _('Reliable')
@ -313,63 +262,7 @@ class Transmitters(models.Model):
return f"{settings.DOVODI_DOMAIN}/arguments/narrators/{self.slug}" return f"{settings.DOVODI_DOMAIN}/arguments/narrators/{self.slug}"
return None 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"): def _get_json_field(self, field_name: str, lang: Optional[str]=None , fallback: str = "en"):
""" """
@ -492,66 +385,13 @@ class HadisTransmitter(models.Model):
return f'{self.hadis.number} - Chain {self.chain_index} - {self.transmitter} (Pos: {self.order})' return f'{self.hadis.number} - Chain {self.chain_index} - {self.transmitter} (Pos: {self.order})'
class OpinionStatus(ColorPaletteMixin, models.Model):
class OpinionStatus(ColorPaletteMixin, LowercaseSlugMixin, models.Model):
slug_source_field = 'title'
title = models.JSONField(default = list , verbose_name=_('Title')) title = models.JSONField(default = list , verbose_name=_('Title'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,null=True) 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): def __str__(self):
return self.title[0]['text'] if self.title else str(self.id) return self.title[0]['text'] if self.title else str(self.id)
@ -695,37 +535,8 @@ class TransmitterOriginalText(models.Model):
""" """
Override save to automatically generate smart slugs. 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
from utils.slug import build_slug_for_instance
build_slug_for_instance(self, source_field='title')
# Generate/update share_link before saving # Generate/update share_link before saving
if self.slug and self.transmitter and self.transmitter.slug: if self.slug and self.transmitter and self.transmitter.slug:

22
apps/library/models.py

@ -11,7 +11,7 @@ from apps.account.models import User
from utils.mixins import LowercaseSlugMixin from utils.mixins import LowercaseSlugMixin
class BookCollection(models.Model):
class BookCollection(LowercaseSlugMixin, models.Model):
class DisplayPosition(models.TextChoices): class DisplayPosition(models.TextChoices):
PINNED = 'pinned', _('Pinned') PINNED = 'pinned', _('Pinned')
MIDDLE = 'middle', _('Middle Section') MIDDLE = 'middle', _('Middle Section')
@ -38,11 +38,6 @@ class BookCollection(models.Model):
verbose_name = _('Book Collection') verbose_name = _('Book Collection')
verbose_name_plural = _('Book Collections') verbose_name_plural = _('Book Collections')
def save(self, *args, **kwargs):
if not self.slug or not self.slug.strip():
self.slug = generate_smart_slug(self.title, BookCollection, instance=self)
super().save(*args, **kwargs)
class PinnedBookCollection(BookCollection): class PinnedBookCollection(BookCollection):
@ -67,7 +62,7 @@ class MiddleBookCollection(BookCollection):
class Category(models.Model):
class Category(LowercaseSlugMixin, models.Model):
title = models.CharField(max_length=255) title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
status = models.BooleanField(default=True, verbose_name=_('status')) status = models.BooleanField(default=True, verbose_name=_('status'))
@ -79,11 +74,6 @@ class Category(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
def save(self, *args, **kwargs):
if not self.slug or not self.slug.strip():
self.slug = generate_smart_slug(self.title, Category, instance=self)
super().save(*args, **kwargs)
@property @property
def books_count(self): def books_count(self):
"""Return the number of books in this category""" """Return the number of books in this category"""
@ -94,7 +84,8 @@ class Category(models.Model):
verbose_name_plural = _('Categories') verbose_name_plural = _('Categories')
class Author(models.Model):
class Author(LowercaseSlugMixin, models.Model):
slug_source_field = 'full_name'
full_name = models.CharField(max_length=255, verbose_name=_('Full Name')) full_name = models.CharField(max_length=255, verbose_name=_('Full Name'))
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True) slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
thumbnail = models.ImageField( thumbnail = models.ImageField(
@ -117,11 +108,6 @@ class Author(models.Model):
def __str__(self): def __str__(self):
return self.full_name return self.full_name
def save(self, *args, **kwargs):
if not self.slug or not self.slug.strip():
self.slug = generate_smart_slug(self.full_name, Author, instance=self)
super().save(*args, **kwargs)
@property @property
def books_count(self): def books_count(self):
return self.books.filter(status=True).count() return self.books.filter(status=True).count()

14
apps/podcast/models.py

@ -6,7 +6,7 @@ from utils import generate_slug_for_model
from utils.mixins import LowercaseSlugMixin from utils.mixins import LowercaseSlugMixin
class PodcastCategory(models.Model):
class PodcastCategory(LowercaseSlugMixin, models.Model):
title = models.CharField(max_length=255, verbose_name=_('title')) title = models.CharField(max_length=255, verbose_name=_('title'))
slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug')) slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug'))
@ -18,18 +18,13 @@ class PodcastCategory(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug_for_model(PodcastCategory, self.title)
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _('Podcast Category') verbose_name = _('Podcast Category')
verbose_name_plural = _('Podcast Categories') verbose_name_plural = _('Podcast Categories')
ordering = ['order'] ordering = ['order']
class PodcastCollection(models.Model):
class PodcastCollection(LowercaseSlugMixin, models.Model):
class DisplayPosition(models.TextChoices): class DisplayPosition(models.TextChoices):
PINNED = 'pinned', _('Pinned') PINNED = 'pinned', _('Pinned')
MIDDLE = 'middle', _('Middle Section') MIDDLE = 'middle', _('Middle Section')
@ -54,11 +49,6 @@ class PodcastCollection(models.Model):
def __str__(self): def __str__(self):
return f'Collection #{self.id}/{self.title}' return f'Collection #{self.id}/{self.title}'
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug_for_model(PodcastCollection, self.title)
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _('Podcast Collection') verbose_name = _('Podcast Collection')
verbose_name_plural = _('Podcasts Collections') verbose_name_plural = _('Podcasts Collections')

22
apps/video/models.py

@ -8,7 +8,7 @@ from django.core.validators import FileExtensionValidator
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
class VideoCategory(models.Model):
class VideoCategory(LowercaseSlugMixin, models.Model):
title = models.CharField(max_length=255, verbose_name=_('title')) title = models.CharField(max_length=255, verbose_name=_('title'))
slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug')) slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug'))
@ -20,18 +20,13 @@ class VideoCategory(models.Model):
def __str__(self): def __str__(self):
return self.title return self.title
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug_for_model(VideoCategory, self.title)
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _('Video Category') verbose_name = _('Video Category')
verbose_name_plural = _('Video Categories') verbose_name_plural = _('Video Categories')
ordering = ['order'] ordering = ['order']
class VideoCollection(models.Model):
class VideoCollection(LowercaseSlugMixin, models.Model):
class DisplayPosition(models.TextChoices): class DisplayPosition(models.TextChoices):
PINNED = 'pinned', _('Pinned') PINNED = 'pinned', _('Pinned')
MIDDLE = 'middle', _('Middle Section') MIDDLE = 'middle', _('Middle Section')
@ -56,12 +51,6 @@ class VideoCollection(models.Model):
def __str__(self): def __str__(self):
return f'Collection #{self.id}/{self.title}' return f'Collection #{self.id}/{self.title}'
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug_for_model(VideoCollection, self.title)
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _('Video Collection') verbose_name = _('Video Collection')
verbose_name_plural = _('Video Collections') verbose_name_plural = _('Video Collections')
@ -140,7 +129,7 @@ class Video(LowercaseSlugMixin, models.Model):
class VideoPlaylist(models.Model):
class VideoPlaylist(LowercaseSlugMixin, models.Model):
title = models.CharField(max_length=255, verbose_name=_('title')) title = models.CharField(max_length=255, verbose_name=_('title'))
slug = models.SlugField(allow_unicode=True, unique=True, null=True, blank=True, verbose_name=_('slug')) slug = models.SlugField(allow_unicode=True, unique=True, null=True, blank=True, verbose_name=_('slug'))
slogan = models.CharField(max_length=512, null=True, blank=True, verbose_name=_('slogan')) slogan = models.CharField(max_length=512, null=True, blank=True, verbose_name=_('slogan'))
@ -189,11 +178,6 @@ class VideoPlaylist(models.Model):
return timedelta(seconds=total_seconds) return timedelta(seconds=total_seconds)
def save(self, *args, **kwargs):
if not self.slug:
self.slug = generate_slug_for_model(VideoPlaylist, self.title)
super().save(*args, **kwargs)
class Meta: class Meta:
verbose_name = _('Video Playlist') verbose_name = _('Video Playlist')
verbose_name_plural = _('Video Playlists') verbose_name_plural = _('Video Playlists')

35
utils/admin.py

@ -32,41 +32,6 @@ def slugify(value):
return _unicode_slugify(value) return _unicode_slugify(value)
# ------------------------------------------------------- # -------------------------------------------------------
# Unfold Imports
import json
import random
from functools import lru_cache
from django import forms
from django.conf import settings
from django.contrib.humanize.templatetags.humanize import intcomma
from django.urls import reverse, path
from django.utils.safestring import mark_safe
from django.utils.translation import gettext_lazy as _
from django.views.generic import RedirectView
from django.utils.translation import get_language
from django.http import JsonResponse
from django.utils.html import format_html
from django.views.decorators.http import require_POST
# --- AGGRESSIVE UNICODE PATCH FOR UNFOLD 0.64.1 TABS ---
import django.utils.text
from django.template.defaultfilters import register
_original_slugify = django.utils.text.slugify
def _unicode_slugify(value, allow_unicode=True):
# We forcefully pass allow_unicode=True to preserve Russian characters for tab IDs
return _original_slugify(value, allow_unicode=True)
django.utils.text.slugify = _unicode_slugify
# We must also override the template filter explicitly because Unfold calls it directly in 0.64.1
@register.filter(is_safe=True)
def slugify(value):
return _unicode_slugify(value)
# -------------------------------------------------------
# Unfold Imports # Unfold Imports
from unfold.sites import UnfoldAdminSite from unfold.sites import UnfoldAdminSite

32
utils/mixins.py

@ -58,36 +58,8 @@ class LowercaseSlugMixin(models.Model):
abstract = True abstract = True
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
# 1. Generate slug if empty
if not self.slug:
source_value = getattr(self, self.slug_source_field, '') or ''
base_slug = slugify(source_value, allow_unicode=True) or 'slug'
base_slug = base_slug.lower()
# Ensure uniqueness
slug = base_slug
counter = 1
model_class = self.__class__
# Exclude current instance from conflict check if it exists in database
queryset = model_class.objects.all()
if self.pk:
queryset = queryset.exclude(pk=self.pk)
while queryset.filter(slug=slug).exists():
slug = f"{base_slug}-{counter}"
counter += 1
self.slug = slug
else:
# 2. Force existing slug to lowercase (handling both string and JSONField lists)
if isinstance(self.slug, str):
self.slug = self.slug.lower()
elif isinstance(self.slug, list):
for item in self.slug:
if isinstance(item, dict) and "title" in item and item["title"]:
item["title"] = str(item["title"]).lower()
from utils.slug import build_slug_for_instance
build_slug_for_instance(self, source_field=self.slug_source_field)
super().save(*args, **kwargs) super().save(*args, **kwargs)

61
utils/slug.py

@ -137,3 +137,64 @@ def generate_unique_slug(
instance=instance, instance=instance,
keep_words=999, # No word limit keep_words=999, # No word limit
) )
def build_slug_for_instance(instance, source_field='title', slug_field='slug', max_length=100):
"""
Helper to automatically build, update, and ensure uniqueness/lowercase for a model's slug.
If the record is new: generates slug from source_field if slug is empty.
If the record is being updated: if source_field has changed, regenerates slug.
"""
model_class = instance.__class__
source_val = getattr(instance, source_field, None)
# Helper to extract raw text if source value is a list (e.g. JSONField translation list)
def extract_text(val):
if not val:
return ""
if isinstance(val, str):
return val.strip()
if isinstance(val, list) and len(val) > 0:
first_item = val[0]
if isinstance(first_item, dict):
return (first_item.get('text') or first_item.get('value') or first_item.get('title') or '').strip()
return str(first_item).strip()
return str(val).strip()
text = extract_text(source_val)
current_slug = getattr(instance, slug_field, None)
is_new = instance.pk is None
should_generate = False
if is_new:
if not current_slug or not str(current_slug).strip():
should_generate = True
else:
# Fetch the original source and slug values from DB
orig = model_class.objects.filter(pk=instance.pk).values(source_field, slug_field).first()
if orig:
orig_source_val = orig.get(source_field)
orig_slug = orig.get(slug_field)
orig_text = extract_text(orig_source_val)
# If the text from the source field changed, or the slug is empty
if orig_text != text or not orig_slug or not str(orig_slug).strip():
should_generate = True
if should_generate:
# Generate new slug using generate_smart_slug to handle uniqueness, unicode, and limits safely
new_slug = generate_smart_slug(
text=text,
model_class=model_class,
max_length=max_length,
field_name=slug_field,
instance=instance,
keep_words=999, # Keep all words up to max_length
)
setattr(instance, slug_field, new_slug.lower())
else:
# Enforce lowercase on existing slug
if current_slug and isinstance(current_slug, str):
setattr(instance, slug_field, current_slug.lower())
Loading…
Cancel
Save