From c007db05cd4ddf8454c9e156722e081af03bf437 Mon Sep 17 00:00:00 2001 From: mohsentaba Date: Mon, 22 Dec 2025 12:12:56 +0330 Subject: [PATCH] HadisCorrection slug and link fixed. --- apps/hadis/management/commands/slug_hadis.py | 109 +++++++++--------- .../migrations/0060_hadiscorrection_slug.py | 19 +++ .../0061_alter_hadiscorrection_slug.py | 19 +++ apps/hadis/models/hadis.py | 35 +++++- 4 files changed, 127 insertions(+), 55 deletions(-) create mode 100644 apps/hadis/migrations/0060_hadiscorrection_slug.py create mode 100644 apps/hadis/migrations/0061_alter_hadiscorrection_slug.py diff --git a/apps/hadis/management/commands/slug_hadis.py b/apps/hadis/management/commands/slug_hadis.py index 898a9b0..c18a6e1 100644 --- a/apps/hadis/management/commands/slug_hadis.py +++ b/apps/hadis/management/commands/slug_hadis.py @@ -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] diff --git a/apps/hadis/migrations/0060_hadiscorrection_slug.py b/apps/hadis/migrations/0060_hadiscorrection_slug.py new file mode 100644 index 0000000..7ef44f5 --- /dev/null +++ b/apps/hadis/migrations/0060_hadiscorrection_slug.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.27 on 2025-12-22 11:56 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("hadis", "0059_alter_hadis_slug"), + ] + + operations = [ + migrations.AddField( + model_name="hadiscorrection", + name="slug", + field=models.SlugField( + blank=True, max_length=255, null=True, verbose_name="slug" + ), + ), + ] diff --git a/apps/hadis/migrations/0061_alter_hadiscorrection_slug.py b/apps/hadis/migrations/0061_alter_hadiscorrection_slug.py new file mode 100644 index 0000000..0402bd5 --- /dev/null +++ b/apps/hadis/migrations/0061_alter_hadiscorrection_slug.py @@ -0,0 +1,19 @@ +# Generated by Django 4.2.27 on 2025-12-22 12:06 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("hadis", "0060_hadiscorrection_slug"), + ] + + operations = [ + migrations.AlterField( + model_name="hadiscorrection", + name="slug", + field=models.SlugField( + blank=True, max_length=255, unique=True, verbose_name="slug" + ), + ), + ] diff --git a/apps/hadis/models/hadis.py b/apps/hadis/models/hadis.py index e5582c8..624627a 100644 --- a/apps/hadis/models/hadis.py +++ b/apps/hadis/models/hadis.py @@ -289,7 +289,7 @@ class Hadis(models.Model): def save(self, *args, **kwargs): # ساخت share_link قبل از ذخیره if not self.share_link: - self.share_link = f"{settings.SITE_DOMAIN}/hadis/{self.id}" + self.share_link = f"{settings.SITE_DOMAIN}/hadis/{self.slug}" super().save(*args, **kwargs) class Meta: @@ -372,6 +372,7 @@ class ReferenceImage(models.Model): class HadisCorrection(models.Model): hadis = models.ForeignKey(Hadis, verbose_name=_("hadis correction"), on_delete=models.CASCADE) title = models.JSONField(default = list , verbose_name=_('Title')) + slug = models.SlugField(max_length=255, verbose_name=_('slug'), blank=True,unique=True) description =models.JSONField(default = list , verbose_name=_('Description')) translation = models.JSONField(verbose_name=_("translation"), default=list) created_at = models.DateTimeField(auto_now_add=True, verbose_name=_("created at")) @@ -386,6 +387,38 @@ class HadisCorrection(models.Model): def __str__(self): return f"{self.hadis.number} - {self.title[0]['text']}" + def save(self, *args, **kwargs): + """ + Override save to automatically generate smart slugs. + """ + + # Generate slug if not already set + if not self.slug and self.title: + # Extract title text + title_text = None + if isinstance(self.title, list) and self.title: + first_item = self.title[0] + if isinstance(first_item, dict): + title_text = first_item.get("text") + + # Generate smart slug + if title_text: + self.slug = generate_smart_slug( + text=title_text, + model_class=HadisCorrection, + max_length=100, # ← Adjust max length here + keep_words=8, # ← Limit to 8 words (your requirement) + instance=self, + ) + else: + # Fallback if title is empty + self.slug = f"correction-{self.hadis.slug}-{self.id}" + + # Call parent save + if not self.share_link: + self.share_link = f"{settings.SITE_DOMAIN}/hadis/{self.hadis.slug}/corrections/{self.slug}" + super().save(*args, **kwargs) + def get_title(self,lang): """ Get title for a specific language