|
|
|
@ -1,38 +1,33 @@ |
|
|
|
""" |
|
|
|
Django Management Command: Regenerate Hadis Slugs |
|
|
|
Django Management Command: Regenerate HadisCorrection Slugs |
|
|
|
|
|
|
|
This command: |
|
|
|
1. Takes all existing Hadis objects |
|
|
|
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 |
|
|
|
|
|
|
|
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 |
|
|
|
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 collections import defaultdict |
|
|
|
|
|
|
|
from apps.hadis.models import Hadis |
|
|
|
from utils.slug import generate_smart_slug |
|
|
|
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 Hadis objects. " |
|
|
|
"Regenerate smart slugs for all HadisCorrection objects. " |
|
|
|
"Replaces existing slug values with optimized, short, meaningful ones. " |
|
|
|
"Automatically adds counters for duplicates." |
|
|
|
) |
|
|
|
@ -64,59 +59,68 @@ class Command(BaseCommand): |
|
|
|
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("🔄 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 hadis |
|
|
|
qs = Hadis.objects.all().order_by("id") |
|
|
|
# Get all corrections |
|
|
|
qs = HadisCorrection.objects.all().order_by("id") |
|
|
|
total = qs.count() |
|
|
|
|
|
|
|
self.stdout.write(f"Step 1: Analyzing {total} hadis objects...\n") |
|
|
|
self.stdout.write(f"Step 1: Analyzing {total} HadisCorrection objects...\n") |
|
|
|
|
|
|
|
# Dictionary to track duplicate slugs: {base_slug: [hadis_ids]} |
|
|
|
# Dictionary to track duplicate slugs: {base_slug: [correction_ids]} |
|
|
|
slug_map = defaultdict(list) |
|
|
|
hadis_slug_map = {} # {hadis_id: generated_slug} |
|
|
|
correction_slug_map = {} # {correction_id: data} |
|
|
|
|
|
|
|
# First pass: Generate slugs and identify duplicates |
|
|
|
for hadis in qs: |
|
|
|
# First pass: Generate base slugs and identify duplicates |
|
|
|
for correction in qs: |
|
|
|
try: |
|
|
|
# Extract title text |
|
|
|
# 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 |
|
|
|
if hadis.title and isinstance(hadis.title, list) and hadis.title: |
|
|
|
first_item = hadis.title[0] |
|
|
|
|
|
|
|
# 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 if no title |
|
|
|
# Fallback: use plain text field or id |
|
|
|
if not title_text: |
|
|
|
title_text = f"hadis-{hadis.number or hadis.id}" |
|
|
|
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 space for counter |
|
|
|
max_length - 5, # reserve for counter |
|
|
|
keep_words, |
|
|
|
) |
|
|
|
|
|
|
|
hadis_slug_map[hadis.id] = { |
|
|
|
correction_slug_map[correction.id] = { |
|
|
|
"base_slug": base_slug, |
|
|
|
"title_text": title_text, |
|
|
|
"old_slug": hadis.slug or "(empty)", |
|
|
|
"old_slug": correction.slug or "(empty)", |
|
|
|
} |
|
|
|
|
|
|
|
slug_map[base_slug].append(hadis.id) |
|
|
|
slug_map[base_slug].append(correction.id) |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
self.stdout.write( |
|
|
|
self.style.ERROR(f" ❌ Error analyzing Hadis {hadis.id}: {str(e)}") |
|
|
|
self.style.ERROR(f" ❌ Error analyzing HadisCorrection {correction.id}: {str(e)}") |
|
|
|
) |
|
|
|
|
|
|
|
self.stdout.write(f"✅ Analysis complete!\n") |
|
|
|
self.stdout.write(f"Step 2: Detecting duplicates...\n") |
|
|
|
self.stdout.write("✅ Analysis complete!\n") |
|
|
|
self.stdout.write("Step 2: Detecting duplicates...\n") |
|
|
|
|
|
|
|
# Identify groups with duplicates |
|
|
|
duplicate_groups = { |
|
|
|
@ -130,11 +134,11 @@ class Command(BaseCommand): |
|
|
|
) |
|
|
|
) |
|
|
|
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): |
|
|
|
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" - Hadis {hadis_id}: {base_slug}{counter}" |
|
|
|
f" - HadisCorrection {corr_id}: {base_slug}{counter}" |
|
|
|
) |
|
|
|
else: |
|
|
|
self.stdout.write("✅ No duplicates found! All slugs are unique.\n") |
|
|
|
@ -146,9 +150,9 @@ class Command(BaseCommand): |
|
|
|
unchanged = 0 |
|
|
|
errors = [] |
|
|
|
|
|
|
|
for hadis in qs: |
|
|
|
for correction in qs: |
|
|
|
try: |
|
|
|
slug_info = hadis_slug_map.get(hadis.id) |
|
|
|
slug_info = correction_slug_map.get(correction.id) |
|
|
|
if not slug_info: |
|
|
|
continue |
|
|
|
|
|
|
|
@ -157,11 +161,9 @@ class Command(BaseCommand): |
|
|
|
|
|
|
|
# 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) |
|
|
|
position = duplicate_ids.index(correction.id) |
|
|
|
|
|
|
|
# First one gets no counter, rest get -1, -2, etc. |
|
|
|
if position == 0: |
|
|
|
new_slug = base_slug |
|
|
|
else: |
|
|
|
@ -174,22 +176,21 @@ class Command(BaseCommand): |
|
|
|
else: |
|
|
|
new_slug = base_slug |
|
|
|
|
|
|
|
changed = hadis.slug != new_slug |
|
|
|
changed = correction.slug != new_slug |
|
|
|
|
|
|
|
if changed: |
|
|
|
self.stdout.write( |
|
|
|
f" [{hadis.id:5d}] {old_slug:45s} → {new_slug}" |
|
|
|
f" [{correction.id:5d}] {old_slug:45s} → {new_slug}" |
|
|
|
) |
|
|
|
|
|
|
|
if not dry_run: |
|
|
|
hadis.slug = new_slug |
|
|
|
hadis.save(update_fields=["slug"]) |
|
|
|
correction.slug = new_slug |
|
|
|
correction.save(update_fields=["slug"]) |
|
|
|
updated += 1 |
|
|
|
else: |
|
|
|
unchanged += 1 |
|
|
|
|
|
|
|
except Exception as e: |
|
|
|
error_msg = f"Hadis {hadis.id}: {str(e)}" |
|
|
|
error_msg = f"HadisCorrection {correction.id}: {str(e)}" |
|
|
|
self.stdout.write(self.style.ERROR(f" ❌ {error_msg}")) |
|
|
|
errors.append(error_msg) |
|
|
|
|
|
|
|
@ -225,7 +226,7 @@ class Command(BaseCommand): |
|
|
|
if not dry_run and updated > 0: |
|
|
|
self.stdout.write( |
|
|
|
self.style.SUCCESS( |
|
|
|
f"✅ Successfully regenerated {updated} slug(s)!" |
|
|
|
f"✅ Successfully regenerated {updated} HadisCorrection slug(s)!" |
|
|
|
) |
|
|
|
) |
|
|
|
elif dry_run: |
|
|
|
@ -238,12 +239,12 @@ class Command(BaseCommand): |
|
|
|
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. |
|
|
|
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-unknown" |
|
|
|
return "hadis-correction-unknown" |
|
|
|
|
|
|
|
# Extract first N words |
|
|
|
words = text.strip().split()[:keep_words] |
|
|
|
|