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.
 
 

258 lines
9.1 KiB

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