|
|
|
@ -1,11 +1,11 @@ |
|
|
|
import os |
|
|
|
import random |
|
|
|
from django.core.management.base import BaseCommand |
|
|
|
from django.core.files import File |
|
|
|
from django.core.files.base import ContentFile |
|
|
|
from django.conf import settings |
|
|
|
from django.db import transaction |
|
|
|
from django.utils.text import slugify |
|
|
|
|
|
|
|
# REPLACE 'apps.article.models' WITH YOUR ACTUAL APP PATH |
|
|
|
# REPLACE WITH YOUR ACTUAL APP PATHS |
|
|
|
from apps.article.models import ( |
|
|
|
Article, |
|
|
|
ArticleCategory, |
|
|
|
@ -17,107 +17,182 @@ from apps.article.models import ( |
|
|
|
) |
|
|
|
|
|
|
|
class Command(BaseCommand): |
|
|
|
help = 'Seeds 25 articles with categories, collections, and structured content.' |
|
|
|
help = 'Seeds 25 rich Islamic articles (No Atomic Transaction).' |
|
|
|
|
|
|
|
def handle(self, *args, **kwargs): |
|
|
|
self.stdout.write('Seeding Articles...') |
|
|
|
self.stdout.write('--- Starting Rich Article Seeding ---') |
|
|
|
|
|
|
|
# 1. Configuration |
|
|
|
TOTAL_ARTICLES = 25 |
|
|
|
# ========================================== |
|
|
|
# 1. CONFIGURATION |
|
|
|
# ========================================== |
|
|
|
TOTAL_ARTICLES_TO_CREATE = 25 |
|
|
|
CATEGORY_IDS = [6, 7, 8, 9] |
|
|
|
CONTENT_SOURCE_IDS = range(13, 24) # IDs 13 to 23 |
|
|
|
PROTECTED_SLUG = "امام-حسین-ع-از-ولادت-تا-جاودانگی-تحلیلی-بر-مبانی-ق" |
|
|
|
|
|
|
|
IMAGE_DIR = os.path.join(settings.BASE_DIR, 'seeds', 'images') |
|
|
|
IMAGE_NAMES = [f'articl{i}.jpg' for i in range(2, 5)] |
|
|
|
|
|
|
|
# 2. Fetch Existing Dependencies |
|
|
|
# Explicit file names based on your folder structure |
|
|
|
IMAGE_NAMES = ['articl2.jpg', 'articl3.jpg', 'articl4.jpg'] |
|
|
|
|
|
|
|
RICH_DATA = [ |
|
|
|
{ |
|
|
|
'title_en': 'The Event of Ghadir Khumm: Completion of Religion', |
|
|
|
'title_fa': 'واقعه غدیر خم: کمال دین و اتمام نعمت', |
|
|
|
'summary': 'An analysis of the sermon of the Prophet (PBUH) at Ghadir Khumm.', |
|
|
|
'arabic': 'مَنْ كُنْتُ مَوْلَاهُ فَهَذَا عَلِيٌّ مَوْلَاهُ', |
|
|
|
'trans': 'For whomever I am his Master (Mawla), then Ali is his Master.', |
|
|
|
'content_body': 'The event of Ghadir is one of the most undeniable historical events in Islam...' |
|
|
|
}, |
|
|
|
{ |
|
|
|
'title_en': 'Lady Fatimah (SA): The Role Model for Women', |
|
|
|
'title_fa': 'حضرت فاطمه زهرا (س): الگوی زنان عالم', |
|
|
|
'summary': 'Exploring the virtues of the daughter of the Prophet.', |
|
|
|
'arabic': 'فَاطِمَةُ بَضْعَةٌ مِنِّي', |
|
|
|
'trans': 'Fatimah is a part of me.', |
|
|
|
'content_body': 'Lady Fatimah az-Zahra (SA) is not just a role model for women...' |
|
|
|
}, |
|
|
|
{ |
|
|
|
'title_en': 'Nahj al-Balagha: The Peak of Eloquence', |
|
|
|
'title_fa': 'نهج البلاغه: قله فصاحت و بلاغت', |
|
|
|
'summary': 'A look into the sermons of Imam Ali (AS).', |
|
|
|
'arabic': 'سَلُونِي قَبْلَ أَنْ تَفْقِدُونِي', |
|
|
|
'trans': 'Ask me before you lose me.', |
|
|
|
'content_body': 'Nahj al-Balagha contains deep philosophical guidance...' |
|
|
|
}, |
|
|
|
{ |
|
|
|
'title_en': 'The Significance of Laylat al-Qadr', |
|
|
|
'title_fa': 'عظمت شب قدر', |
|
|
|
'summary': 'Why is the Night of Power better than a thousand months?', |
|
|
|
'arabic': 'إِنَّا أَنزَلْنَاهُ فِي لَيْلَةِ الْقَدْرِ', |
|
|
|
'trans': 'Indeed, We sent the Qur\'an down during the Night of Decree.', |
|
|
|
'content_body': 'The Night of Qadr is the night where destiny is written...' |
|
|
|
}, |
|
|
|
{ |
|
|
|
'title_en': 'Imam Mahdi (AJ): The Savior', |
|
|
|
'title_fa': 'امام مهدی (عج): منجی بشریت', |
|
|
|
'summary': 'The concept of the Awaited One in Islamic eschatology.', |
|
|
|
'arabic': 'بَقِيَّتُ اللَّهِ خَيْرٌ لَكُمْ', |
|
|
|
'trans': 'The remnant of Allah is better for you.', |
|
|
|
'content_body': 'The belief in a savior is universal...' |
|
|
|
}, |
|
|
|
{ |
|
|
|
'title_en': 'Rights of Parents', |
|
|
|
'title_fa': 'حقوق والدین', |
|
|
|
'summary': 'Examining the status of parents.', |
|
|
|
'arabic': 'وَبِالْوَالِدَيْنِ إِحْسَانًا', |
|
|
|
'trans': 'And be good to parents.', |
|
|
|
'content_body': 'Respecting parents is placed immediately after worship of God...' |
|
|
|
} |
|
|
|
] |
|
|
|
|
|
|
|
# ========================================== |
|
|
|
# 2. CLEANUP |
|
|
|
# ========================================== |
|
|
|
self.stdout.write("Cleaning old articles (preserving protected one)...") |
|
|
|
deleted_count, _ = Article.objects.exclude(slug=PROTECTED_SLUG).delete() |
|
|
|
ArticleCollection.objects.all().delete() |
|
|
|
self.stdout.write(self.style.WARNING(f"Deleted {deleted_count} articles.")) |
|
|
|
|
|
|
|
# ========================================== |
|
|
|
# 3. PREPARE DEPENDENCIES |
|
|
|
# ========================================== |
|
|
|
categories = list(ArticleCategory.objects.filter(id__in=CATEGORY_IDS)) |
|
|
|
if not categories: |
|
|
|
self.stdout.write(self.style.WARNING(f"Categories with IDs {CATEGORY_IDS} not found. Skipping category assignment.")) |
|
|
|
|
|
|
|
# 3. Create 4 New Collections |
|
|
|
collection_names = [ |
|
|
|
("Islamic History", "تاریخ اسلام"), |
|
|
|
("Theology & Beliefs", "عقاید و کلام"), |
|
|
|
("Ethics & Spirituality", "اخلاق و عرفان"), |
|
|
|
("Quran & Hadith", "قرآن و حدیث") |
|
|
|
] |
|
|
|
collections = [] |
|
|
|
for i in range(1, 5): |
|
|
|
col, created = ArticleCollection.objects.get_or_create( |
|
|
|
title=f'Seeded Collection {i}', |
|
|
|
defaults={ |
|
|
|
'slug': f'seeded-collection-{i}', |
|
|
|
'summary': f'This is a generated summary for collection {i}', |
|
|
|
'display_position': ArticleCollection.DisplayPosition.MIDDLE |
|
|
|
} |
|
|
|
for en_name, fa_name in collection_names: |
|
|
|
col = ArticleCollection.objects.create( |
|
|
|
title=en_name, |
|
|
|
slug=slugify(en_name), |
|
|
|
summary=f'Collection of {en_name}', |
|
|
|
display_position=ArticleCollection.DisplayPosition.MIDDLE |
|
|
|
) |
|
|
|
collections.append(col) |
|
|
|
self.stdout.write(self.style.SUCCESS(f'Created/Loaded {len(collections)} collections.')) |
|
|
|
|
|
|
|
# 4. Fetch Source Content for Cloning |
|
|
|
# NOTE: Since ArticleContent is a ForeignKey (One-to-Many), we cannot |
|
|
|
# link existing content objects to new articles without removing them |
|
|
|
# from their old articles. Instead, we will CLONE their data. |
|
|
|
source_contents = list(ArticleContent.objects.filter(id__in=CONTENT_SOURCE_IDS)) |
|
|
|
if not source_contents: |
|
|
|
self.stdout.write(self.style.WARNING(f"ArticleContents with IDs 13-23 not found. Creating generic content instead.")) |
|
|
|
|
|
|
|
# 5. Main Loop: Create Articles |
|
|
|
with transaction.atomic(): |
|
|
|
for i in range(1, TOTAL_ARTICLES + 1): |
|
|
|
title = f'Seeded Article Title {i}' |
|
|
|
|
|
|
|
# Create Article |
|
|
|
article = Article.objects.create( |
|
|
|
title=title, |
|
|
|
description=f'Description for article {i}. Lorem ipsum dolor sit amet.', |
|
|
|
content=f'Full content body for article {i}.', |
|
|
|
status=True |
|
|
|
) |
|
|
|
|
|
|
|
# Assign Thumbnail (Cycle through images 1-5) |
|
|
|
img_name = IMAGE_NAMES[(i - 1) % len(IMAGE_NAMES)] |
|
|
|
img_path = os.path.join(IMAGE_DIR, img_name) |
|
|
|
|
|
|
|
if os.path.exists(img_path): |
|
|
|
|
|
|
|
# ========================================== |
|
|
|
# 4. MAIN CREATION LOOP (NO ATOMIC TRANSACTION) |
|
|
|
# ========================================== |
|
|
|
self.stdout.write("Starting creation loop...") |
|
|
|
|
|
|
|
for i in range(1, TOTAL_ARTICLES_TO_CREATE + 1): |
|
|
|
|
|
|
|
# Select Data |
|
|
|
data = RICH_DATA[(i - 1) % len(RICH_DATA)] |
|
|
|
|
|
|
|
# Construct Unique Data |
|
|
|
final_title = f"{data['title_en']} (Vol {i})" |
|
|
|
short_slug_base = slugify(data['title_en'])[:30] |
|
|
|
unique_slug = f"{short_slug_base}-{i}" |
|
|
|
|
|
|
|
if unique_slug == PROTECTED_SLUG: |
|
|
|
unique_slug = f"{unique_slug}-generated" |
|
|
|
|
|
|
|
# 1. Create Article Object |
|
|
|
article = Article.objects.create( |
|
|
|
title=final_title, |
|
|
|
slug=unique_slug, |
|
|
|
description=data['summary'], |
|
|
|
content=data['content_body'], |
|
|
|
status=True |
|
|
|
) |
|
|
|
|
|
|
|
# 2. Handle Image |
|
|
|
img_name = IMAGE_NAMES[(i - 1) % len(IMAGE_NAMES)] |
|
|
|
img_path = os.path.join(IMAGE_DIR, img_name) |
|
|
|
|
|
|
|
if os.path.exists(img_path): |
|
|
|
try: |
|
|
|
with open(img_path, 'rb') as f: |
|
|
|
article.thumbnail.save(img_name, File(f), save=True) |
|
|
|
else: |
|
|
|
self.stdout.write(self.style.WARNING(f"Image not found at {img_path}")) |
|
|
|
|
|
|
|
# Assign Categories (Randomly pick 1 or 2) |
|
|
|
if categories: |
|
|
|
article.categories.add(*random.sample(categories, k=random.randint(1, 2))) |
|
|
|
|
|
|
|
# Assign to Collections (Randomly pick 1) |
|
|
|
random_col = random.choice(collections) |
|
|
|
ArticleInCollection.objects.create( |
|
|
|
article=article, |
|
|
|
collection=random_col, |
|
|
|
order=i |
|
|
|
) |
|
|
|
|
|
|
|
# Create Article Content (Cloning from IDs 13-23 or Generic) |
|
|
|
if source_contents: |
|
|
|
# Pick a random template from the requested IDs |
|
|
|
template = random.choice(source_contents) |
|
|
|
content_title = f"{template.title} (Copy for Article {i})" |
|
|
|
content_body = template.content |
|
|
|
else: |
|
|
|
content_title = f"Structured Content for Article {i}" |
|
|
|
content_body = "This is a section of structured content." |
|
|
|
|
|
|
|
# Create the ArticleContent object linked to this NEW article |
|
|
|
ac = ArticleContent.objects.create( |
|
|
|
article=article, |
|
|
|
title=content_title, |
|
|
|
content=content_body, |
|
|
|
priority=1 |
|
|
|
) |
|
|
|
|
|
|
|
# Create nested ContentParts and TextSections |
|
|
|
part = ContentPart.objects.create(article_content=ac, order=1) |
|
|
|
TextSection.objects.create( |
|
|
|
content_part=part, |
|
|
|
arabic_text="نص تجريبي باللغة العربية", |
|
|
|
translation="Experimental text in English translation", |
|
|
|
order=1 |
|
|
|
) |
|
|
|
|
|
|
|
self.stdout.write(f'Created Article: {article.title}') |
|
|
|
|
|
|
|
self.stdout.write(self.style.SUCCESS(f'Successfully seeded {TOTAL_ARTICLES} articles!')) |
|
|
|
file_content = f.read() |
|
|
|
|
|
|
|
# NOTE: If the script hangs here, check Article.save() signals |
|
|
|
article.thumbnail.save(img_name, ContentFile(file_content), save=True) |
|
|
|
except Exception as e: |
|
|
|
self.stdout.write(self.style.ERROR(f"Error saving image for item {i}: {e}")) |
|
|
|
else: |
|
|
|
self.stdout.write(self.style.WARNING(f"Image missing: {img_path}")) |
|
|
|
|
|
|
|
# 3. Assign Categories |
|
|
|
if categories: |
|
|
|
article.categories.add(*random.sample(categories, k=random.randint(1, 2))) |
|
|
|
|
|
|
|
# 4. Assign Collection |
|
|
|
random_col = random.choice(collections) |
|
|
|
ArticleInCollection.objects.create( |
|
|
|
article=article, |
|
|
|
collection=random_col, |
|
|
|
order=i |
|
|
|
) |
|
|
|
|
|
|
|
# 5. Create Content Hierarchy |
|
|
|
ac = ArticleContent.objects.create( |
|
|
|
article=article, |
|
|
|
title=data['title_fa'], |
|
|
|
content=data['content_body'], |
|
|
|
priority=1 |
|
|
|
) |
|
|
|
|
|
|
|
part = ContentPart.objects.create(article_content=ac, order=1) |
|
|
|
|
|
|
|
TextSection.objects.create( |
|
|
|
content_part=part, |
|
|
|
arabic_text=data['arabic'], |
|
|
|
translation=data['trans'], |
|
|
|
order=1 |
|
|
|
) |
|
|
|
|
|
|
|
TextSection.objects.create( |
|
|
|
content_part=part, |
|
|
|
arabic_text=f"شرح: {data['title_fa']}", |
|
|
|
translation=f"Commentary on {data['title_en']}", |
|
|
|
order=2 |
|
|
|
) |
|
|
|
|
|
|
|
self.stdout.write(f"[{i}/{TOTAL_ARTICLES_TO_CREATE}] Created: {unique_slug}") |
|
|
|
|
|
|
|
self.stdout.write(self.style.SUCCESS(f'Successfully seeded {TOTAL_ARTICLES_TO_CREATE} rich articles.')) |