Files
guilan-ace-backend/apps/blog/management/commands/seed_blog_mock_data.py

339 lines
14 KiB
Python

from __future__ import annotations
import random
from io import BytesIO
from pathlib import Path
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.core.files.base import ContentFile
from django.core.management.base import BaseCommand
from django.utils import timezone
from apps.blog.models import BlogBanner, Category, Comment, Like, Post, SavedPost, Tag
from apps.blog.permissions import BLOG_EDITOR_GROUP
try:
from PIL import Image, ImageDraw
except ImportError: # pragma: no cover - command gracefully explains missing optional dependency.
Image = None
ImageDraw = None
User = get_user_model()
WRITERS = [
{
"username": "mock-blog-writer-ali",
"first_name": "علی",
"last_name": "کریمی",
"bio": "دانشجوی مهندسی کامپیوتر و علاقه‌مند به معماری نرم‌افزار، لینوکس و تجربه‌های واقعی تیمی.",
},
{
"username": "mock-blog-writer-sara",
"first_name": "سارا",
"last_name": "احمدی",
"bio": "نویسنده حوزه تجربه کاربری، فرانت‌اند و یادگیری کاربردی برای دانشجویان تازه‌وارد.",
},
{
"username": "mock-blog-writer-nima",
"first_name": "نیما",
"last_name": "رضایی",
"bio": "علاقه‌مند به الگوریتم، بک‌اند و انتقال تجربه‌های مسابقه‌ای به پروژه‌های واقعی.",
},
]
TAG_NAMES = [
"پایتون",
"فرانت‌اند",
"بک‌اند",
"الگوریتم",
"هوش مصنوعی",
"تجربه دانشجویی",
"مسیر شغلی",
"لینوکس",
]
POSTS = [
{
"title": "چطور یک پروژه دانشجویی را مثل محصول واقعی جلو ببریم؟",
"slug": "mock-پروژه-دانشجویی-محصول-واقعی",
"category": "توسعه نرم‌افزار",
"tags": ["بک‌اند", "فرانت‌اند", "تجربه دانشجویی"],
},
{
"title": "راهنمای شروع پایتون برای دانشجویان مهندسی کامپیوتر",
"slug": "mock-شروع-پایتون-برای-دانشجویان",
"category": "برنامه‌نویسی",
"tags": ["پایتون", "مسیر شغلی"],
},
{
"title": "الگوریتم‌ها را چطور کاربردی یاد بگیریم؟",
"slug": "mock-یادگیری-کاربردی-الگوریتم",
"category": "علوم کامپیوتر",
"tags": ["الگوریتم", "تجربه دانشجویی"],
},
{
"title": "از ترمینال نترسیم: لینوکس برای زندگی روزمره دانشجویی",
"slug": "mock-لینوکس-برای-دانشجویان",
"category": "ابزارها",
"tags": ["لینوکس", "مسیر شغلی"],
},
{
"title": "هوش مصنوعی در پروژه‌های کوچک دانشجویی",
"slug": "mock-هوش-مصنوعی-پروژه-دانشجویی",
"category": "هوش مصنوعی",
"tags": ["هوش مصنوعی", "پایتون"],
},
]
def make_markdown(title: str) -> str:
return f"""# {title}
این نوشته برای تست نمای واقعی بلاگ ساخته شده است. متن عمداً چند بخش دارد تا فهرست محتوا، خوانایی، کدبلاک و کامنت‌ها در صفحه جزئیات بهتر دیده شوند.
## مسئله از کجا شروع می‌شود؟
وقتی یک تیم دانشجویی روی پروژه کار می‌کند، معمولاً تمرکز اصلی روی تمام کردن سریع کار است. اما اگر کمی ساختار داشته باشیم، خروجی هم قابل ارائه‌تر می‌شود و هم بعداً قابل توسعه خواهد بود.
## یک نمونه کد کوتاه
```python
def normalize_title(title: str) -> str:
return "-".join(title.strip().lower().split())
print(normalize_title("Guilan ACE Blog"))
```
## پیشنهاد عملی
- ابتدا مسئله را واضح بنویسید.
- کارها را کوچک و قابل بررسی کنید.
- خروجی هر مرحله را مستند کنید.
- بازخورد گرفتن را به آخر کار موکول نکنید.
### نکته تکمیلی
اگر نوشته شامل تصویر، کد یا لینک است، بهتر است ساختار آن از ابتدا با تیترهای واضح جدا شود تا کاربر بتواند سریع‌تر بخش موردنظرش را پیدا کند.
"""
def make_image_bytes(label: str, width: int, height: int, color: tuple[int, int, int]) -> bytes:
if Image is None or ImageDraw is None:
raise RuntimeError("Pillow is required to generate mock images.")
image = Image.new("RGB", (width, height), color)
draw = ImageDraw.Draw(image)
for index in range(0, width, 48):
draw.line((index, 0, index - height, height), fill=(255, 255, 255), width=2)
draw.rectangle((32, height - 112, width - 32, height - 32), fill=(20, 24, 38))
draw.text((52, height - 84), label[:70], fill=(255, 255, 255))
output = BytesIO()
image.save(output, format="JPEG", quality=88)
return output.getvalue()
def set_image_field(instance, field_name: str, path: str, label: str, width: int, height: int, color: tuple[int, int, int]):
field = getattr(instance, field_name)
if field:
return
image_bytes = make_image_bytes(label, width, height, color)
field.save(path, ContentFile(image_bytes), save=False)
class Command(BaseCommand):
help = "Seed rich mock blog data for local visual QA."
def add_arguments(self, parser):
parser.add_argument("--reset", action="store_true", help="Delete previous mock blog data before seeding.")
parser.add_argument("--password", default="MockPass12345!", help="Password for generated writer users.")
def handle(self, *args, **options):
if Image is None:
raise RuntimeError("Pillow is required. Install project requirements before running this command.")
random.seed(42)
if options["reset"]:
self._reset_mock_data()
editor_group, _ = Group.objects.get_or_create(name=BLOG_EDITOR_GROUP)
writers = self._seed_writers(editor_group, options["password"])
categories = self._seed_categories()
tags = self._seed_tags()
self._seed_banners()
posts = self._seed_posts(writers, categories, tags)
self._seed_comments_and_reactions(posts, writers)
self.stdout.write(self.style.SUCCESS("Mock blog data seeded successfully."))
self.stdout.write("Writer login usernames:")
for writer in writers:
self.stdout.write(f" - {writer.username} / {options['password']}")
def _reset_mock_data(self):
Post.all_objects.filter(slug__startswith="mock-").delete()
BlogBanner.all_objects.filter(title__startswith="Mock ").delete()
Category.all_objects.filter(slug__startswith="mock-").delete()
Tag.all_objects.filter(slug__startswith="mock-").delete()
User.objects.filter(username__startswith="mock-blog-writer-").delete()
def _seed_writers(self, editor_group: Group, password: str):
writers = []
for index, spec in enumerate(WRITERS, start=1):
user, created = User.objects.get_or_create(
username=spec["username"],
defaults={
"first_name": spec["first_name"],
"last_name": spec["last_name"],
"email": f"{spec['username']}@example.local",
"mobile": f"09199000{index:03d}",
"bio": spec["bio"],
"is_active": True,
"is_mobile_verified": True,
},
)
user.first_name = spec["first_name"]
user.last_name = spec["last_name"]
user.bio = spec["bio"]
user.is_active = True
user.is_mobile_verified = True
if created:
user.set_password(password)
set_image_field(
user,
"profile_picture",
f"profile_pictures/mock-writer-{index}.jpg",
spec["first_name"],
512,
512,
(42 + index * 30, 95 + index * 20, 130 + index * 15),
)
user.save()
user.groups.add(editor_group)
writers.append(user)
return writers
def _seed_categories(self):
root, _ = Category.objects.get_or_create(
slug="mock-بلاگ-انجمن",
defaults={"name": "بلاگ انجمن", "description": "دسته اصلی محتوای تستی بلاگ"},
)
names = ["برنامه‌نویسی", "علوم کامپیوتر", "توسعه نرم‌افزار", "ابزارها", "هوش مصنوعی"]
categories = {"بلاگ انجمن": root}
for name in names:
category, _ = Category.objects.get_or_create(
slug=f"mock-{name}",
defaults={"name": name, "parent": root, "description": f"مطالب تستی درباره {name}"},
)
category.name = name
category.parent = root
category.save()
categories[name] = category
return categories
def _seed_tags(self):
tags = {}
for name in TAG_NAMES:
tag, _ = Tag.objects.get_or_create(slug=f"mock-{name}", defaults={"name": name})
tag.name = name
tag.save()
tags[name] = tag
return tags
def _seed_banners(self):
colors = [(9, 80, 90), (120, 64, 24), (38, 70, 83)]
for index in range(1, 4):
banner, _ = BlogBanner.objects.get_or_create(
title=f"Mock Blog Banner {index}",
defaults={
"url": f"https://east-guilan-ce.ir/blog?mock-banner={index}",
"alt_text": f"بنر تستی بلاگ {index}",
"sort_order": index,
"is_active": True,
},
)
banner.url = f"https://east-guilan-ce.ir/blog?mock-banner={index}"
banner.alt_text = f"بنر تستی بلاگ {index}"
banner.sort_order = index
banner.is_active = True
set_image_field(
banner,
"image",
f"blog/banners/mock-banner-{index}.jpg",
f"Mock Banner {index}",
1440,
320,
colors[index - 1],
)
banner.save()
def _seed_posts(self, writers, categories, tags):
posts = []
for index, spec in enumerate(POSTS, start=1):
writer_pool = writers[: 1 + (index % len(writers))]
post, _ = Post.all_objects.get_or_create(
slug=spec["slug"],
defaults={
"title": spec["title"],
"author": writer_pool[0],
"content": make_markdown(spec["title"]),
"excerpt": f"خلاصه تستی برای نوشته «{spec['title']}» که برای بررسی کارت‌ها و سئوی بلاگ استفاده می‌شود.",
"status": Post.StatusChoices.PUBLISHED,
"category": categories[spec["category"]],
"is_featured": index <= 2,
"seo_title": spec["title"][:70],
"seo_description": f"توضیح سئوی تستی برای {spec['title']}",
"og_title": spec["title"][:95],
"og_description": f"متن شبکه‌های اجتماعی برای {spec['title']}",
"focus_keyword": spec["tags"][0],
"published_at": timezone.now() - timezone.timedelta(days=index * 3),
},
)
post.title = spec["title"]
post.author = writer_pool[0]
post.content = make_markdown(spec["title"])
post.excerpt = f"خلاصه تستی برای نوشته «{spec['title']}» که برای بررسی کارت‌ها و سئوی بلاگ استفاده می‌شود."
post.status = Post.StatusChoices.PUBLISHED
post.category = categories[spec["category"]]
post.is_featured = index <= 2
post.published_at = post.published_at or timezone.now() - timezone.timedelta(days=index * 3)
set_image_field(
post,
"featured_image",
f"blog/featured/mock-post-{index}.jpg",
spec["title"],
1280,
720,
(25 + index * 28, 90 + index * 18, 120 + index * 12),
)
post.save()
post.tags.set([tags[name] for name in spec["tags"]])
post.writers.set(writer_pool)
posts.append(post)
return posts
def _seed_comments_and_reactions(self, posts, writers):
for post in posts:
for index, writer in enumerate(writers, start=1):
if writer == post.author:
continue
comment, _ = Comment.objects.get_or_create(
post=post,
author=writer,
parent=None,
defaults={"content": f"کامنت تستی {index}: این بخش برای بررسی ظاهر کامنت‌ها و پاسخ‌ها ساخته شده است."},
)
Comment.objects.get_or_create(
post=post,
author=post.author,
parent=comment,
defaults={"content": "پاسخ تستی نویسنده برای بررسی حالت nested در کامنت‌ها."},
)
Like.objects.get_or_create(post=post, user=writer)
if index % 2 == 0:
SavedPost.objects.get_or_create(post=post, user=writer)