""" 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 )