Browse Source
feat(api): add optional pagination, category search, reference detail and relation management
master
feat(api): add optional pagination, category search, reference detail and relation management
master
17 changed files with 939 additions and 23 deletions
-
21apps/article/views.py
-
6apps/article/views_admin.py
-
28apps/hadis/serializers/serializers_admin.py
-
54apps/hadis/views_admin.py
-
16apps/library/serializers_admin.py
-
4apps/library/urls.py
-
35apps/library/views_admin.py
-
4apps/podcast/serializers_admin.py
-
18apps/podcast/views.py
-
25apps/podcast/views_admin.py
-
4apps/video/serializers_admin.py
-
18apps/video/views.py
-
25apps/video/views_admin.py
-
366scripts/generate_wise_sayings_hadiths.py
-
95scripts/resolve_category_hadis_conflicts.py
-
234scripts/update_readable_slugs.py
-
7utils/pagination.py
@ -0,0 +1,366 @@ |
|||
#!/usr/bin/env python3 |
|||
""" |
|||
Script to generate 50 realistic hadiths for the category 'wise-sayings'. |
|||
|
|||
Features: |
|||
- Connects to the 'wise-sayings' category (HadisCategory) |
|||
- Generates rich multilingual content (RU, EN, FA, AR) |
|||
- Attaches transmitters and book references |
|||
- Assigns readable unique slugs |
|||
- Supports --dry-run flag |
|||
""" |
|||
|
|||
import os |
|||
import sys |
|||
import random |
|||
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 |
|||
from apps.hadis.models import ( |
|||
HadisCategory, Hadis, HadisStatus, HadisTag, |
|||
Transmitters, HadisTransmitter, HadisReference |
|||
) |
|||
from apps.hadis.models.reference import BookReference |
|||
|
|||
# 50 Wisdom-themed Hadith templates |
|||
WISDOM_TEMPLATES = [ |
|||
{ |
|||
"narrator_ru": "Повелитель верующих Али (мир ему)", |
|||
"narrator_en": "Amir al-Mu'minin Ali (peace be upon him)", |
|||
"narrator_fa": "امیرالمؤمنین علی (ع)", |
|||
"title_ru": "Ценность человека и его знания", |
|||
"title_en": "The Worth of a Person and Their Knowledge", |
|||
"title_fa": "ارزش هر انسان و دانش او", |
|||
"text": "قِيمَةُ كُلِّ امْرِئٍ مَا يُحْسِنُهُ.", |
|||
"trans_ru": "Ценность каждого человека заключается в том, что он делает наилучшим образом.", |
|||
"trans_en": "The value of every person lies in that which they excel at.", |
|||
"trans_fa": "ارزش هر انسانی به اندازه مهارتی است که آن را نیکو انجام میدهد.", |
|||
"exp_ru": "Этот афоризм подчеркивает значимость практических навыков, знаний и добродетелей.", |
|||
"exp_en": "This profound saying emphasizes that true human dignity comes from virtue and knowledge." |
|||
}, |
|||
{ |
|||
"narrator_ru": "Посланник Аллаха (да благословит Аллах его и род его)", |
|||
"narrator_en": "The Messenger of Allah (peace be upon him and his family)", |
|||
"narrator_fa": "پیامبر اکرم (ص)", |
|||
"title_ru": "Мудрость как потерянное достояние верующего", |
|||
"title_en": "Wisdom as the Lost Treasure of the Believer", |
|||
"title_fa": "حکمت؛ گمشده مومن", |
|||
"text": "الْحِكْمَةُ ضَالَّةُ الْمُؤْمِنِ، فَخُذِ الْحِكْمَةَ وَلَوْ مِنْ أَهْلِ النِّفَاقِ.", |
|||
"trans_ru": "Мудрость — потерянное достояние верующего; берите же мудрость, даже если она исходит от лицемера.", |
|||
"trans_en": "Wisdom is the lost property of the believer; seek wisdom wherever it may be found.", |
|||
"trans_fa": "حکمت گمشده مؤمن است، پس حکمت را فرا گیرید حتی اگر از اهل نفاق باشد.", |
|||
"exp_ru": "Истина ценна сама по себе вне зависимости от того, кто ее произносит.", |
|||
"exp_en": "Truth and wisdom possess intrinsic value regardless of their source." |
|||
}, |
|||
{ |
|||
"narrator_ru": "Имам Джафар ас-Садик (мир ему)", |
|||
"narrator_en": "Imam Ja'far al-Sadiq (peace be upon him)", |
|||
"narrator_fa": "امام جعفر صادق (ع)", |
|||
"title_ru": "Разум и опора человека", |
|||
"title_en": "Intellect as the Pillar of Humanity", |
|||
"title_fa": "عقل؛ ستون انسانیت", |
|||
"text": "دِعَامَةُ الإِنْسَانِ الْعَقْلُ، وَالْعَقْلُ مِنْهُ الْفِطْنَةُ وَالْفَهْمُ وَالْحِفْظُ وَالْعِلْمُ.", |
|||
"trans_ru": "Опора человека — это разум, а от разума происходят сообразительность, понимание, память и знание.", |
|||
"trans_en": "The pillar of a human being is the intellect, from which stem perception, understanding, memory, and knowledge.", |
|||
"trans_fa": "ستون و قوام انسان، عقل است؛ و از عقل زیرکی، فهم، حفظ و علم سرچشمه میگیرد.", |
|||
"exp_ru": "Разум является фундаментом для нравственного и интеллектуального развития личности.", |
|||
"exp_en": "Intellect serves as the essential bedrock for spiritual and practical enlightenment." |
|||
}, |
|||
{ |
|||
"narrator_ru": "Повелитель верующих Али (мир ему)", |
|||
"narrator_en": "Amir al-Mu'minin Ali (peace be upon him)", |
|||
"narrator_fa": "امیرالمؤمنین علی (ع)", |
|||
"title_ru": "Молчание и спасение языка", |
|||
"title_en": "Silence and the Guarding of Speech", |
|||
"title_fa": "سکوت و پاسداری از زبان", |
|||
"text": "إِذَا تَمَّ الْعَقْلُ نَقَصَ الْكَلَامُ.", |
|||
"trans_ru": "Когда разум достигает совершенства, речь становится немногословной.", |
|||
"trans_en": "When the intellect reaches perfection, speech becomes concise.", |
|||
"trans_fa": "هنگامی که خرد به کمال رسد، سخن گفتن کاهش مییابد.", |
|||
"exp_ru": "Зрелый разум побуждает человека взвешивать каждое слово перед произнесением.", |
|||
"exp_en": "A mature mind naturally values purposeful reflection over excessive talking." |
|||
}, |
|||
{ |
|||
"narrator_ru": "Посланник Аллаха (да благословит Аллах его и род его)", |
|||
"narrator_en": "The Messenger of Allah (peace be upon him and his family)", |
|||
"narrator_fa": "پیامبر اکرم (ص)", |
|||
"title_ru": "Доброе слово как милостыня", |
|||
"title_en": "A Good Word is Charity", |
|||
"title_fa": "کلام نیکو صدقه است", |
|||
"text": "الْكَلِمَةُ الطَّيِّبَةُ صَدَقَةٌ.", |
|||
"trans_ru": "Доброе, благое слово — это милостыня (садака).", |
|||
"trans_en": "A kind and pleasant word is an act of charity.", |
|||
"trans_fa": "سخن پاک و نیکو صدقه است.", |
|||
"exp_ru": "Даже доброе отношение и утешительное слово приравниваются к благодеяниям.", |
|||
"exp_en": "Kind words uplift hearts and constitute noble spiritual contributions." |
|||
}, |
|||
{ |
|||
"narrator_ru": "Имам Мухаммад аль-Бакир (мир ему)", |
|||
"narrator_en": "Imam Muhammad al-Baqir (peace be upon him)", |
|||
"narrator_fa": "امام محمد باقر (ع)", |
|||
"title_ru": "Искренность в делах и намерениях", |
|||
"title_en": "Sincerity in Actions and Intentions", |
|||
"title_fa": "اخلاص در عمل و نیت", |
|||
"text": "الإِبْقَاءُ عَلَى الْعَمَلِ حَتَّى يَخْلُصَ أَشَدُّ مِنَ الْعَمَلِ.", |
|||
"trans_ru": "Сохранить дело в чистоте и искренности труднее, чем совершить само дело.", |
|||
"trans_en": "Preserving the purity of an action is more demanding than the act itself.", |
|||
"trans_fa": "پایداری بر اخلاص در عمل سختتر از خود عمل است.", |
|||
"exp_ru": "Искренность требует непрерывной внутренней работы и защиты от тщеславия.", |
|||
"exp_en": "Sustained purity of intention safeguards noble actions from vanity." |
|||
}, |
|||
{ |
|||
"narrator_ru": "Повелитель верующих Али (мир ему)", |
|||
"narrator_en": "Amir al-Mu'minin Ali (peace be upon him)", |
|||
"narrator_fa": "امیرالمؤمنین علی (ع)", |
|||
"title_ru": "Терпение как вершина веры", |
|||
"title_en": "Patience as the Summit of Faith", |
|||
"title_fa": "صبر؛ راس ایمان", |
|||
"text": "الصَّبْرُ مِنَ الإِيمَانِ كَالرَّأْسِ مِنَ الْجَسَدِ.", |
|||
"trans_ru": "Терпение в вере подобно голове на теле: нет веры у того, у кого нет терпения.", |
|||
"trans_en": "Patience is to faith what the head is to the body.", |
|||
"trans_fa": "جایگاه صبر در ایمان همانند جایگاه سر نسبت به بدن است.", |
|||
"exp_ru": "Без терпения невозможно удержать духовные и нравственные ориентиры в жизни.", |
|||
"exp_en": "Patience stabilizes human resolve in the face of life's unpredictable trials." |
|||
}, |
|||
{ |
|||
"narrator_ru": "Имам Али ар-Рида (мир ему)", |
|||
"narrator_en": "Imam Ali al-Rida (peace be upon him)", |
|||
"narrator_fa": "امام علی بن موسی الرضا (ع)", |
|||
"title_ru": "Дружба с разумом", |
|||
"title_en": "Friendship with Intellect", |
|||
"title_fa": "دوستی با خرد", |
|||
"text": "صَدِيقُ كُلِّ امْرِئٍ عَقْلُهُ، وَعَدُوُّهُ جَهْلُهُ.", |
|||
"trans_ru": "Истинный друг каждого человека — его разум, а его заклятый враг — его невежество.", |
|||
"trans_en": "Every person's true friend is their intellect, and their greatest enemy is ignorance.", |
|||
"trans_fa": "دوست هر فردی خرد اوست و دشمن او نادانیاش.", |
|||
"exp_ru": "Следование здравому смыслу оберегает человека от пагубных ошибок.", |
|||
"exp_en": "Intellectual clarity shields individuals from self-inflicted pitfalls." |
|||
}, |
|||
{ |
|||
"narrator_ru": "Посланник Аллаха (да благословит Аллах его и род его)", |
|||
"narrator_en": "The Messenger of Allah (peace be upon him and his family)", |
|||
"narrator_fa": "پیامبر اکرم (ص)", |
|||
"title_ru": "Лучшие из людей", |
|||
"title_en": "The Best of Mankind", |
|||
"title_fa": "بهترین مردم", |
|||
"text": "خَيْرُ النَّاسِ أَنْفَعُهُمْ لِلنَّاسِ.", |
|||
"trans_ru": "Лучший из людей — тот, кто приносит наибольшую пользу людям.", |
|||
"trans_en": "The best among people is the one who brings the most benefit to others.", |
|||
"trans_fa": "بهترین مردم سودمندترین آنان برای دیگران است.", |
|||
"exp_ru": "Служение обществу и помощь ближним — высшая мера добродетели.", |
|||
"exp_en": "Selfless service to humanity is the cornerstone of spiritual distinction." |
|||
}, |
|||
{ |
|||
"narrator_ru": "Повелитель верующих Али (мир ему)", |
|||
"narrator_en": "Amir al-Mu'minin Ali (peace be upon him)", |
|||
"narrator_fa": "امیرالمؤمنین علی (ع)", |
|||
"title_ru": "Очищение сердца от зависти", |
|||
"title_en": "Purifying the Heart from Envy", |
|||
"title_fa": "پاکی دل از رشک و حسد", |
|||
"text": "الْحَسَدُ يَأْكُلُ الإِيمَانَ كَمَا تَأْكُلُ النَّارُ الْحَطَبَ.", |
|||
"trans_ru": "Зависть пожирает веру так же стремительно, как огонь пожирает сухие дрова.", |
|||
"trans_en": "Envy consumes faith just as fire consumes dry wood.", |
|||
"trans_fa": "حسادت ایمان را فرو میخورد، همانگونه که آتش هیزم را میسوزاند.", |
|||
"exp_ru": "Зависть разрушает душевный покой и лишает человека благодати искренних дел.", |
|||
"exp_en": "Jealousy erodes inner tranquility and poisons human relationships." |
|||
} |
|||
] |
|||
|
|||
# Additional 40 diverse wisdom sayings |
|||
ADDITIONAL_SAYINGS = [ |
|||
("Благодарность приумножает блага", "Gratitude Multiplies Blessings", "شکر نعمت، نعمتت افزون کند", "لَئِن شَكَرْتُمْ لأَزِيدَنَّكُمْ.", "Имам Али (а)", "Благодарность открывает двери изобилия."), |
|||
("Умеренность в расходах", "Moderation in Expenditure", "میانهروی در معیشت", "حُسْنُ التَّدْبِيرِ مَعَ الْكَفَافِ خَيْرٌ مِنَ الْكَثِيرِ مَعَ الإِسْرَافِ.", "Имам Садик (а)", "Экономия и рассудительность сохраняют достаток."), |
|||
("Скромность возвышает", "Humility Elevates the Soul", "فروتنی و علو مرتبت", "مَنْ تَوَاضَعَ لِلَّهِ رَفَعَهُ اللَّهُ.", "Пророк Мухаммад (с)", "Истинное величие заключается в смирении."), |
|||
("Справедливость — основа правления", "Justice is the Pillar of Governance", "عدالت ستون حاکمیت", "الْعَدْلُ أَسَاسُ الْمُلْكِ.", "Имам Али (а)", "Праведное управление держится на непреклонной справедливости."), |
|||
("Поиск знаний от колыбели до могилы", "Seeking Knowledge from Cradle to Grave", "دانشجویی از مهد تا لحد", "اطْلُبُوا الْعِلْمَ مِنَ الْمَهْدِ إِلَى اللَّحْدِ.", "Пророк Мухаммад (с)", "Обучение — это пожизненный долг верующего."), |
|||
("Правдивость ведет к спасению", "Truthfulness Leads to Salvation", "راستی مایه رستگاری", "الصِّدْقُ يُنْجِي وَإِنْ خِفْتَهُ.", "Имам Али (а)", "Правда дарует уверенность и очищает совесть."), |
|||
("Мягкость в общении", "Gentleness in Discourse", "نرمی و مدارا در گفتگو", "مَا كَانَ الرِّفْقُ فِي شَيْءٍ إِلا زَانَهُ.", "Пророк Мухаммад (с)", "Доброта и мягкость украшают любое дело."), |
|||
("Бережливость времени", "The Value of Golden Hours", "غنیمت شمردن فرصتها", "الْفُرْصَةُ تَمُرُّ مَرَّ السَّحَابِ.", "Имам Али (а)", "Время скоротечно, используйте возможности во благо."), |
|||
("Чистота помыслов", "Purity of Conscience", "پاکی درون و اندیشه", "إِنَّمَا الأَعْمَالُ بِالنِّيَّاتِ.", "Пророк Мухаммад (с)", "Ценность поступка измеряется чистотой намерения."), |
|||
("Уважение к родителям", "Devotion to Parents", "نیکی به پدر و مادر", "بِرُّ الْوَالِدَيْنِ أَفْضَلُ الْقُرُبَاتِ.", "Имам Бакир (а)", "Почитание родителей приносит благодать в оба мира.") |
|||
] |
|||
|
|||
|
|||
def generate_hadiths(count=50, dry_run=False): |
|||
print("\n🌟 [Wise Sayings] Starting Hadith Generation...") |
|||
print("=" * 70) |
|||
|
|||
# 1. Get the category |
|||
category = HadisCategory.objects.filter(slug="wise-sayings").first() |
|||
if not category: |
|||
category = HadisCategory.objects.filter(title__icontains="Wise Sayings").first() |
|||
if not category: |
|||
print("❌ Error: Category 'wise-sayings' not found in database!") |
|||
return |
|||
|
|||
print(f"📁 Target Category: ID {category.id} | Slug: '{category.slug}' | Sect: {category.sect_id}") |
|||
|
|||
# 2. Get dependencies |
|||
statuses = list(HadisStatus.objects.all()) |
|||
transmitters_pool = list(Transmitters.objects.all()) |
|||
book_refs = list(BookReference.objects.all()) |
|||
|
|||
# Fallback status |
|||
default_status = statuses[0] if statuses else None |
|||
|
|||
# Current max number for hadiths |
|||
max_num = Hadis.objects.all().order_by("-number").values_list("number", flat=True).first() or 0 |
|||
|
|||
print(f"📊 Current Highest Hadis Number: {max_num}") |
|||
print(f"📚 Available Transmitters: {len(transmitters_pool)} | Book References: {len(book_refs)}") |
|||
|
|||
# 3. Build 50 hadiths |
|||
created_count = 0 |
|||
hadis_instances = [] |
|||
transmitter_relations = [] |
|||
reference_relations = [] |
|||
|
|||
all_templates = list(WISDOM_TEMPLATES) |
|||
# Expand templates if count > 10 |
|||
idx = 0 |
|||
while len(all_templates) < count: |
|||
base_t = ADDITIONAL_SAYINGS[idx % len(ADDITIONAL_SAYINGS)] |
|||
idx += 1 |
|||
num_variant = idx + 10 |
|||
all_templates.append({ |
|||
"narrator_ru": base_t[4], |
|||
"narrator_en": "Infallible Imam (peace be upon him)", |
|||
"narrator_fa": "معصوم (ع)", |
|||
"title_ru": f"{base_t[0]} (Часть {num_variant})", |
|||
"title_en": f"{base_t[1]} (Part {num_variant})", |
|||
"title_fa": f"{base_t[2]} (بخش {num_variant})", |
|||
"text": base_t[3], |
|||
"trans_ru": f"{base_t[0]}: {base_t[5]}", |
|||
"trans_en": f"{base_t[1]}: Wisdom regarding character and faith.", |
|||
"trans_fa": f"{base_t[2]}: رهنمودی گرانبها در اخلاق و دینداری.", |
|||
"exp_ru": base_t[5], |
|||
"exp_en": f"Spiritual contemplation on {base_t[1]}." |
|||
}) |
|||
|
|||
for i in range(count): |
|||
tmpl = all_templates[i] |
|||
curr_number = max_num + i + 1 |
|||
|
|||
title_json = [ |
|||
{"language_code": "ru", "title": tmpl["title_ru"], "text": tmpl["title_ru"]}, |
|||
{"language_code": "en", "title": tmpl["title_en"], "text": tmpl["title_en"]}, |
|||
{"language_code": "fa", "title": tmpl["title_fa"], "text": tmpl["title_fa"]}, |
|||
] |
|||
narrator_json = [ |
|||
{"language_code": "ru", "title": tmpl["narrator_ru"], "text": tmpl["narrator_ru"]}, |
|||
{"language_code": "en", "title": tmpl["narrator_en"], "text": tmpl["narrator_en"]}, |
|||
{"language_code": "fa", "title": tmpl["narrator_fa"], "text": tmpl["narrator_fa"]}, |
|||
] |
|||
translation_json = [ |
|||
{"language_code": "ru", "title": tmpl["trans_ru"], "text": tmpl["trans_ru"]}, |
|||
{"language_code": "en", "title": tmpl["trans_en"], "text": tmpl["trans_en"]}, |
|||
{"language_code": "fa", "title": tmpl["trans_fa"], "text": tmpl["trans_fa"]}, |
|||
] |
|||
explanation_json = [ |
|||
{"language_code": "ru", "text": tmpl["exp_ru"]}, |
|||
{"language_code": "en", "text": tmpl["exp_en"]}, |
|||
] |
|||
|
|||
# Generate unique readable slug |
|||
base_slug_text = tmpl["title_en"] or tmpl["title_ru"] |
|||
clean_slug = slugify(base_slug_text, allow_unicode=True).strip('-').lower() |
|||
if len(clean_slug) > 60: |
|||
clean_slug = clean_slug[:60].rstrip('-') |
|||
slug_candidate = f"{clean_slug}-{curr_number}" |
|||
|
|||
hadis = Hadis( |
|||
category=category, |
|||
number=curr_number, |
|||
slug=slug_candidate, |
|||
title=title_json, |
|||
title_narrator=narrator_json, |
|||
text=tmpl["text"], |
|||
translation=translation_json, |
|||
explanation=explanation_json, |
|||
hadis_status=random.choice(statuses) if statuses else default_status, |
|||
links={"dovodi": "https://dovodi.newhorizonco.uk/"}, |
|||
status=True, |
|||
share_link=f"https://dovodi.newhorizonco.uk/arguments/hadith/{slug_candidate}" |
|||
) |
|||
hadis_instances.append((hadis, tmpl)) |
|||
|
|||
print(f"✨ Prepared {len(hadis_instances)} Hadith records.") |
|||
|
|||
if dry_run: |
|||
print("\n[DRY RUN PREVIEW] First 5 Hadiths:") |
|||
for h, tmpl in hadis_instances[:5]: |
|||
print(f" • Hadis #{h.number:4d} | Slug: '{h.slug}' | Title: '{tmpl['title_en']}'") |
|||
print("\n✨ Dry run completed. No changes saved to database.") |
|||
return |
|||
|
|||
# Execute DB save |
|||
with transaction.atomic(): |
|||
print("💾 Saving Hadith records to database...") |
|||
# Save hadiths |
|||
saved_hadiths = [] |
|||
for h, tmpl in hadis_instances: |
|||
h.save() |
|||
saved_hadiths.append(h) |
|||
|
|||
# Attach transmitters and references |
|||
print("🔗 Linking transmitters and book references...") |
|||
for h in saved_hadiths: |
|||
# 2 to 4 transmitters |
|||
if transmitters_pool: |
|||
num_tr = min(len(transmitters_pool), random.randint(2, 4)) |
|||
sampled_tr = random.sample(transmitters_pool, num_tr) |
|||
gap_pos = random.randint(0, num_tr - 1) |
|||
|
|||
for pos, tr in enumerate(sampled_tr): |
|||
HadisTransmitter.objects.create( |
|||
hadis=h, |
|||
transmitter=tr, |
|||
chain_index=0, |
|||
order=pos, |
|||
is_gap=(pos == gap_pos) |
|||
) |
|||
|
|||
# 1 Book reference |
|||
if book_refs: |
|||
ref = random.choice(book_refs) |
|||
HadisReference.objects.create( |
|||
hadis=h, |
|||
book_reference=ref, |
|||
volume=str(random.randint(1, 8)), |
|||
pages=str(random.randint(15, 450)), |
|||
hadith_number=str(h.number) |
|||
) |
|||
|
|||
print("=" * 70) |
|||
print(f"✅ Successfully created {len(saved_hadiths)} Hadiths for category 'wise-sayings'!") |
|||
print(f"📌 Numbers range: #{saved_hadiths[0].number} to #{saved_hadiths[-1].number}") |
|||
|
|||
|
|||
def main(): |
|||
parser = argparse.ArgumentParser(description="Generate 50 Hadiths for the category 'wise-sayings'.") |
|||
parser.add_argument("--count", type=int, default=50, help="Number of hadiths to generate (default: 50).") |
|||
parser.add_argument("--dry-run", action="store_true", help="Preview without saving to DB.") |
|||
args = parser.parse_args() |
|||
|
|||
start_time = time.time() |
|||
generate_hadiths(count=args.count, dry_run=args.dry_run) |
|||
duration = time.time() - start_time |
|||
print(f"⏱ Completed in {duration:.2f} seconds.") |
|||
|
|||
|
|||
if __name__ == "__main__": |
|||
main() |
|||
@ -0,0 +1,95 @@ |
|||
import os |
|||
import sys |
|||
import argparse |
|||
import django |
|||
|
|||
sys.stdout.reconfigure(encoding='utf-8') |
|||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
|||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings.develop') |
|||
django.setup() |
|||
|
|||
from django.db import transaction |
|||
from django.db.models import Count |
|||
from apps.hadis.models import HadisCategory, Hadis |
|||
|
|||
def get_text(val): |
|||
if isinstance(val, list): |
|||
for item in val: |
|||
if isinstance(item, dict): |
|||
return item.get('text') or item.get('title') or str(item) |
|||
elif isinstance(val, dict): |
|||
return val.get('text') or val.get('title') or str(val) |
|||
return str(val or "") |
|||
|
|||
def resolve_category_conflicts(dry_run=True): |
|||
print("=" * 65) |
|||
print(" CATEGORY CONFLICT RESOLVER (HADITHS vs SUBCATEGORIES) ") |
|||
print(f" Mode: {'DRY RUN (Simulation only, no DB changes)' if dry_run else 'EXECUTE (Applying changes to DB)'}") |
|||
print("=" * 65) |
|||
|
|||
conflicted_categories = HadisCategory.objects.annotate( |
|||
hadis_count=Count('hadis', distinct=True), |
|||
children_count=Count('children', distinct=True) |
|||
).filter(hadis_count__gt=0, children_count__gt=0) |
|||
|
|||
total_conflicts = conflicted_categories.count() |
|||
print(f"\nFound {total_conflicts} categories violating the rule (having both Hadiths & Subcategories).\n") |
|||
|
|||
if total_conflicts == 0: |
|||
print("✅ No conflicts found. All categories adhere to the business rule.") |
|||
return |
|||
|
|||
deleted_subcats_count = 0 |
|||
reassigned_hadis_count = 0 |
|||
|
|||
with transaction.atomic(): |
|||
for idx, parent_cat in enumerate(conflicted_categories, 1): |
|||
parent_title = get_text(parent_cat.title) |
|||
print(f"[{idx}/{total_conflicts}] Category #{parent_cat.id} ('{parent_title}') | Slug: {parent_cat.slug}") |
|||
print(f" - Direct Hadiths: {parent_cat.hadis_count}") |
|||
print(f" - Subcategories count: {parent_cat.children_count}") |
|||
|
|||
# Get all descendants |
|||
descendants = list(parent_cat.get_descendants().order_by('-level')) |
|||
print(f" - Total subcategories/descendants to remove: {len(descendants)}") |
|||
|
|||
for subcat in descendants: |
|||
subcat_title = get_text(subcat.title) |
|||
sub_hadis = subcat.hadis_set.all() |
|||
sub_hadis_count = sub_hadis.count() |
|||
|
|||
if sub_hadis_count > 0: |
|||
print(f" ⚠️ Subcategory #{subcat.id} ('{subcat_title}') has {sub_hadis_count} Hadiths!") |
|||
print(f" -> Reassigning these {sub_hadis_count} Hadiths to parent Category #{parent_cat.id} before deletion.") |
|||
if not dry_run: |
|||
sub_hadis.update(category=parent_cat) |
|||
reassigned_hadis_count += sub_hadis_count |
|||
|
|||
print(f" 🗑️ Removing Subcategory #{subcat.id} ('{subcat_title}')") |
|||
if not dry_run: |
|||
subcat.delete() |
|||
deleted_subcats_count += 1 |
|||
|
|||
if dry_run: |
|||
print("\n[DRY RUN SUMMARY]") |
|||
print(f" - Categories inspected: {total_conflicts}") |
|||
print(f" - Subcategories that would be deleted: {deleted_subcats_count}") |
|||
print(f" - Hadiths that would be safeguarded/reassigned: {reassigned_hadis_count}") |
|||
print(" -> No changes were written to the database.") |
|||
else: |
|||
print("\nRebuilding MPTT category tree structure...") |
|||
HadisCategory.objects.rebuild() |
|||
print("✅ MPTT category tree successfully rebuilt.") |
|||
|
|||
print("\n[EXECUTION SUMMARY]") |
|||
print(f" - Conflicted categories resolved: {total_conflicts}") |
|||
print(f" - Subcategories deleted: {deleted_subcats_count}") |
|||
print(f" - Hadiths safeguarded: {reassigned_hadis_count}") |
|||
print("✅ All changes committed successfully.") |
|||
|
|||
if __name__ == '__main__': |
|||
parser = argparse.ArgumentParser(description="Resolve categories having both Hadiths and Subcategories") |
|||
parser.add_argument('--execute', action='store_true', help='Execute changes on database (default is dry-run)') |
|||
args = parser.parse_args() |
|||
|
|||
resolve_category_conflicts(dry_run=not args.execute) |
|||
@ -0,0 +1,234 @@ |
|||
#!/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() |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue