feat: add mock content initialization command
This commit is contained in:
1
apps/content/management/__init__.py
Normal file
1
apps/content/management/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
1
apps/content/management/commands/__init__.py
Normal file
1
apps/content/management/commands/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
|
||||||
182
apps/content/management/commands/seed_mock_content.py
Normal file
182
apps/content/management/commands/seed_mock_content.py
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
from django.contrib.admin.models import CHANGE, LogEntry
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.management.base import BaseCommand
|
||||||
|
from django.db import DEFAULT_DB_ALIAS, connections, transaction
|
||||||
|
|
||||||
|
from apps.content.current import AboutContent, AboutItem, HomeContent, ListingContent, SiteContent
|
||||||
|
|
||||||
|
|
||||||
|
SITE_MOCK = {
|
||||||
|
"official_name": "نام انجمن نمونه",
|
||||||
|
"short_name": "انجمن نمونه",
|
||||||
|
"faculty_subline": "دانشکده نمونه",
|
||||||
|
"description": "این متن نمونه است و پس از ورود محتوای اصلی از پنل مدیریت جایگزین میشود.",
|
||||||
|
"logo": "",
|
||||||
|
"logo_alt": "",
|
||||||
|
"nav_home": "خانه",
|
||||||
|
"nav_about": "درباره ما",
|
||||||
|
"nav_events": "رویدادها",
|
||||||
|
"nav_blog": "بلاگ",
|
||||||
|
"email": "contact@example.invalid",
|
||||||
|
"phone": "0000000000",
|
||||||
|
"telegram_url": "https://example.invalid/telegram",
|
||||||
|
"instagram_url": "https://example.invalid/instagram",
|
||||||
|
"website_url": "https://example.invalid",
|
||||||
|
"address": "نشانی نمونه",
|
||||||
|
"footer_copy": "متن نمونه پابرگ؛ محتوای نهایی را از پنل مدیریت وارد کنید.",
|
||||||
|
"seo_title": "عنوان نمونه سایت",
|
||||||
|
"seo_description": "توضیحات نمونه برای موتورهای جستوجو.",
|
||||||
|
"seo_image": "",
|
||||||
|
"seo_image_alt": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
HOME_MOCK = {
|
||||||
|
"title": "عنوان نمونه صفحه اصلی",
|
||||||
|
"intro": "این متن نمونه صفحه اصلی است و پس از ورود محتوای اصلی جایگزین میشود.",
|
||||||
|
"hero_heading": "عنوان نمونه صفحه اصلی",
|
||||||
|
"hero_body": "توضیحات نمونه برای معرفی انجمن و فعالیتهای آن.",
|
||||||
|
"hero_image": "",
|
||||||
|
"hero_image_alt": "",
|
||||||
|
"hero_action_label": "مشاهده نمونهها",
|
||||||
|
"featured_events_heading": "رویدادهای نمونه",
|
||||||
|
"featured_events_intro": "توضیحات نمونه برای بخش رویدادها.",
|
||||||
|
"featured_events_link_label": "همه رویدادها",
|
||||||
|
"featured_posts_heading": "نوشتههای نمونه",
|
||||||
|
"featured_posts_intro": "توضیحات نمونه برای بخش نوشتهها.",
|
||||||
|
"featured_posts_link_label": "همه نوشتهها",
|
||||||
|
"participation_heading": "به ما بپیوندید",
|
||||||
|
"participation_body": "این متن نمونه راههای مشارکت را توضیح میدهد.",
|
||||||
|
"participation_image": "",
|
||||||
|
"participation_image_alt": "",
|
||||||
|
"participation_action_label": "اطلاعات بیشتر",
|
||||||
|
"seo_title": "عنوان نمونه صفحه اصلی",
|
||||||
|
"seo_description": "توضیحات نمونه صفحه اصلی برای موتورهای جستوجو.",
|
||||||
|
"seo_image": "",
|
||||||
|
"seo_image_alt": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
ABOUT_MOCK = {
|
||||||
|
"title": "عنوان نمونه صفحه درباره ما",
|
||||||
|
"intro": "این متن نمونه معرفی انجمن است و بعداً از پنل مدیریت قابل ویرایش است.",
|
||||||
|
"identity_heading": "معرفی نمونه",
|
||||||
|
"identity_body": "اطلاعات نمونه درباره هویت انجمن.",
|
||||||
|
"mission_heading": "ماموریت نمونه",
|
||||||
|
"mission_body": "توضیحات نمونه درباره ماموریت انجمن.",
|
||||||
|
"activities_heading": "فعالیتهای نمونه",
|
||||||
|
"activities_body": "توضیحات نمونه درباره فعالیتها.",
|
||||||
|
"values_heading": "ارزشهای نمونه",
|
||||||
|
"values_body": "یادگیری، همکاری و مسئولیتپذیری نمونه.",
|
||||||
|
"committees_heading": "کمیتههای نمونه",
|
||||||
|
"committees_body": "اطلاعات نمونه درباره کمیتهها.",
|
||||||
|
"affiliations_heading": "همکاریهای نمونه",
|
||||||
|
"affiliations_body": "اطلاعات نمونه درباره گروههای همکار.",
|
||||||
|
"faq_heading": "پرسشهای نمونه",
|
||||||
|
"contact_heading": "راههای ارتباطی نمونه",
|
||||||
|
"seo_title": "عنوان نمونه صفحه درباره ما",
|
||||||
|
"seo_description": "توضیحات نمونه صفحه درباره ما برای موتورهای جستوجو.",
|
||||||
|
"seo_image": "",
|
||||||
|
"seo_image_alt": "",
|
||||||
|
}
|
||||||
|
|
||||||
|
LISTING_MOCKS = {
|
||||||
|
"events": {
|
||||||
|
"title": "عنوان نمونه رویدادها",
|
||||||
|
"intro": "این متن نمونه برای معرفی فهرست رویدادهاست.",
|
||||||
|
"seo_title": "عنوان نمونه رویدادها",
|
||||||
|
"seo_description": "توضیحات نمونه صفحه رویدادها برای موتورهای جستوجو.",
|
||||||
|
"seo_image": "",
|
||||||
|
"seo_image_alt": "",
|
||||||
|
},
|
||||||
|
"blog": {
|
||||||
|
"title": "عنوان نمونه بلاگ",
|
||||||
|
"intro": "این متن نمونه برای معرفی نوشتههای بلاگ است.",
|
||||||
|
"seo_title": "عنوان نمونه بلاگ",
|
||||||
|
"seo_description": "توضیحات نمونه صفحه بلاگ برای موتورهای جستوجو.",
|
||||||
|
"seo_image": "",
|
||||||
|
"seo_image_alt": "",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ABOUT_ITEM_MOCKS = (
|
||||||
|
("mission", "ماموریت نمونه", "این متن نمونه برای توضیح ماموریت انجمن است."),
|
||||||
|
("activities", "فعالیت نمونه", "این متن نمونه یکی از فعالیتهای انجمن را معرفی میکند."),
|
||||||
|
("values", "ارزش نمونه", "این متن نمونه یکی از ارزشهای انجمن را توضیح میدهد."),
|
||||||
|
("committees", "کمیته نمونه", "این متن نمونه برای معرفی یک کمیته است."),
|
||||||
|
("affiliations", "همکاری نمونه", "این متن نمونه برای معرفی یک گروه همکار است."),
|
||||||
|
("faq", "پرسش نمونه", "پاسخ نمونه؛ اطلاعات نهایی را از پنل مدیریت وارد کنید."),
|
||||||
|
)
|
||||||
|
|
||||||
|
RECORDS = (
|
||||||
|
("site", SiteContent, {"pk": 1}, SITE_MOCK),
|
||||||
|
("home", HomeContent, {"pk": 1}, HOME_MOCK),
|
||||||
|
("about", AboutContent, {"pk": 1}, ABOUT_MOCK),
|
||||||
|
("events", ListingContent, {"pk": "events"}, LISTING_MOCKS["events"]),
|
||||||
|
("blog", ListingContent, {"pk": "blog"}, LISTING_MOCKS["blog"]),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def has_admin_change(model, object_id, using):
|
||||||
|
content_type = ContentType.objects.db_manager(using).get_for_model(model)
|
||||||
|
return LogEntry.objects.using(using).filter(
|
||||||
|
content_type=content_type,
|
||||||
|
object_id=str(object_id),
|
||||||
|
action_flag=CHANGE,
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseCommand):
|
||||||
|
help = "Fill untouched site content records with obvious sample copy."
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--database",
|
||||||
|
default=DEFAULT_DB_ALIAS,
|
||||||
|
choices=tuple(connections),
|
||||||
|
help="Database alias to update.",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--dry-run",
|
||||||
|
action="store_true",
|
||||||
|
help="Report intended changes without writing to the database.",
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, database, dry_run, **options):
|
||||||
|
updated = 0
|
||||||
|
skipped = 0
|
||||||
|
prefix = "Dry run: " if dry_run else ""
|
||||||
|
|
||||||
|
with transaction.atomic(using=database):
|
||||||
|
for label, model, lookup, values in RECORDS:
|
||||||
|
record = model.objects.using(database).select_for_update().filter(**lookup).first()
|
||||||
|
object_id = lookup["pk"]
|
||||||
|
if record is not None and has_admin_change(model, object_id, database):
|
||||||
|
self.stdout.write(f"Skipped {label}: it has a Django admin change entry.")
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if dry_run:
|
||||||
|
action = "create" if record is None else "replace"
|
||||||
|
self.stdout.write(f"Dry run: would {action} {label} with mock content.")
|
||||||
|
updated += 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
if record is None:
|
||||||
|
record = model(**lookup)
|
||||||
|
for field, value in values.items():
|
||||||
|
setattr(record, field, value)
|
||||||
|
record.save(using=database)
|
||||||
|
updated += 1
|
||||||
|
self.stdout.write(f"Loaded mock content for {label}.")
|
||||||
|
|
||||||
|
if label == "about":
|
||||||
|
AboutItem.objects.using(database).filter(about_id=record.pk).delete()
|
||||||
|
AboutItem.objects.using(database).bulk_create(
|
||||||
|
[
|
||||||
|
AboutItem(about_id=record.pk, kind=kind, title=title, text=text, position=position)
|
||||||
|
for position, (kind, title, text) in enumerate(ABOUT_ITEM_MOCKS)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
action = "would be updated" if dry_run else "updated"
|
||||||
|
summary = f"{updated} content records {action}; {skipped} admin-edited records skipped."
|
||||||
|
self.stdout.write(f"{prefix}{summary}")
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from django.contrib.admin.models import CHANGE, LogEntry
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.core.management import call_command
|
||||||
|
|
||||||
|
from apps.content.current import AboutContent, HomeContent, ListingContent
|
||||||
|
from apps.users.models import User
|
||||||
|
|
||||||
|
|
||||||
|
pytestmark = [pytest.mark.django_db, pytest.mark.integration]
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_replaces_unedited_migration_copy_with_mock_content_idempotently():
|
||||||
|
call_command("seed_mock_content", stdout=StringIO())
|
||||||
|
|
||||||
|
home = HomeContent.objects.get(pk=1)
|
||||||
|
about = AboutContent.objects.get(pk=1)
|
||||||
|
events = ListingContent.objects.get(pk="events")
|
||||||
|
blog = ListingContent.objects.get(pk="blog")
|
||||||
|
|
||||||
|
assert home.hero_heading == "عنوان نمونه صفحه اصلی"
|
||||||
|
assert about.title == "عنوان نمونه صفحه درباره ما"
|
||||||
|
assert about.items.filter(title="کمیته نمونه").exists()
|
||||||
|
assert not about.items.filter(title="انجمن علمی مهندسی کامپیوتر دانشکدهی فنی").exists()
|
||||||
|
assert events.title == "عنوان نمونه رویدادها"
|
||||||
|
assert blog.title == "عنوان نمونه بلاگ"
|
||||||
|
|
||||||
|
call_command("seed_mock_content", stdout=StringIO())
|
||||||
|
assert HomeContent.objects.get(pk=1).hero_heading == "عنوان نمونه صفحه اصلی"
|
||||||
|
|
||||||
|
|
||||||
|
def test_command_preserves_content_with_an_admin_change_log():
|
||||||
|
editor = User.objects.create_user(
|
||||||
|
username="mock-editor",
|
||||||
|
mobile="09120000001",
|
||||||
|
password="TestPass123!",
|
||||||
|
is_staff=True,
|
||||||
|
)
|
||||||
|
home = HomeContent.objects.get(pk=1)
|
||||||
|
home.hero_heading = "Admin's real heading"
|
||||||
|
home.save(update_fields=["hero_heading"])
|
||||||
|
LogEntry.objects.create(
|
||||||
|
user=editor,
|
||||||
|
content_type=ContentType.objects.get_for_model(HomeContent),
|
||||||
|
object_id="1",
|
||||||
|
object_repr=str(home),
|
||||||
|
action_flag=CHANGE,
|
||||||
|
change_message="Changed through admin",
|
||||||
|
)
|
||||||
|
|
||||||
|
call_command("seed_mock_content", stdout=StringIO())
|
||||||
|
|
||||||
|
assert HomeContent.objects.get(pk=1).hero_heading == "Admin's real heading"
|
||||||
|
assert ListingContent.objects.get(slug="events").title == "عنوان نمونه رویدادها"
|
||||||
|
|
||||||
|
|
||||||
|
def test_dry_run_does_not_change_seeded_content():
|
||||||
|
output = StringIO()
|
||||||
|
|
||||||
|
call_command("seed_mock_content", dry_run=True, stdout=output)
|
||||||
|
|
||||||
|
assert HomeContent.objects.get(pk=1).hero_heading == "انجمن علمی کامپیوتر دانشگاه گیلان"
|
||||||
|
assert "dry run" in output.getvalue().lower()
|
||||||
Reference in New Issue
Block a user