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)