""" Django Management Command: Regenerate Hadis Slugs This command: 1. Takes all existing Hadis objects 2. Extracts their title (first 8 words max) 3. Generates smart, short, meaningful slugs 4. Handles uniqueness with counters (-1, -2, -3, etc.) 5. REPLACES all old slug values with new ones 6. Detects and numbers duplicates automatically Key feature: - If multiple hadis have the same shortened title, they get numbered: - hadis-1830: "достоинство-молитвы-и-ее-место" → "достоинство-молитвы-и-ее-место" - hadis-1831: same title → "достоинство-молитвы-и-ее-место-1" - hadis-1832: same title → "достоинство-молитвы-и-ее-место-2" Usage: python manage.py regenerate_hadis_slugs python manage.py regenerate_hadis_slugs --max-length 75 python manage.py regenerate_hadis_slugs --keep-words 6 python manage.py regenerate_hadis_slugs --dry-run """ from django.core.management.base import BaseCommand from django.db import transaction from collections import defaultdict from apps.hadis.models import Hadis from utils.slug import generate_smart_slug class Command(BaseCommand): help = ( "Regenerate smart slugs for all Hadis objects. " "Replaces existing slug values with optimized, short, meaningful ones. " "Automatically adds counters for duplicates." ) def add_arguments(self, parser): """Add optional command-line arguments""" parser.add_argument( "--max-length", type=int, default=100, help="Maximum slug length (default: 100)", ) parser.add_argument( "--keep-words", type=int, default=8, help="Maximum number of words to keep in slug (default: 8)", ) parser.add_argument( "--dry-run", action="store_true", help="Show what would change without saving", ) @transaction.atomic def handle(self, *args, **options): max_length = options["max_length"] keep_words = options["keep_words"] dry_run = options["dry_run"] self.stdout.write(self.style.HTTP_INFO("=" * 80)) self.stdout.write("🔄 HADIS SLUG REGENERATION WITH DUPLICATE HANDLING\n") self.stdout.write(f"Configuration:") self.stdout.write(f" • Max length: {max_length} chars") self.stdout.write(f" • Keep words: {keep_words} words") self.stdout.write(f" • Dry run: {'Yes (no changes)' if dry_run else 'No (will save)'}") self.stdout.write(self.style.HTTP_INFO("=" * 80) + "\n") # Get all hadis qs = Hadis.objects.all().order_by("id") total = qs.count() self.stdout.write(f"Step 1: Analyzing {total} hadis objects...\n") # Dictionary to track duplicate slugs: {base_slug: [hadis_ids]} slug_map = defaultdict(list) hadis_slug_map = {} # {hadis_id: generated_slug} # First pass: Generate slugs and identify duplicates for hadis in qs: try: # Extract title text title_text = None if hadis.title and isinstance(hadis.title, list) and hadis.title: first_item = hadis.title[0] if isinstance(first_item, dict): title_text = first_item.get("text") # Fallback if no title if not title_text: title_text = f"hadis-{hadis.number or hadis.id}" # Generate base slug (without counter) base_slug = self._generate_base_slug( title_text, max_length - 5, # Reserve space for counter keep_words, ) hadis_slug_map[hadis.id] = { "base_slug": base_slug, "title_text": title_text, "old_slug": hadis.slug or "(empty)", } slug_map[base_slug].append(hadis.id) except Exception as e: self.stdout.write( self.style.ERROR(f" ❌ Error analyzing Hadis {hadis.id}: {str(e)}") ) self.stdout.write(f"✅ Analysis complete!\n") self.stdout.write(f"Step 2: Detecting duplicates...\n") # Identify groups with duplicates duplicate_groups = { slug: ids for slug, ids in slug_map.items() if len(ids) > 1 } if duplicate_groups: self.stdout.write( self.style.WARNING( f"⚠️ Found {len(duplicate_groups)} groups with duplicate slugs:\n" ) ) for base_slug, ids in sorted(duplicate_groups.items()): self.stdout.write(f" • '{base_slug}' → {len(ids)} hadis") for idx, hadis_id in enumerate(ids): counter = "" if idx == 0 else f"-{idx}" self.stdout.write( f" - Hadis {hadis_id}: {base_slug}{counter}" ) else: self.stdout.write("✅ No duplicates found! All slugs are unique.\n") self.stdout.write("\nStep 3: Applying slugs with counters...\n") # Second pass: Apply slugs with counters for duplicates updated = 0 unchanged = 0 errors = [] for hadis in qs: try: slug_info = hadis_slug_map.get(hadis.id) if not slug_info: continue base_slug = slug_info["base_slug"] old_slug = slug_info["old_slug"] # If this slug has duplicates, add counter if len(slug_map[base_slug]) > 1: # Find position of this hadis in the duplicate group duplicate_ids = slug_map[base_slug] position = duplicate_ids.index(hadis.id) # First one gets no counter, rest get -1, -2, etc. if position == 0: new_slug = base_slug else: counter_suffix = f"-{position}" available_length = max_length - len(counter_suffix) new_slug = ( base_slug[:available_length].rstrip("-") + counter_suffix ) else: new_slug = base_slug changed = hadis.slug != new_slug if changed: self.stdout.write( f" [{hadis.id:5d}] {old_slug:45s} → {new_slug}" ) if not dry_run: hadis.slug = new_slug hadis.save(update_fields=["slug"]) updated += 1 else: unchanged += 1 except Exception as e: error_msg = f"Hadis {hadis.id}: {str(e)}" self.stdout.write(self.style.ERROR(f" ❌ {error_msg}")) errors.append(error_msg) # Summary self.stdout.write("\n" + self.style.HTTP_INFO("=" * 80)) self.stdout.write("📊 RESULTS\n") self.stdout.write(f" ✅ Updated: {updated}") self.stdout.write(f" ➡️ Unchanged: {unchanged}") self.stdout.write(f" ❌ Errors: {len(errors)}") self.stdout.write( f" 🔢 Duplicate groups handled: {len(duplicate_groups)}" ) if dry_run: self.stdout.write( self.style.WARNING( "\n ⚠️ DRY RUN MODE: No changes were saved." ) ) self.stdout.write( self.style.WARNING( " Run without --dry-run to apply changes." ) ) if errors: self.stdout.write(self.style.ERROR("\n❌ ERRORS ENCOUNTERED:")) for error in errors: self.stdout.write(f" • {error}") self.stdout.write(self.style.HTTP_INFO("=" * 80) + "\n") if not dry_run and updated > 0: self.stdout.write( self.style.SUCCESS( f"✅ Successfully regenerated {updated} slug(s)!" ) ) elif dry_run: self.stdout.write( self.style.HTTP_INFO( f"ℹ️ Would update {updated} slug(s) in production run." ) ) def _generate_base_slug(self, text: str, max_length: int, keep_words: int) -> str: """ Generate a base slug without counter. Returns the base slug that might be used for multiple hadis. """ from django.utils.text import slugify if not text or not isinstance(text, str): return "hadis-unknown" # Extract first N words words = text.strip().split()[:keep_words] text_shortened = " ".join(words) # Slugify base_slug = slugify(text_shortened, allow_unicode=True) # Truncate slug = base_slug[:max_length].rstrip("-") return slug