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
class ArticleCategory(models.Model):
class ArticleCategory(LowercaseSlugMixin, models.Model):
title = models.CharField(max_length=255, verbose_name=_('title'))
slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug'))
@ -18,18 +18,13 @@ class ArticleCategory(models.Model):
def __str__(self):
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:
verbose_name = _('Article Category')
verbose_name_plural = _('Article Categories')
ordering = ['order']
class ArticleCollection(models.Model):
class ArticleCollection(LowercaseSlugMixin, models.Model):
class DisplayPosition(models.TextChoices):
PINNED = 'pinned', _('Pinned')
MIDDLE = 'middle', _('Middle Section')
@ -60,11 +55,6 @@ class ArticleCollection(models.Model):
def __str__(self):
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:
verbose_name = _('Article Collection')
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 django.utils.text import slugify
from django.conf import settings
from utils.mixins import LowercaseSlugMixin
class HadisSect(models.Model):
@ -63,7 +64,7 @@ class HadisSect(models.Model):
ordering = ('order',)
class HadisCategory(MPTTModel):
class HadisCategory(LowercaseSlugMixin, MPTTModel):
class SourceType(models.TextChoices):
QURAN = 'quran', _('Quran')
HADITH = 'hadith', _('Hadith')
@ -92,63 +93,10 @@ class HadisCategory(MPTTModel):
f'Your sect: {self.sect.sect_type}')
)
slug_source_field = 'title'
def save(self, *args, **kwargs):
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)
@property

146
apps/hadis/models/hadis.py

@ -9,7 +9,8 @@ from .reference import BookReference
from utils.slug import generate_smart_slug
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'))
slug = models.SlugField(max_length=255, unique=True, verbose_name=_('slug'), blank=True)
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'))
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'] if self.title else "Untitled Collection"
@ -164,7 +107,8 @@ class HadisTag(models.Model):
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'))
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'))
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'] if self.title else str(self.id)
@ -517,27 +402,8 @@ class HadisCorrection(models.Model):
def save(self, *args, **kwargs):
# 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:
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.conf import settings
from typing import Optional
from utils.mixins import LowercaseSlugMixin
class BookSubjectArea(models.Model):
title = models.JSONField(default = list , verbose_name=_('Title'))
@ -83,7 +84,8 @@ class BookType(models.Model):
return tr.get('text', '')
return None
class BookReference(models.Model):
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
@ -192,63 +194,7 @@ class BookReference(models.Model):
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):
"""
@ -377,7 +323,8 @@ class BookReferenceDocument(models.Model):
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
"""
@ -436,32 +383,6 @@ class BookAuthor(models.Model):
return None
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)
self.researcher_profiles.all().update(name=self.name, slug=self.slug)
@ -528,7 +449,8 @@ class BookAttribute(models.Model):
return None
class BookResearcher(models.Model):
class BookResearcher(LowercaseSlugMixin, models.Model):
slug_source_field = 'name'
book_edition = models.ForeignKey(
'BookEdition',
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 utils.slug import generate_smart_slug
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)
Represents the classification level of narrators in hadis chains
@ -122,67 +123,14 @@ class NarratorLayer(models.Model):
self.slug = slug
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'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,null=True)
def save(self, *args, **kwargs):
if not self.slug or (isinstance(self.slug, str) and self.slug.strip() == ''):
# Try to get text from title field with robust error handling
try:
if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0]
if isinstance(first_item, dict):
text = first_item.get('text', '').strip()
if text:
slug = slugify(text)
# Ensure uniqueness
counter = 1
base_slug = slug
while TransmitterReliability.objects.filter(slug=slug).exclude(pk=self.pk).exists():
slug = f"{base_slug}-{counter}"
counter += 1
self.slug = slug
else:
# Fallback to a timestamp-based slug
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"reliability-{suffix}"
counter = 1
while TransmitterReliability.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"reliability-{suffix}-{counter}"
counter += 1
self.slug = base_slug
else:
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"reliability-{suffix}"
counter = 1
while TransmitterReliability.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"reliability-{suffix}-{counter}"
counter += 1
self.slug = base_slug
else:
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"reliability-{suffix}"
counter = 1
while TransmitterReliability.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"reliability-{suffix}-{counter}"
counter += 1
self.slug = base_slug
except (IndexError, KeyError, AttributeError, TypeError):
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"reliability-{suffix}"
counter = 1
while TransmitterReliability.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"reliability-{suffix}-{counter}"
counter += 1
self.slug = base_slug
super().save(*args, **kwargs)
def __str__(self):
return self.title[0]['text'] if self.title else str(self.id)
@ -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):
# VERY_RELIABLE = 'very_reliable', _('Very Reliable')
# RELIABLE = 'reliable', _('Reliable')
@ -313,63 +262,7 @@ class Transmitters(models.Model):
return f"{settings.DOVODI_DOMAIN}/arguments/narrators/{self.slug}"
return None
def save(self, *args, **kwargs):
if not self.slug or (isinstance(self.slug, str) and self.slug.strip() == ''):
# Try to get text from full_name field with robust error handling
try:
if self.full_name and isinstance(self.full_name, list) and len(self.full_name) > 0:
first_item = self.full_name[0]
if isinstance(first_item, dict):
name_text = first_item.get('text', '').strip()
if name_text:
base_slug = slugify(name_text, allow_unicode=True)
slug = base_slug
counter = 1
while Transmitters.objects.filter(slug=slug).exclude(pk=self.pk).exists():
slug = f"{base_slug}-{counter}"
counter += 1
self.slug = slug
else:
# Fallback if text is empty
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"transmitter-{suffix}"
counter = 1
while Transmitters.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"transmitter-{suffix}-{counter}"
counter += 1
self.slug = base_slug
else:
# Fallback if structure is invalid
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"transmitter-{suffix}"
counter = 1
while Transmitters.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"transmitter-{suffix}-{counter}"
counter += 1
self.slug = base_slug
else:
# Fallback if full_name is empty or invalid
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"transmitter-{suffix}"
counter = 1
while Transmitters.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"transmitter-{suffix}-{counter}"
counter += 1
self.slug = base_slug
except (IndexError, KeyError, AttributeError, TypeError):
# Fallback on any error
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"transmitter-{suffix}"
counter = 1
while Transmitters.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"transmitter-{suffix}-{counter}"
counter += 1
self.slug = base_slug
super().save(*args, **kwargs)
def _get_json_field(self, field_name: str, lang: Optional[str]=None , fallback: str = "en"):
"""
@ -492,66 +385,13 @@ class HadisTransmitter(models.Model):
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'))
slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,null=True)
def save(self, *args, **kwargs):
if not self.slug or (isinstance(self.slug, str) and self.slug.strip() == ''):
# Try to get text from title field with robust error handling
try:
if self.title and isinstance(self.title, list) and len(self.title) > 0:
first_item = self.title[0]
if isinstance(first_item, dict):
text = first_item.get('text', '').strip()
if text:
slug = slugify(text)
# Ensure uniqueness
counter = 1
base_slug = slug
while OpinionStatus.objects.filter(slug=slug).exclude(pk=self.pk).exists():
slug = f"{base_slug}-{counter}"
counter += 1
self.slug = slug
else:
# Fallback to a timestamp-based slug
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"opinion-{suffix}"
counter = 1
while OpinionStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"opinion-{suffix}-{counter}"
counter += 1
self.slug = base_slug
else:
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"opinion-{suffix}"
counter = 1
while OpinionStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"opinion-{suffix}-{counter}"
counter += 1
self.slug = base_slug
else:
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"opinion-{suffix}"
counter = 1
while OpinionStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"opinion-{suffix}-{counter}"
counter += 1
self.slug = base_slug
except (IndexError, KeyError, AttributeError, TypeError):
import time
suffix = int(time.time() * 1000) % 1000000
base_slug = f"opinion-{suffix}"
counter = 1
while OpinionStatus.objects.filter(slug=base_slug).exclude(pk=self.pk).exists():
base_slug = f"opinion-{suffix}-{counter}"
counter += 1
self.slug = base_slug
super().save(*args, **kwargs)
def __str__(self):
return self.title[0]['text'] if self.title else str(self.id)
@ -695,37 +535,8 @@ class TransmitterOriginalText(models.Model):
"""
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
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
class BookCollection(models.Model):
class BookCollection(LowercaseSlugMixin, models.Model):
class DisplayPosition(models.TextChoices):
PINNED = 'pinned', _('Pinned')
MIDDLE = 'middle', _('Middle Section')
@ -38,11 +38,6 @@ class BookCollection(models.Model):
verbose_name = _('Book Collection')
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):
@ -67,7 +62,7 @@ class MiddleBookCollection(BookCollection):
class Category(models.Model):
class Category(LowercaseSlugMixin, models.Model):
title = models.CharField(max_length=255)
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
status = models.BooleanField(default=True, verbose_name=_('status'))
@ -79,11 +74,6 @@ class Category(models.Model):
def __str__(self):
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
def books_count(self):
"""Return the number of books in this category"""
@ -94,7 +84,8 @@ class Category(models.Model):
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'))
slug = models.SlugField(max_length=255, unique=True, blank=True, null=True)
thumbnail = models.ImageField(
@ -117,11 +108,6 @@ class Author(models.Model):
def __str__(self):
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
def books_count(self):
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
class PodcastCategory(models.Model):
class PodcastCategory(LowercaseSlugMixin, models.Model):
title = models.CharField(max_length=255, verbose_name=_('title'))
slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug'))
@ -18,18 +18,13 @@ class PodcastCategory(models.Model):
def __str__(self):
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:
verbose_name = _('Podcast Category')
verbose_name_plural = _('Podcast Categories')
ordering = ['order']
class PodcastCollection(models.Model):
class PodcastCollection(LowercaseSlugMixin, models.Model):
class DisplayPosition(models.TextChoices):
PINNED = 'pinned', _('Pinned')
MIDDLE = 'middle', _('Middle Section')
@ -54,11 +49,6 @@ class PodcastCollection(models.Model):
def __str__(self):
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:
verbose_name = _('Podcast Collection')
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
class VideoCategory(models.Model):
class VideoCategory(LowercaseSlugMixin, models.Model):
title = models.CharField(max_length=255, verbose_name=_('title'))
slug = models.SlugField(allow_unicode=True, unique=True, verbose_name=_('slug'))
@ -20,18 +20,13 @@ class VideoCategory(models.Model):
def __str__(self):
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:
verbose_name = _('Video Category')
verbose_name_plural = _('Video Categories')
ordering = ['order']
class VideoCollection(models.Model):
class VideoCollection(LowercaseSlugMixin, models.Model):
class DisplayPosition(models.TextChoices):
PINNED = 'pinned', _('Pinned')
MIDDLE = 'middle', _('Middle Section')
@ -56,12 +51,6 @@ class VideoCollection(models.Model):
def __str__(self):
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:
verbose_name = _('Video Collection')
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'))
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'))
@ -189,11 +178,6 @@ class VideoPlaylist(models.Model):
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:
verbose_name = _('Video Playlist')
verbose_name_plural = _('Video Playlists')

35
utils/admin.py

@ -32,41 +32,6 @@ def 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
from unfold.sites import UnfoldAdminSite

32
utils/mixins.py

@ -58,36 +58,8 @@ class LowercaseSlugMixin(models.Model):
abstract = True
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)

61
utils/slug.py

@ -137,3 +137,64 @@ def generate_unique_slug(
instance=instance,
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