""" Django Management Command: Regenerate HadisCorrection Slugs This command: 1. Takes all existing HadisCorrection 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 Usage: python manage.py regenerate_hadis_corrections_slugs python manage.py regenerate_hadis_corrections_slugs --max-length 75 python manage.py regenerate_hadis_corrections_slugs --keep-words 6 python manage.py regenerate_hadis_corrections_slugs --dry-run """ from collections import defaultdict from django.core.management.base import BaseCommand from django.db import transaction from apps.hadis.models import HadisCorrection # ← adjust import path to your app from utils.slug import generate_smart_slug # ← your existing helper class Command(BaseCommand): help = ( "Regenerate smart slugs for all HadisCorrection 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("šŸ”„ HADISCORRECTION SLUG REGENERATION WITH DUPLICATE HANDLING\n") self.stdout.write("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 corrections qs = HadisCorrection.objects.all().order_by("id") total = qs.count() self.stdout.write(f"Step 1: Analyzing {total} HadisCorrection objects...\n") # Dictionary to track duplicate slugs: {base_slug: [correction_ids]} slug_map = defaultdict(list) correction_slug_map = {} # {correction_id: data} # First pass: Generate base slugs and identify duplicates for correction in qs: try: # 1) Decide which field to use as base text for slug # Adjust this depending on your HadisCorrection model: # - if you have `title` JSONField like Hadis: # correction.title[0]['text'] # - or maybe `text`, `summary`, etc. title_text = None # Example: using a JSONField called `title` if getattr(correction, "title", None) and isinstance(correction.title, list): first_item = correction.title[0] if isinstance(first_item, dict): title_text = first_item.get("text") # Fallback: use plain text field or id if not title_text: if hasattr(correction, "text") and isinstance(correction.text, str): title_text = correction.text else: title_text = f"hadis-correction-{correction.id}" # Generate base slug (without counter) base_slug = self._generate_base_slug( title_text, max_length - 5, # reserve for counter keep_words, ) correction_slug_map[correction.id] = { "base_slug": base_slug, "title_text": title_text, "old_slug": correction.slug or "(empty)", } slug_map[base_slug].append(correction.id) except Exception as e: self.stdout.write( self.style.ERROR(f" āŒ Error analyzing HadisCorrection {correction.id}: {str(e)}") ) self.stdout.write("āœ… Analysis complete!\n") self.stdout.write("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)} corrections") for idx, corr_id in enumerate(ids): counter = "" if idx == 0 else f"-{idx}" self.stdout.write( f" - HadisCorrection {corr_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 correction in qs: try: slug_info = correction_slug_map.get(correction.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: duplicate_ids = slug_map[base_slug] position = duplicate_ids.index(correction.id) 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 = correction.slug != new_slug if changed: self.stdout.write( f" [{correction.id:5d}] {old_slug:45s} → {new_slug}" ) if not dry_run: correction.slug = new_slug correction.save(update_fields=["slug"]) updated += 1 else: unchanged += 1 except Exception as e: error_msg = f"HadisCorrection {correction.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} HadisCorrection 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 corrections. """ from django.utils.text import slugify if not text or not isinstance(text, str): return "hadis-correction-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