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.
139 lines
4.7 KiB
139 lines
4.7 KiB
"""
|
|
Smart slug generation utility for Django models.
|
|
Handles truncation, counter-based uniqueness, and word-limit preservation.
|
|
"""
|
|
|
|
from django.utils.text import slugify
|
|
|
|
|
|
def generate_smart_slug(
|
|
text: str,
|
|
model_class,
|
|
max_length: int = 100,
|
|
field_name: str = "slug",
|
|
instance=None,
|
|
keep_words: int = 8,
|
|
reserve_for_counter: int = 5, # Reserve space for "-1", "-2", etc. (max 5 chars)
|
|
) -> str:
|
|
"""
|
|
Generate a unique, meaningful slug with a max length and word limit.
|
|
|
|
This function:
|
|
1. Extracts the first N words from the text
|
|
2. Slugifies them
|
|
3. Truncates to max_length (reserving space for counter if needed)
|
|
4. Adds a counter (-1, -2, etc.) if the slug already exists
|
|
|
|
Args:
|
|
text (str): The text to slugify (e.g., hadis title)
|
|
model_class: The Django model class (e.g., Hadis)
|
|
max_length (int): Maximum slug length (default: 100)
|
|
field_name (str): The slug field name (default: 'slug')
|
|
instance: Current instance to exclude from uniqueness check (optional)
|
|
keep_words (int): Number of words to keep (default: 8)
|
|
reserve_for_counter (int): Space reserved for counter suffix (default: 5, enough for "-9999")
|
|
|
|
Returns:
|
|
str: A unique slug within max_length and word constraints
|
|
|
|
Raises:
|
|
ValueError: If unable to generate unique slug after 1000 attempts
|
|
|
|
Examples:
|
|
>>> from utils.slugs import generate_smart_slug
|
|
>>> from hadis.models import Hadis
|
|
>>>
|
|
>>> text = "Fatwa on Combining Prayers While Traveling and Missing the Congregational Prayer"
|
|
>>> slug = generate_smart_slug(text, Hadis, keep_words=4)
|
|
>>> print(slug)
|
|
'fatwa-on-combining-prayers'
|
|
>>>
|
|
>>> # With counter for duplicate
|
|
>>> slug2 = generate_smart_slug(text, Hadis, keep_words=4, instance=hadis)
|
|
>>> print(slug2)
|
|
'fatwa-on-combining-prayers-1'
|
|
"""
|
|
|
|
# Validation: Check if text is valid
|
|
if not text or not isinstance(text, str):
|
|
fallback = (
|
|
f"{model_class.__name__.lower()}-{instance.id}"
|
|
if instance and instance.pk
|
|
else f"{model_class.__name__.lower()}-new"
|
|
)
|
|
return fallback
|
|
|
|
# ==================== STEP 1: Extract first N words ====================
|
|
words = text.strip().split()[:keep_words]
|
|
text_shortened = " ".join(words)
|
|
|
|
# ==================== STEP 2: Slugify ====================
|
|
base_slug = slugify(text_shortened, allow_unicode=True)
|
|
|
|
# ==================== STEP 3: Truncate to max_length ====================
|
|
# Reserve space for potential counter suffix
|
|
available_length = max_length - reserve_for_counter
|
|
slug = base_slug[:available_length].rstrip("-")
|
|
|
|
# ==================== STEP 4: Ensure uniqueness with counter ====================
|
|
counter = 0 # Start at 0 for first attempt (no counter)
|
|
original_slug = slug
|
|
|
|
while True:
|
|
# Build the final slug
|
|
if counter == 0:
|
|
final_slug = slug # First attempt: no counter
|
|
else:
|
|
counter_suffix = f"-{counter}"
|
|
# Ensure total doesn't exceed max_length
|
|
available_for_base = max_length - len(counter_suffix)
|
|
final_slug = original_slug[:available_for_base].rstrip("-") + counter_suffix
|
|
|
|
# Build filter query
|
|
filter_kwargs = {field_name: final_slug}
|
|
qs = model_class.objects.filter(**filter_kwargs)
|
|
|
|
# Exclude current instance if provided
|
|
if instance and instance.pk:
|
|
qs = qs.exclude(pk=instance.pk)
|
|
|
|
# If no conflict, slug is unique
|
|
if not qs.exists():
|
|
return final_slug
|
|
|
|
# Try with counter
|
|
counter += 1
|
|
|
|
# Safety: prevent infinite loop
|
|
if counter > 1000:
|
|
raise ValueError(
|
|
f"Could not generate unique slug for text: '{text}'. "
|
|
f"Attempted 1000+ variations of '{original_slug}'"
|
|
)
|
|
|
|
return slug
|
|
|
|
|
|
# ============================================================================
|
|
# Backward compatibility aliases
|
|
# ============================================================================
|
|
|
|
def generate_unique_slug(
|
|
text: str,
|
|
model_class,
|
|
max_length: int = 100,
|
|
field_name: str = "slug",
|
|
instance=None,
|
|
) -> str:
|
|
"""
|
|
Backward compatible version without word limit.
|
|
Uses default keep_words=999 (essentially unlimited).
|
|
"""
|
|
return generate_smart_slug(
|
|
text=text,
|
|
model_class=model_class,
|
|
max_length=max_length,
|
|
field_name=field_name,
|
|
instance=instance,
|
|
keep_words=999, # No word limit
|
|
)
|