339 lines
14 KiB
Python
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)
|