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.
 
 
 
 
 

234 lines
8.7 KiB

#!/usr/bin/env python3
"""
High-Performance Script to Regenerate and Update Slugs for:
- HadisCategory
- Hadis
- Transmitters
Features:
- Human-readable slugs generated from actual multilingual titles and names
- Fast in-memory deduplication and uniqueness guarantees
- Safe two-phase atomic update using raw SQL temporary assignment + bulk update
- Supports --dry-run flag for inspection before applying
"""
import os
import sys
import re
import time
import argparse
from pathlib import Path
from django.utils.text import slugify
# Force UTF-8 stdout for Windows consoles
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8")
BASE_DIR = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(BASE_DIR))
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.base')
import django
django.setup()
from django.db import transaction, connection
from apps.hadis.models import HadisCategory, Hadis, Transmitters
DUMMY_NAMES = {
"al-imam-al-hafiz-abu-isa-muhammad-ibn-isa-ibn-sawrah-ibn-musa-ibn-al-dahhak-al-sulami-al-tirmidhi",
"al-imam-al-hafiz-abu-isa-muhammad",
"الإمام الحافظ أبو عيسى محمد بن عيسى بن سَوْرَة بن موسى بن الضحاک السُّلَمي الترمذي",
"اسم-عربی",
}
def extract_best_text(json_val, preferred_langs=('en', 'ru', 'fa', 'ar')):
"""Extract the best text from a multilingual JSONField (list of dicts)."""
if not json_val:
return ""
if isinstance(json_val, str):
return json_val.strip()
if isinstance(json_val, list):
for lang in preferred_langs:
for item in json_val:
if isinstance(item, dict) and item.get('language_code') == lang:
val = item.get('text') or item.get('title') or item.get('value') or item.get('name')
if val and str(val).strip():
return str(val).strip()
for item in json_val:
if isinstance(item, dict):
val = item.get('text') or item.get('title') or item.get('value') or item.get('name')
if val and str(val).strip():
return str(val).strip()
elif isinstance(item, str) and item.strip():
return item.strip()
return ""
def extract_transmitter_text(transmitter):
"""Extract best specific name for a transmitter, avoiding dummy/repeated texts."""
candidates = []
for field_val in (transmitter.full_name, transmitter.known_as, transmitter.nickname):
if not field_val or not isinstance(field_val, list):
continue
for lang in ('ru', 'fa', 'en', 'ar'):
for item in field_val:
if isinstance(item, dict) and item.get('language_code') == lang:
val = (item.get('text') or item.get('title') or '').strip()
if val and val not in DUMMY_NAMES and not val.startswith("313"):
candidates.append(val)
if candidates:
return candidates[0]
return extract_best_text(transmitter.full_name) or extract_best_text(transmitter.known_as) or ""
def clean_slug_text(text: str, max_len: int = 80) -> str:
"""Clean and slugify text while preserving readability and length constraints."""
if not text:
return ""
cleaned = re.sub(r'[\(\)\[\]\{\}\<\>\'\"\`\:\;\,\.\?\!\@\#\$\%\^\&\*\+\=\|\/\\]+', ' ', text)
slug = slugify(cleaned, allow_unicode=True).strip('-').lower()
if len(slug) > max_len:
slug = slug[:max_len].rstrip('-')
return slug
def generate_unique_slug(base_slug: str, used_slugs: set, max_len: int = 90) -> str:
"""Ensure slug uniqueness using an in-memory set with counter suffix."""
if not base_slug:
base_slug = "item"
candidate = base_slug
counter = 1
while candidate in used_slugs:
counter += 1
suffix = f"-{counter}"
avail_len = max_len - len(suffix)
candidate = f"{base_slug[:avail_len].rstrip('-')}{suffix}"
used_slugs.add(candidate)
return candidate
def update_categories_slugs(dry_run=False):
print("\n📂 [1/3] Processing HadisCategory...")
categories = list(HadisCategory.objects.all().order_by('id'))
used_slugs = set()
sample_changes = []
for cat in categories:
raw_text = extract_best_text(cat.title, ('en', 'ru', 'fa', 'ar')) or extract_best_text(cat.description)
base_slug = clean_slug_text(raw_text, max_len=75)
if not base_slug:
base_slug = f"category-{cat.source_type or 'item'}-{cat.id}"
new_slug = generate_unique_slug(base_slug, used_slugs)
if len(sample_changes) < 5 and cat.slug != new_slug:
sample_changes.append((cat.id, cat.slug, new_slug, raw_text))
cat.slug = new_slug
print(f" Total categories: {len(categories)}")
for item_id, old_s, new_s, title in sample_changes:
print(f" • ID {item_id:4d} | '{title[:30]}' -> {old_s} ➔ {new_s}")
if not dry_run:
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute("UPDATE hadis_hadiscategory SET slug = CONCAT('tmp-c-', id);")
HadisCategory.objects.bulk_update(categories, ['slug'], batch_size=1000)
print(" ✅ All categories updated successfully.")
def update_hadis_slugs(dry_run=False):
print("\n📜 [2/3] Processing Hadis...")
hadiths = list(Hadis.objects.all().order_by('id'))
used_slugs = set()
sample_changes = []
for h in hadiths:
raw_text = extract_best_text(h.title, ('en', 'ru', 'fa', 'ar')) or extract_best_text(h.translation) or extract_best_text(h.hadis_status_text)
base_slug = clean_slug_text(raw_text, max_len=75)
if not base_slug or (base_slug.isdigit() and len(base_slug) <= 3):
if h.number:
base_slug = f"hadis-{h.number}" + (f"-{base_slug}" if base_slug else "")
else:
base_slug = f"hadis-{h.id}"
new_slug = generate_unique_slug(base_slug, used_slugs)
if len(sample_changes) < 5 and h.slug != new_slug:
sample_changes.append((h.id, h.slug, new_slug, raw_text or str(h.number)))
h.slug = new_slug
print(f" Total hadiths: {len(hadiths)}")
for item_id, old_s, new_s, title in sample_changes:
print(f" • ID {item_id:4d} | '{title[:30]}' -> {old_s} ➔ {new_s}")
if not dry_run:
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute("UPDATE hadis_hadis SET slug = CONCAT('tmp-h-', id);")
Hadis.objects.bulk_update(hadiths, ['slug'], batch_size=1000)
print(" ✅ All hadiths updated successfully.")
def update_transmitters_slugs(dry_run=False):
print("\n👤 [3/3] Processing Transmitters...")
transmitters = list(Transmitters.objects.all().order_by('id'))
used_slugs = set()
sample_changes = []
for tr in transmitters:
raw_text = extract_transmitter_text(tr)
base_slug = clean_slug_text(raw_text, max_len=75)
if not base_slug:
base_slug = f"transmitter-{tr.id}"
new_slug = generate_unique_slug(base_slug, used_slugs)
if len(sample_changes) < 5 and tr.slug != new_slug:
sample_changes.append((tr.id, tr.slug, new_slug, raw_text))
tr.slug = new_slug
print(f" Total transmitters: {len(transmitters)}")
for item_id, old_s, new_s, name in sample_changes:
print(f" • ID {item_id:4d} | '{name[:30]}' -> {old_s} ➔ {new_s}")
if not dry_run:
with transaction.atomic():
with connection.cursor() as cursor:
cursor.execute("UPDATE hadis_transmitters SET slug = CONCAT('tmp-t-', id);")
Transmitters.objects.bulk_update(transmitters, ['slug'], batch_size=1000)
print(" ✅ All transmitters updated successfully.")
def main():
parser = argparse.ArgumentParser(description="Update readable slugs for Categories, Hadiths, and Transmitters.")
parser.add_argument("--dry-run", action="store_true", help="Simulate slug generation without saving to database.")
args = parser.parse_args()
start_time = time.time()
mode_text = "DRY RUN (Preview Only)" if args.dry_run else "EXECUTE (Database Update)"
print(f"\n🚀 Starting Slug Optimization [{mode_text}]...")
print("=" * 70)
update_categories_slugs(dry_run=args.dry_run)
update_hadis_slugs(dry_run=args.dry_run)
update_transmitters_slugs(dry_run=args.dry_run)
duration = time.time() - start_time
print("\n" + "=" * 70)
print(f"✨ Completed in {duration:.2f} seconds!")
if __name__ == "__main__":
main()