|
|
@ -1,33 +1,34 @@ |
|
|
""" |
|
|
""" |
|
|
Django Management Command: Regenerate HadisCorrection Slugs |
|
|
|
|
|
|
|
|
Django Management Command: Regenerate TransmitterOriginalText Slugs |
|
|
|
|
|
|
|
|
This command: |
|
|
This command: |
|
|
1. Takes all existing HadisCorrection objects |
|
|
|
|
|
2. Extracts their title (first 8 words max) |
|
|
|
|
|
|
|
|
1. Takes all existing TransmitterOriginalText objects |
|
|
|
|
|
2. Extracts their title (first N words, default 8) |
|
|
3. Generates smart, short, meaningful slugs |
|
|
3. Generates smart, short, meaningful slugs |
|
|
4. Handles uniqueness with counters (-1, -2, -3, etc.) |
|
|
4. Handles uniqueness with counters (-1, -2, -3, etc.) |
|
|
5. REPLACES all old slug values with new ones |
|
|
5. REPLACES all old slug values with new ones |
|
|
6. Detects and numbers duplicates automatically |
|
|
6. Detects and numbers duplicates automatically |
|
|
|
|
|
|
|
|
Usage: |
|
|
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 |
|
|
|
|
|
|
|
|
python manage.py regenerate_transmitter_originaltext_slugs |
|
|
|
|
|
python manage.py regenerate_transmitter_originaltext_slugs --max-length 75 |
|
|
|
|
|
python manage.py regenerate_transmitter_originaltext_slugs --keep-words 6 |
|
|
|
|
|
python manage.py regenerate_transmitter_originaltext_slugs --dry-run |
|
|
""" |
|
|
""" |
|
|
|
|
|
|
|
|
from collections import defaultdict |
|
|
from collections import defaultdict |
|
|
|
|
|
|
|
|
from django.core.management.base import BaseCommand |
|
|
from django.core.management.base import BaseCommand |
|
|
from django.db import transaction |
|
|
from django.db import transaction |
|
|
|
|
|
from django.utils.text import slugify |
|
|
|
|
|
|
|
|
from apps.hadis.models import HadisCorrection # ← adjust import path to your app |
|
|
|
|
|
from utils.slug import generate_smart_slug # ← your existing helper |
|
|
|
|
|
|
|
|
from apps.hadis.models import TransmitterOriginalText |
|
|
|
|
|
from utils.slug import generate_smart_slug # your existing helper |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class Command(BaseCommand): |
|
|
class Command(BaseCommand): |
|
|
help = ( |
|
|
help = ( |
|
|
"Regenerate smart slugs for all HadisCorrection objects. " |
|
|
|
|
|
|
|
|
"Regenerate smart slugs for all TransmitterOriginalText objects. " |
|
|
"Replaces existing slug values with optimized, short, meaningful ones. " |
|
|
"Replaces existing slug values with optimized, short, meaningful ones. " |
|
|
"Automatically adds counters for duplicates." |
|
|
"Automatically adds counters for duplicates." |
|
|
) |
|
|
) |
|
|
@ -59,64 +60,61 @@ class Command(BaseCommand): |
|
|
dry_run = options["dry_run"] |
|
|
dry_run = options["dry_run"] |
|
|
|
|
|
|
|
|
self.stdout.write(self.style.HTTP_INFO("=" * 80)) |
|
|
self.stdout.write(self.style.HTTP_INFO("=" * 80)) |
|
|
self.stdout.write("🔄 HADISCORRECTION SLUG REGENERATION WITH DUPLICATE HANDLING\n") |
|
|
|
|
|
|
|
|
self.stdout.write("🔄 TRANSMITTER ORIGINAL TEXT SLUG REGENERATION\n") |
|
|
self.stdout.write("Configuration:") |
|
|
self.stdout.write("Configuration:") |
|
|
self.stdout.write(f" • Max length: {max_length} chars") |
|
|
self.stdout.write(f" • Max length: {max_length} chars") |
|
|
self.stdout.write(f" • Keep words: {keep_words} words") |
|
|
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(f" • Dry run: {'Yes (no changes)' if dry_run else 'No (will save)'}") |
|
|
self.stdout.write(self.style.HTTP_INFO("=" * 80) + "\n") |
|
|
self.stdout.write(self.style.HTTP_INFO("=" * 80) + "\n") |
|
|
|
|
|
|
|
|
# Get all corrections |
|
|
|
|
|
qs = HadisCorrection.objects.all().order_by("id") |
|
|
|
|
|
|
|
|
# Get all objects |
|
|
|
|
|
qs = TransmitterOriginalText.objects.all().order_by("id") |
|
|
total = qs.count() |
|
|
total = qs.count() |
|
|
|
|
|
|
|
|
self.stdout.write(f"Step 1: Analyzing {total} HadisCorrection objects...\n") |
|
|
|
|
|
|
|
|
self.stdout.write(f"Step 1: Analyzing {total} TransmitterOriginalText objects...\n") |
|
|
|
|
|
|
|
|
# Dictionary to track duplicate slugs: {base_slug: [correction_ids]} |
|
|
|
|
|
|
|
|
# Dictionary to track duplicate slugs: {base_slug: [ids]} |
|
|
slug_map = defaultdict(list) |
|
|
slug_map = defaultdict(list) |
|
|
correction_slug_map = {} # {correction_id: data} |
|
|
|
|
|
|
|
|
obj_slug_map = {} # {id: data} |
|
|
|
|
|
|
|
|
# First pass: Generate base slugs and identify duplicates |
|
|
# First pass: Generate base slugs and identify duplicates |
|
|
for correction in qs: |
|
|
|
|
|
|
|
|
for obj in qs: |
|
|
try: |
|
|
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. |
|
|
|
|
|
|
|
|
# Extract title text from JSONField |
|
|
title_text = None |
|
|
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 obj.title and isinstance(obj.title, list) and obj.title: |
|
|
|
|
|
first_item = obj.title[0] |
|
|
if isinstance(first_item, dict): |
|
|
if isinstance(first_item, dict): |
|
|
title_text = first_item.get("text") |
|
|
title_text = first_item.get("text") |
|
|
|
|
|
|
|
|
# Fallback: use plain text field or id |
|
|
|
|
|
|
|
|
# Fallback if no title |
|
|
if not title_text: |
|
|
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}" |
|
|
|
|
|
|
|
|
# use transmitter name if available, otherwise id |
|
|
|
|
|
base = None |
|
|
|
|
|
if obj.transmitter and isinstance(obj.transmitter.full_name, list): |
|
|
|
|
|
first_name = obj.transmitter.full_name[0] |
|
|
|
|
|
if isinstance(first_name, dict): |
|
|
|
|
|
base = first_name.get("text") |
|
|
|
|
|
title_text = base or f"transmitter-originaltext-{obj.id}" |
|
|
|
|
|
|
|
|
# Generate base slug (without counter) |
|
|
# Generate base slug (without counter) |
|
|
base_slug = self._generate_base_slug( |
|
|
base_slug = self._generate_base_slug( |
|
|
title_text, |
|
|
title_text, |
|
|
max_length - 5, # reserve for counter |
|
|
|
|
|
|
|
|
max_length - 5, # Reserve space for counter |
|
|
keep_words, |
|
|
keep_words, |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
correction_slug_map[correction.id] = { |
|
|
|
|
|
|
|
|
obj_slug_map[obj.id] = { |
|
|
"base_slug": base_slug, |
|
|
"base_slug": base_slug, |
|
|
"title_text": title_text, |
|
|
"title_text": title_text, |
|
|
"old_slug": correction.slug or "(empty)", |
|
|
|
|
|
|
|
|
"old_slug": obj.slug or "(empty)", |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
slug_map[base_slug].append(correction.id) |
|
|
|
|
|
|
|
|
slug_map[base_slug].append(obj.id) |
|
|
|
|
|
|
|
|
except Exception as e: |
|
|
except Exception as e: |
|
|
self.stdout.write( |
|
|
self.stdout.write( |
|
|
self.style.ERROR(f" ❌ Error analyzing HadisCorrection {correction.id}: {str(e)}") |
|
|
|
|
|
|
|
|
self.style.ERROR(f" ❌ Error analyzing TransmitterOriginalText {obj.id}: {str(e)}") |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
self.stdout.write("✅ Analysis complete!\n") |
|
|
self.stdout.write("✅ Analysis complete!\n") |
|
|
@ -134,11 +132,11 @@ class Command(BaseCommand): |
|
|
) |
|
|
) |
|
|
) |
|
|
) |
|
|
for base_slug, ids in sorted(duplicate_groups.items()): |
|
|
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): |
|
|
|
|
|
|
|
|
self.stdout.write(f" • '{base_slug}' → {len(ids)} objects") |
|
|
|
|
|
for idx, obj_id in enumerate(ids): |
|
|
counter = "" if idx == 0 else f"-{idx}" |
|
|
counter = "" if idx == 0 else f"-{idx}" |
|
|
self.stdout.write( |
|
|
self.stdout.write( |
|
|
f" - HadisCorrection {corr_id}: {base_slug}{counter}" |
|
|
|
|
|
|
|
|
f" - TransmitterOriginalText {obj_id}: {base_slug}{counter}" |
|
|
) |
|
|
) |
|
|
else: |
|
|
else: |
|
|
self.stdout.write("✅ No duplicates found! All slugs are unique.\n") |
|
|
self.stdout.write("✅ No duplicates found! All slugs are unique.\n") |
|
|
@ -150,9 +148,9 @@ class Command(BaseCommand): |
|
|
unchanged = 0 |
|
|
unchanged = 0 |
|
|
errors = [] |
|
|
errors = [] |
|
|
|
|
|
|
|
|
for correction in qs: |
|
|
|
|
|
|
|
|
for obj in qs: |
|
|
try: |
|
|
try: |
|
|
slug_info = correction_slug_map.get(correction.id) |
|
|
|
|
|
|
|
|
slug_info = obj_slug_map.get(obj.id) |
|
|
if not slug_info: |
|
|
if not slug_info: |
|
|
continue |
|
|
continue |
|
|
|
|
|
|
|
|
@ -162,8 +160,9 @@ class Command(BaseCommand): |
|
|
# If this slug has duplicates, add counter |
|
|
# If this slug has duplicates, add counter |
|
|
if len(slug_map[base_slug]) > 1: |
|
|
if len(slug_map[base_slug]) > 1: |
|
|
duplicate_ids = slug_map[base_slug] |
|
|
duplicate_ids = slug_map[base_slug] |
|
|
position = duplicate_ids.index(correction.id) |
|
|
|
|
|
|
|
|
position = duplicate_ids.index(obj.id) |
|
|
|
|
|
|
|
|
|
|
|
# First one gets no counter, rest get -1, -2, etc. |
|
|
if position == 0: |
|
|
if position == 0: |
|
|
new_slug = base_slug |
|
|
new_slug = base_slug |
|
|
else: |
|
|
else: |
|
|
@ -176,21 +175,22 @@ class Command(BaseCommand): |
|
|
else: |
|
|
else: |
|
|
new_slug = base_slug |
|
|
new_slug = base_slug |
|
|
|
|
|
|
|
|
changed = correction.slug != new_slug |
|
|
|
|
|
|
|
|
changed = obj.slug != new_slug |
|
|
|
|
|
|
|
|
if changed: |
|
|
if changed: |
|
|
self.stdout.write( |
|
|
self.stdout.write( |
|
|
f" [{correction.id:5d}] {old_slug:45s} → {new_slug}" |
|
|
|
|
|
|
|
|
f" [{obj.id:5d}] {old_slug:45s} → {new_slug}" |
|
|
) |
|
|
) |
|
|
|
|
|
|
|
|
if not dry_run: |
|
|
if not dry_run: |
|
|
correction.slug = new_slug |
|
|
|
|
|
correction.save(update_fields=["slug"]) |
|
|
|
|
|
|
|
|
obj.slug = new_slug |
|
|
|
|
|
obj.save(update_fields=["slug"]) |
|
|
updated += 1 |
|
|
updated += 1 |
|
|
else: |
|
|
else: |
|
|
unchanged += 1 |
|
|
unchanged += 1 |
|
|
|
|
|
|
|
|
except Exception as e: |
|
|
except Exception as e: |
|
|
error_msg = f"HadisCorrection {correction.id}: {str(e)}" |
|
|
|
|
|
|
|
|
error_msg = f"TransmitterOriginalText {obj.id}: {str(e)}" |
|
|
self.stdout.write(self.style.ERROR(f" ❌ {error_msg}")) |
|
|
self.stdout.write(self.style.ERROR(f" ❌ {error_msg}")) |
|
|
errors.append(error_msg) |
|
|
errors.append(error_msg) |
|
|
|
|
|
|
|
|
@ -226,7 +226,7 @@ class Command(BaseCommand): |
|
|
if not dry_run and updated > 0: |
|
|
if not dry_run and updated > 0: |
|
|
self.stdout.write( |
|
|
self.stdout.write( |
|
|
self.style.SUCCESS( |
|
|
self.style.SUCCESS( |
|
|
f"✅ Successfully regenerated {updated} HadisCorrection slug(s)!" |
|
|
|
|
|
|
|
|
f"✅ Successfully regenerated {updated} slug(s) for TransmitterOriginalText!" |
|
|
) |
|
|
) |
|
|
) |
|
|
) |
|
|
elif dry_run: |
|
|
elif dry_run: |
|
|
@ -239,12 +239,10 @@ class Command(BaseCommand): |
|
|
def _generate_base_slug(self, text: str, max_length: int, keep_words: int) -> str: |
|
|
def _generate_base_slug(self, text: str, max_length: int, keep_words: int) -> str: |
|
|
""" |
|
|
""" |
|
|
Generate a base slug without counter. |
|
|
Generate a base slug without counter. |
|
|
Returns the base slug that might be used for multiple corrections. |
|
|
|
|
|
|
|
|
Returns the base slug that might be used for multiple objects. |
|
|
""" |
|
|
""" |
|
|
from django.utils.text import slugify |
|
|
|
|
|
|
|
|
|
|
|
if not text or not isinstance(text, str): |
|
|
if not text or not isinstance(text, str): |
|
|
return "hadis-correction-unknown" |
|
|
|
|
|
|
|
|
return "transmitter-originaltext-unknown" |
|
|
|
|
|
|
|
|
# Extract first N words |
|
|
# Extract first N words |
|
|
words = text.strip().split()[:keep_words] |
|
|
words = text.strip().split()[:keep_words] |
|
|
|