diff --git a/apps/hadis/management/commands/seed_category_data.py b/apps/hadis/management/commands/seed_category_data.py index 58bb0aa..30147ac 100644 --- a/apps/hadis/management/commands/seed_category_data.py +++ b/apps/hadis/management/commands/seed_category_data.py @@ -1,154 +1,95 @@ import random from django.core.management.base import BaseCommand -from django.db import transaction -# Adjust the import path if your app name is different -from apps.hadis.models import Hadis, HadisCategory, HadisSect +from apps.hadis.models import Hadis, HadisCategory class Command(BaseCommand): - help = 'Seeds HadisCategories with rich Islamic data and links existing Hadiths' + help = 'Part 2: Assigns Hadiths to Leaves and cleans empty categories' def handle(self, *args, **options): - self.stdout.write(self.style.WARNING('--- Starting Islamic Category Seeding ---')) + self.stdout.write(self.style.WARNING('--- Part 2: Starting Hadith Assignment ---')) - # 1. Fetch or Create Sects - shia_sect, _ = HadisSect.objects.get_or_create( - sect_type=HadisSect.SectType.SHIA, - defaults={'title': [{'text': 'Shia (Twelver)', 'language_code': 'en'}], 'is_active': True} - ) - sunni_sect, _ = HadisSect.objects.get_or_create( - sect_type=HadisSect.SectType.SUNNI, - defaults={'title': [{'text': 'Sunni (Ahlus Sunnah)', 'language_code': 'en'}], 'is_active': True} - ) + # 1. FETCH LEAF CATEGORIES + # A leaf is a category that has NO children + # We assume Script 1 has run, so we just query the DB. + # Note: Related name for children defaults to 'children' usually, check your model if different. + # If your model doesn't have a 'children' reverse relation explicitly, + # Django usually provides 'hadiscategory_set' or similar. + # Standard approach for adjacency list parent: + + leaf_categories = [] + all_cats = HadisCategory.objects.all() + + self.stdout.write("identifying leaf categories...") + for cat in all_cats: + if not HadisCategory.objects.filter(parent=cat).exists(): + leaf_categories.append(cat) + + if not leaf_categories: + self.stdout.write(self.style.ERROR("No leaf categories found! Did you run Part 1?")) + return - # 2. Rich Islamic Data Dictionary - ISLAMIC_DATA = { - HadisCategory.SourceType.QURAN: { - HadisSect.SectType.SHIA: [ - ("Quranic Sciences", "Thematic Exegesis", ["Verses of Wilayah", "Ahlulbayt in Quran", "Moral Teachings in Surah Yusuf"]), - ("Tafsir Studies", "Interpretation Principles", ["Tafsir al-Mizan Topics", "Esoteric Meanings (Ta'wil)"]) - ], - HadisSect.SectType.SUNNI: [ - ("Quranic Studies", "Tafsir Methodology", ["Asbab al-Nuzul", "Stories of the Prophets", "Legislative Verses (Ahkam)"]), - ("Recitation (Tajweed)", "Qira'at", ["Hafs an Asim", "Warsh an Nafi", "Rules of Nun Sakinah"]) - ] - }, - HadisCategory.SourceType.HADITH: { - HadisSect.SectType.SHIA: [ - ("The Four Books", "Usul al-Kafi", ["Book of Intellect", "Book of Divine Proof", "Book of Belief"]), - ("Nahj al-Balagha", "Sermons of Imam Ali", ["The Shiqshiqiyyah Sermon", "Letter to Malik al-Ashtar", "Aphorisms of Wisdom"]) - ], - HadisSect.SectType.SUNNI: [ - ("The Six Books", "Sahih al-Bukhari", ["Book of Revelation", "Book of Belief", "Book of Knowledge"]), - ("Sunan Collections", "Sunan Abu Dawood", ["Book of Prayer", "Book of Zakat", "Book of Jihad"]) - ] - }, - HadisCategory.SourceType.HISTORY: { - HadisSect.SectType.SHIA: [ - ("Life of the Infallibles", "The Tragedy of Karbala", ["Day of Ashura", "The Sermon of Zaynab (SA)", "Journey of Captives"]), - ("Occultation (Ghaybah)", "Major Occultation", ["Signs of Reappearance", "Duties of Believers", "Deputies of the Imam"]) - ], - HadisSect.SectType.SUNNI: [ - ("Seerah of the Prophet", "Meccan Period", ["The First Revelation", "Migration to Abyssinia", "The Year of Sorrow"]), - ("The Rashidun Caliphate", "Conquests and Expansion", ["Caliphate of Abu Bakr", "Caliphate of Umar", "Battles of Yarmouk"]) - ] - }, - HadisCategory.SourceType.FATWA: { - HadisSect.SectType.SHIA: [ - ("Jurisprudence (Fiqh)", "Acts of Worship", ["Rules of Taqlid", "Khums and Zakat", "Rules of Salatul Layl"]), - ("Modern Legal Issues", "Medical Jurisprudence", ["Organ Donation", "Assisted Reproduction", "Gender Reassignment"]) - ], - HadisSect.SectType.SUNNI: [ - ("Fiqh Schools", "Shafi'i School", ["Rules of Prayer", "Inheritance Laws", "Marriage Contracts"]), - ("Contemporary Fatawa", "Islamic Finance", ["Prohibition of Riba", "Islamic Banking Principles", "Crypto-currency Rulings"]) - ] - }, - HadisCategory.SourceType.QUOTE: { - HadisSect.SectType.SHIA: [ - ("Wisdom of the Imams", "Supplications (Du'a)", ["Du'a Kumayl Themes", "Whispered Prayers (Munajat)", "Du'a Arafah"]), - ("Mysticism (Irfan)", "Spiritual Wayfaring", ["Combat with the Self", "Degrees of Piety", "Love for the Divine"]) - ], - HadisSect.SectType.SUNNI: [ - ("Sayings of the Companions", "Wisdom of Abu Bakr & Umar", ["Justice in Governance", "Fear of Allah", "Humility"]), - ("Sufi Wisdom", "Spiritual Purification", ["Tazkiyat al-Nafs", "Remembrance of Death", "Reliance on Allah (Tawakkul)"]) - ] - } - } + self.stdout.write(f"Found {len(leaf_categories)} leaf categories.") - created_leaves = [] + # 2. FETCH HADITHS + all_hadiths = list(Hadis.objects.all()) + total_hadiths = len(all_hadiths) + + if total_hadiths < 3: + self.stdout.write(self.style.ERROR('Not enough Hadiths in DB (need at least 3).')) + return - with transaction.atomic(): - self.stdout.write('Building Rich Islamic Category Tree...') - - for source_code, sect_data in ISLAMIC_DATA.items(): - for sect_type_enum, categories_list in sect_data.items(): - - current_sect = shia_sect if sect_type_enum == HadisSect.SectType.SHIA else sunni_sect - - for root_title, sub_title, leaf_titles in categories_list: - - # --- Level 1: Root --- - # ADDED: description to defaults - root_cat, _ = HadisCategory.objects.get_or_create( - sect=current_sect, - source_type=source_code, - title=[{'text': root_title, 'language_code': 'en'}], - defaults={ - 'parent': None, - 'description': [{'text': f'Root category for {root_title}', 'language_code': 'en'}] - } - ) - - # --- Level 2: Sub --- - # ADDED: description to defaults - sub_cat, _ = HadisCategory.objects.get_or_create( - sect=current_sect, - source_type=source_code, - title=[{'text': sub_title, 'language_code': 'en'}], - defaults={ - 'parent': root_cat, - 'description': [{'text': f'Sub-category for {sub_title}', 'language_code': 'en'}] - } - ) - - # --- Level 3: Leaves --- - for leaf_title in leaf_titles: - # ADDED: description to defaults - leaf_cat, _ = HadisCategory.objects.get_or_create( - sect=current_sect, - source_type=source_code, - title=[{'text': leaf_title, 'language_code': 'en'}], - defaults={ - 'parent': sub_cat, - 'description': [{'text': f'Leaf category for {leaf_title}', 'language_code': 'en'}] - } - ) - created_leaves.append(leaf_cat) - self.stdout.write(f"Created Leaf: {leaf_title} ({current_sect.get_title('en')})") - - self.stdout.write(self.style.SUCCESS(f'Successfully structured {len(created_leaves)} leaf categories.')) + # 3. DISTRIBUTE + self.stdout.write(f'Distributing {total_hadiths} Hadiths among leaves (Target: 3 per leaf)...') + + random.shuffle(all_hadiths) + hadith_index = 0 + assigned_count = 0 + + # Loop through leaves and assign + for leaf in leaf_categories: + # Cycle logic: restart index if we run out of unique hadiths + if hadith_index + 3 > total_hadiths: + hadith_index = 0 + + batch = all_hadiths[hadith_index : hadith_index + 3] + hadith_index += 3 - # 3. Connect Hadiths to Leaves - all_hadiths = list(Hadis.objects.all()) - total_hadiths = len(all_hadiths) + for hadis_obj in batch: + hadis_obj.category = leaf + hadis_obj.save() + assigned_count += 1 - if total_hadiths < 3: - self.stdout.write(self.style.ERROR('Not enough Hadiths in DB. Please seed Hadiths first (need at least 3).')) - return + # Print a dot every 5 leaves to show progress without spamming + if leaf_categories.index(leaf) % 5 == 0: + self.stdout.write(".", ending="") - self.stdout.write(f'Distributing {total_hadiths} Hadiths among leaves...') + self.stdout.write(f"\nAssigned {assigned_count} hadith links.") + + # 4. CLEANUP EMPTY CATEGORIES + # Constraint: "every category is either have hadiths , or childs" + self.stdout.write("Running cleanup for empty categories...") + + empty_deleted_count = 0 + + # Re-fetch all categories to ensure fresh state + all_cats_check = HadisCategory.objects.all() + + for cat in all_cats_check: + # Check for children + has_children = HadisCategory.objects.filter(parent=cat).exists() - random.shuffle(all_hadiths) - hadith_index = 0 + # Check for hadiths + # Ensure 'category' matches your field name in Hadis model + has_hadiths = Hadis.objects.filter(category=cat).exists() - for leaf in created_leaves: - if hadith_index + 3 > total_hadiths: - hadith_index = 0 + if not has_children and not has_hadiths: + # self.stdout.write(f"Deleting empty: {cat.id}") # Optional debug + cat.delete() + empty_deleted_count += 1 - batch = all_hadiths[hadith_index : hadith_index + 3] - hadith_index += 3 - - for hadis_obj in batch: - hadis_obj.category = leaf - hadis_obj.save() + if empty_deleted_count > 0: + self.stdout.write(self.style.WARNING(f"Cleaned up {empty_deleted_count} empty categories.")) + else: + self.stdout.write(self.style.SUCCESS("No empty categories found.")) - self.stdout.write(self.style.SUCCESS('--- Islamic Category Seeding Complete ---')) \ No newline at end of file + self.stdout.write(self.style.SUCCESS('--- Part 2 Complete ---')) \ No newline at end of file diff --git a/apps/hadis/urls.py b/apps/hadis/urls.py index 283b062..a1bfeb3 100644 --- a/apps/hadis/urls.py +++ b/apps/hadis/urls.py @@ -25,11 +25,11 @@ urlpatterns = [ path('info/', cached_view(HadisInfoView.as_view()), name='hadis-info'), # Category paths (more specific first) - path('categories/tree/', cached_view(HadisCategoryTreeNormalView.as_view()), name='hadis-category-tree-normal'), - path('categories////', cached_view(HadisCategorySelectBySectSourceView.as_view()), name='categories-tree-by-sect-source'), - path('categories///', cached_view(HadisCategorySelectBySectView.as_view()), name='categories-tree-by-sect'), - path('categories//', cached_view(CategoriesBySectView.as_view()), name='categories-by-sect'), - path('categories/', cached_view(CategoriesView.as_view()), name='categories'), # ← Least specific LAST + path('categories/tree/', HadisCategoryTreeNormalView.as_view(), name='hadis-category-tree-normal'), + path('categories////', HadisCategorySelectBySectSourceView.as_view(), name='categories-tree-by-sect-source'), + path('categories///', HadisCategorySelectBySectView.as_view(), name='categories-tree-by-sect'), + path('categories//', CategoriesBySectView.as_view(), name='categories-by-sect'), + path('categories/', CategoriesView.as_view(), name='categories'), # ← Least specific LAST # Hadis paths path('category//', HadisListView.as_view(), name='hadis-list'),