Replace content publishing workflow with direct Unfold editing
This commit is contained in:
208
apps/content/migrations/0004_direct_edit_content.py
Normal file
208
apps/content/migrations/0004_direct_edit_content.py
Normal file
@@ -0,0 +1,208 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
from django.db.models import Q
|
||||
import django.db.models.deletion
|
||||
|
||||
import apps.content.current
|
||||
|
||||
|
||||
def seo_fields():
|
||||
return [
|
||||
("seo_title", models.CharField(blank=True, max_length=120, verbose_name="SEO title")),
|
||||
("seo_description", models.CharField(blank=True, max_length=300, verbose_name="SEO description")),
|
||||
("seo_image", models.ImageField(blank=True, upload_to=apps.content.current.content_upload_path, verbose_name="Social sharing image")),
|
||||
("seo_image_alt", models.CharField(blank=True, max_length=180, verbose_name="Social image description")),
|
||||
]
|
||||
|
||||
|
||||
def single_id():
|
||||
return [("id", models.PositiveSmallIntegerField(default=1, editable=False, primary_key=True, serialize=False))]
|
||||
|
||||
|
||||
def strings(*names):
|
||||
return [(name, models.CharField(blank=True, max_length=length)) for name, length in names]
|
||||
|
||||
|
||||
def image(name):
|
||||
return (name, models.ImageField(blank=True, upload_to=apps.content.current.content_upload_path))
|
||||
|
||||
|
||||
def copy_image(target, name, asset_id, alt, assets):
|
||||
asset = assets.get(asset_id)
|
||||
if asset:
|
||||
setattr(target, name, asset.file.name)
|
||||
setattr(target, f"{name}_alt", alt or asset.alt)
|
||||
|
||||
|
||||
def populate_current_content(apps, schema_editor):
|
||||
db = schema_editor.connection.alias
|
||||
SiteContent = apps.get_model("content", "SiteContent")
|
||||
HomeContent = apps.get_model("content", "HomeContent")
|
||||
AboutContent = apps.get_model("content", "AboutContent")
|
||||
AboutItem = apps.get_model("content", "AboutItem")
|
||||
ListingContent = apps.get_model("content", "ListingContent")
|
||||
SiteIdentity = apps.get_model("content", "SiteIdentity")
|
||||
EditorialPage = apps.get_model("content", "EditorialPage")
|
||||
MediaAsset = apps.get_model("content", "MediaAsset")
|
||||
assets = {asset.pk: asset for asset in MediaAsset.objects.using(db).all()}
|
||||
|
||||
site = SiteContent(id=1)
|
||||
identity = SiteIdentity.objects.using(db).filter(pk=1).first()
|
||||
revision = identity.published_revision if identity and identity.published_revision_id else None
|
||||
if revision:
|
||||
for field in ("official_name", "short_name", "faculty_subline", "description", "footer_copy", "seo_title", "seo_description"):
|
||||
setattr(site, field, getattr(revision, field))
|
||||
copy_image(site, "logo", revision.logo_id, revision.logo_alt, assets)
|
||||
copy_image(site, "seo_image", revision.seo_image_id, "Social sharing image", assets)
|
||||
for nav in revision.navigation:
|
||||
if nav.get("route_id") in ("home", "about", "events", "blog"):
|
||||
setattr(site, f"nav_{nav['route_id']}", nav.get("label", ""))
|
||||
for contact in revision.contacts:
|
||||
kind, url = contact.get("kind"), contact.get("url", "")
|
||||
if kind == "email":
|
||||
site.email = url.removeprefix("mailto:")
|
||||
elif kind == "phone":
|
||||
site.phone = url.removeprefix("tel:")
|
||||
elif kind in ("telegram", "instagram", "website"):
|
||||
setattr(site, f"{kind}_url", url)
|
||||
site.save(using=db)
|
||||
|
||||
pages = {page.slug: page for page in EditorialPage.objects.using(db).all()}
|
||||
for slug, Model in (("home", HomeContent), ("about", AboutContent), ("events", ListingContent), ("blog", ListingContent)):
|
||||
page = pages.get(slug)
|
||||
revision = page.published_revision if page and page.published_revision_id else None
|
||||
record = Model(id=1) if slug in ("home", "about") else Model(slug=slug)
|
||||
if revision:
|
||||
for field in ("title", "intro", "seo_title", "seo_description"):
|
||||
setattr(record, field, getattr(revision, field))
|
||||
copy_image(record, "seo_image", revision.seo_image_id, "Social sharing image", assets)
|
||||
if slug in ("home", "about"):
|
||||
sections = list(revision.sections.using(db).all())
|
||||
for section in sections:
|
||||
prefix = section.key
|
||||
if slug == "home":
|
||||
if prefix in ("hero", "participation"):
|
||||
setattr(record, f"{prefix}_heading", section.heading)
|
||||
setattr(record, f"{prefix}_body", section.body)
|
||||
setattr(record, f"{prefix}_action_label", (section.primary_action or {}).get("label", ""))
|
||||
copy_image(record, f"{prefix}_image", section.image_id, section.image_alt, assets)
|
||||
elif prefix in ("featured_events", "featured_posts"):
|
||||
setattr(record, f"{prefix}_heading", section.heading)
|
||||
setattr(record, f"{prefix}_intro", section.intro)
|
||||
setattr(record, f"{prefix}_link_label", section.all_link_label)
|
||||
elif prefix in ("identity", "mission", "activities", "values", "committees", "affiliations", "faq", "contact"):
|
||||
setattr(record, f"{prefix}_heading", section.heading)
|
||||
if hasattr(record, f"{prefix}_body"):
|
||||
setattr(record, f"{prefix}_body", section.body)
|
||||
record.save(using=db)
|
||||
if revision and slug == "about":
|
||||
for section in sections:
|
||||
if section.key in ("mission", "activities", "values", "committees", "affiliations", "faq"):
|
||||
for position, entry in enumerate(section.entries):
|
||||
AboutItem.objects.using(db).create(about_id=1, kind=section.key, title=entry["title"], text=entry["text"], position=position)
|
||||
|
||||
|
||||
def preserve_editor_access(apps, schema_editor):
|
||||
db = schema_editor.connection.alias
|
||||
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||
Permission = apps.get_model("auth", "Permission")
|
||||
Group = apps.get_model("auth", "Group")
|
||||
User = apps.get_model(*settings.AUTH_USER_MODEL.split("."))
|
||||
models_to_change = ("sitecontent", "homecontent", "aboutcontent", "aboutitem", "listingcontent")
|
||||
new_permissions = []
|
||||
for model in models_to_change:
|
||||
content_type, _ = ContentType.objects.using(db).get_or_create(app_label="content", model=model)
|
||||
permission, _ = Permission.objects.using(db).get_or_create(
|
||||
content_type=content_type, codename=f"change_{model}", defaults={"name": f"Can change {model}"}
|
||||
)
|
||||
new_permissions.append(permission)
|
||||
for group in Group.objects.using(db).filter(name__in=("content_editor", "content_publisher")):
|
||||
group.permissions.add(*new_permissions)
|
||||
old_permissions = Permission.objects.using(db).filter(
|
||||
content_type__app_label="content", codename__in=("change_content_draft", "publish_content")
|
||||
)
|
||||
for user in User.objects.using(db).filter(user_permissions__in=old_permissions).distinct():
|
||||
user.user_permissions.add(*new_permissions)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("content", "0003_content_groups"),
|
||||
("auth", "0012_alter_user_first_name_max_length"),
|
||||
("contenttypes", "0002_remove_content_type_name"),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="SiteContent",
|
||||
fields=single_id() + seo_fields() + strings(
|
||||
("official_name", 180), ("short_name", 80), ("faculty_subline", 140),
|
||||
("logo_alt", 180), ("phone", 30), ("footer_copy", 300)
|
||||
) + [
|
||||
("description", models.TextField(blank=True)), image("logo"),
|
||||
("nav_home", models.CharField(default="Home", max_length=80, verbose_name="Home navigation label")),
|
||||
("nav_about", models.CharField(default="About", max_length=80, verbose_name="About navigation label")),
|
||||
("nav_events", models.CharField(default="Events", max_length=80, verbose_name="Events navigation label")),
|
||||
("nav_blog", models.CharField(default="Blog", max_length=80, verbose_name="Blog navigation label")),
|
||||
("email", models.EmailField(blank=True, max_length=254)),
|
||||
("telegram_url", models.URLField(blank=True)),
|
||||
("instagram_url", models.URLField(blank=True)),
|
||||
("website_url", models.URLField(blank=True)),
|
||||
("address", models.TextField(blank=True)),
|
||||
],
|
||||
options={"verbose_name": "Site content"},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="HomeContent",
|
||||
fields=single_id() + seo_fields() + strings(
|
||||
("title", 180), ("hero_heading", 120), ("hero_image_alt", 180),
|
||||
("hero_action_label", 40), ("featured_events_heading", 120),
|
||||
("featured_events_intro", 300), ("featured_events_link_label", 40),
|
||||
("featured_posts_heading", 120), ("featured_posts_intro", 300),
|
||||
("featured_posts_link_label", 40), ("participation_heading", 120),
|
||||
("participation_image_alt", 180), ("participation_action_label", 40)
|
||||
) + [
|
||||
("intro", models.TextField(blank=True)), ("hero_body", models.TextField(blank=True)),
|
||||
image("hero_image"), ("participation_body", models.TextField(blank=True)),
|
||||
image("participation_image"),
|
||||
],
|
||||
options={"verbose_name": "Home page content"},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AboutContent",
|
||||
fields=single_id() + seo_fields() + strings(
|
||||
("title", 180), ("identity_heading", 120), ("mission_heading", 120),
|
||||
("activities_heading", 120), ("values_heading", 120),
|
||||
("committees_heading", 120), ("affiliations_heading", 120),
|
||||
("contact_heading", 120)
|
||||
) + [
|
||||
("faq_heading", models.CharField(blank=True, max_length=120, verbose_name="FAQ heading")),
|
||||
("intro", models.TextField(blank=True)),
|
||||
*[(f"{name}_body", models.TextField(blank=True)) for name in ("identity", "mission", "activities", "values", "committees", "affiliations")],
|
||||
],
|
||||
options={"verbose_name": "About page content"},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="ListingContent",
|
||||
fields=[("slug", models.CharField(choices=[("events", "Events"), ("blog", "Blog")], max_length=10, primary_key=True, serialize=False))]
|
||||
+ seo_fields() + strings(("title", 180)) + [("intro", models.TextField(blank=True))],
|
||||
options={"verbose_name": "Listing page content", "verbose_name_plural": "Listing page content"},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="AboutItem",
|
||||
fields=[
|
||||
("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
|
||||
("kind", models.CharField(choices=[(key, key.title()) for key in ("mission", "activities", "values", "committees", "affiliations", "faq")], max_length=20)),
|
||||
("title", models.CharField(max_length=120)), ("text", models.TextField()),
|
||||
("position", models.PositiveSmallIntegerField(default=0)),
|
||||
("about", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="items", to="content.aboutcontent")),
|
||||
],
|
||||
options={"verbose_name": "About page item", "ordering": ["kind", "position", "pk"]},
|
||||
),
|
||||
migrations.AddConstraint(model_name="sitecontent", constraint=models.CheckConstraint(condition=Q(id=1), name="content_current_single_site")),
|
||||
migrations.AddConstraint(model_name="homecontent", constraint=models.CheckConstraint(condition=Q(id=1), name="content_current_single_home")),
|
||||
migrations.AddConstraint(model_name="aboutcontent", constraint=models.CheckConstraint(condition=Q(id=1), name="content_current_single_about")),
|
||||
migrations.RunPython(populate_current_content, migrations.RunPython.noop),
|
||||
migrations.RunPython(preserve_editor_access, migrations.RunPython.noop),
|
||||
]
|
||||
136
apps/content/migrations/0005_seed_current_website_copy.py
Normal file
136
apps/content/migrations/0005_seed_current_website_copy.py
Normal file
@@ -0,0 +1,136 @@
|
||||
"""Copy current static website copy into untouched direct-edit content records.
|
||||
|
||||
Sources: frontend src/views/{Home,AboutUs,Events,Blog}.tsx and
|
||||
src/app/{page,about/page,events/page,blog/page,layout}.tsx as of 2026-09-27.
|
||||
The migration deliberately leaves existing CMS edits and uploaded images alone.
|
||||
"""
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
HOME = {
|
||||
"title": "انجمن علمی کامپیوتر دانشگاه گیلان",
|
||||
"intro": "با ما همراه شوید و در دنیای مهندسی/علوم کامپیوتر و فناوری پیشرفت کنید. رویدادها، محتوای آموزشی و جامعهای پویا برای رشد شما فراهم است.",
|
||||
"hero_heading": "انجمن علمی کامپیوتر دانشگاه گیلان",
|
||||
"hero_body": "با ما همراه شوید و در دنیای مهندسی/علوم کامپیوتر و فناوری پیشرفت کنید. رویدادها، محتوای آموزشی و جامعهای پویا برای رشد شما فراهم است.",
|
||||
"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_action_label": "ثبتنام کنید",
|
||||
"seo_title": "انجمن علمی کامپیوتر دانشگاه گیلان",
|
||||
"seo_description": "با ما همراه شوید و در دنیای مهندسی و علوم کامپیوتر پیشرفت کنید. رویدادها، محتوای آموزشی و جامعهای پویا برای رشد شما فراهم است.",
|
||||
}
|
||||
|
||||
ABOUT = {
|
||||
"title": "انجمنهای علمی کامپیوتر گیلان",
|
||||
"intro": "پیوند دانشگاه و صنعت با برگزاری رویدادها، کارگاهها و نشستهای تخصصی — با محوریت رشد مهارتهای فنی و مسیرهای شغلی.",
|
||||
"identity_heading": "انجمنهای علمی کامپیوتر گیلان",
|
||||
"identity_body": "پیوند دانشگاه و صنعت با برگزاری رویدادها، کارگاهها و نشستهای تخصصی — با محوریت رشد مهارتهای فنی و مسیرهای شغلی.",
|
||||
"mission_heading": "ماموریت ما",
|
||||
"mission_body": "توانمندسازی دانشجویان با مهارتهای موردنیاز صنعت، ایجاد شبکههای حرفهای و فراهمکردن مسیرهای یادگیری پروژهمحور؛ از معرفی نقشها و مسیرهای شغلی تا تمرین مهارتهای نرم و فنی.",
|
||||
"values_heading": "ارزشهای ما",
|
||||
"values_body": "یادگیری پیوسته و اشتراک دانش\nکیفیت، شفافیت و مسئولیتپذیری\nفرصت برابر برای مشارکت\nارتباط مؤثر با صنعت و جامعه",
|
||||
"committees_heading": "کمیتهها و گروههای کاری",
|
||||
"affiliations_heading": "انجمنها",
|
||||
"affiliations_body": "اطلاعات انجمنهای زیرمجموعه/همکار",
|
||||
"faq_heading": "پرسشهای متداول",
|
||||
"contact_heading": "راههای ارتباطی",
|
||||
"seo_title": "درباره ما | انجمن علمی کامپیوتر شرق گیلان",
|
||||
"seo_description": "آشنایی با تاریخچه، مأموریتها و دستاوردهای انجمن علمی کامپیوتر شرق گیلان و راههای مشارکت دانشجویان در برنامههای انجمن.",
|
||||
}
|
||||
|
||||
EVENTS = {
|
||||
"title": "رویدادها",
|
||||
"intro": "رویدادهای جاری و گذشته انجمن علمی کامپیوتر شرق گیلان، شامل کارگاهها، مسابقات و برنامههای آموزشی.",
|
||||
"seo_title": "رویدادها",
|
||||
"seo_description": "رویدادهای جاری و گذشته انجمن علمی کامپیوتر شرق گیلان، شامل کارگاهها، مسابقات و برنامههای آموزشی.",
|
||||
}
|
||||
|
||||
BLOG = {
|
||||
"title": "بلاگ",
|
||||
"intro": "نوشتههای آموزشی، تجربههای دانشجویی و یادداشتهای تخصصی اعضای انجمن علمی.",
|
||||
"seo_title": "وبلاگ",
|
||||
"seo_description": "مقالات، نوشتهها و محتوای آموزشی انجمن علمی کامپیوتر شرق گیلان.",
|
||||
}
|
||||
|
||||
SITE = {
|
||||
"official_name": "انجمن علمی مهندسی کامپیوتر دانشگاه گیلان (دانشکدهی فنی و مهندسی شرق گیلان)",
|
||||
"short_name": "انجمن علمی مهندسی کامپیوتر",
|
||||
"faculty_subline": "دانشکدهی فنی و مهندسی شرق گیلان",
|
||||
"description": "ترویج علم کامپیوتر، برگزاری رویدادهای تخصصی، تقویت شبکهی دانشجویی و پیوند با صنعت.",
|
||||
"nav_home": "خانه",
|
||||
"nav_about": "درباره ما",
|
||||
"nav_events": "رویدادها",
|
||||
"nav_blog": "بلاگ",
|
||||
"email": "eastguilanceassociation@gmail.com",
|
||||
"phone": "۰۱۳-۴۲۶۸۸۴۴۷",
|
||||
"telegram_url": "https://t.me/guilancea",
|
||||
"instagram_url": "https://instagram.com/guilance.ir",
|
||||
"website_url": "https://east-guilan-ce.ir",
|
||||
"address": "گیلان، رودسر، واجارگاه، دانشکدهی فنی و مهندسی شرق گیلان\nکدپستی: ۴۴۹۱۸۹۸۵۶۶",
|
||||
"footer_copy": "ترویج علم کامپیوتر، برگزاری رویدادهای تخصصی، تقویت شبکهی دانشجویی و پیوند با صنعت.",
|
||||
"seo_title": "انجمن علمی کامپیوتر شرق گیلان",
|
||||
"seo_description": "انجمن علمی کامپیوتر شرق گیلان، برگزارکننده رویدادها، کارگاهها و محتوای آموزشی برای دانشجویان و علاقهمندان فناوری.",
|
||||
}
|
||||
|
||||
# Each pair is (kind, title, text). These are the visible About page items.
|
||||
ABOUT_ITEMS = (
|
||||
("committees", "کمیته آموزش", "برنامهریزی و اجرای دورهها و کارگاههای مهارتی، هماهنگی با مدرسین و طراحی مسیرهای یادگیری."),
|
||||
("committees", "کمیته صنعت و اشتغال", "تعامل با شرکتها، دعوت از متخصصان صنعت، و شبکهسازی برای فرصتهای کارآموزی و استخدام."),
|
||||
("committees", "کمیته محتوای دیجیتال", "تولید محتوای آموزشی، خبرنامه، مدیریت شبکههای اجتماعی و پوشش رسانهای رویدادها."),
|
||||
("affiliations", "انجمن علمی مهندسی کامپیوتر دانشکدهی فنی شرق", "دانشگاه گیلان، رودسر؛ تأسیس: ۱۳۹۲. برگزاری رویدادهای آموزشی و صنعتی، منتورینگ دانشجویی، و اتصال دانشجویان به فرصتهای شغلی و پژوهشی. حوزهها: مهندسی نرمافزار، هوش مصنوعی و داده، طراحی محصول، DevOps/امنیت."),
|
||||
("affiliations", "انجمن علمی مهندسی کامپیوتر دانشکدهی فنی", "دانشگاه گیلان، رشت؛ تأسیس: ۱۳۵۳. برگزاری رویدادهای آموزشی و صنعتی، منتورینگ دانشجویی، و اتصال دانشجویان به فرصتهای شغلی و پژوهشی. حوزهها: مهندسی نرمافزار، هوش مصنوعی و داده، DevOps."),
|
||||
("affiliations", "انجمن علمی علوم کامپیوتر دانشکدهی علومپایه", "دانشگاه گیلان، رشت؛ تأسیس: ۱۳۵۳. برگزاری رویدادهای آموزشی و صنعتی، منتورینگ دانشجویی، و اتصال دانشجویان به فرصتهای شغلی و پژوهشی. حوزهها: امنیت، سیستمعامل، سختافزار."),
|
||||
("faq", "چطور در رویدادها شرکت کنم؟", "برای ثبتنام در رویدادها ابتدا باید حسابکاربری با کددانشجویی خود ایجاد کنید و سپس از طریق صفحهی ثبتنام در رویداد موردنظرتان شرکت کنید :)"),
|
||||
("faq", "آیا امکان همکاری انجمنها/شرکتها هم وجود دارد؟", "بله، ما همیشه آمادهی همکاری با سازمانها و انجمنهای مختلف دانشگاهها جهت توانمندسازی دانشجویان با مهارتهای موردنیاز صنعت و ایجاد شبکههای حرفهای هستیم."),
|
||||
("faq", "برای سخنرانی/منتورینگ به چه چیزهایی نیاز است؟", "رزومه کوتاه، موضوع پیشنهادی و زمانهای دسترسپذیرتان را ارسال کنید تا هماهنگ کنیم."),
|
||||
)
|
||||
|
||||
|
||||
def seed_if_untouched(record, values, marker_fields, db):
|
||||
if any(getattr(record, name) for name in marker_fields):
|
||||
return False
|
||||
for name, value in values.items():
|
||||
setattr(record, name, value)
|
||||
record.save(using=db)
|
||||
return True
|
||||
|
||||
|
||||
def seed_current_copy(apps, schema_editor):
|
||||
db = schema_editor.connection.alias
|
||||
SiteContent = apps.get_model("content", "SiteContent")
|
||||
HomeContent = apps.get_model("content", "HomeContent")
|
||||
AboutContent = apps.get_model("content", "AboutContent")
|
||||
AboutItem = apps.get_model("content", "AboutItem")
|
||||
ListingContent = apps.get_model("content", "ListingContent")
|
||||
|
||||
site, _ = SiteContent.objects.using(db).get_or_create(pk=1)
|
||||
seed_if_untouched(site, SITE, ("official_name", "short_name", "email", "seo_title"), db)
|
||||
|
||||
home, _ = HomeContent.objects.using(db).get_or_create(pk=1)
|
||||
seed_if_untouched(home, HOME, ("title", "hero_heading", "seo_title"), db)
|
||||
|
||||
about, _ = AboutContent.objects.using(db).get_or_create(pk=1)
|
||||
seeded_about = seed_if_untouched(about, ABOUT, ("title", "identity_heading", "seo_title"), db)
|
||||
if seeded_about and not AboutItem.objects.using(db).filter(about_id=1).exists():
|
||||
positions = {}
|
||||
for kind, title, item_text in ABOUT_ITEMS:
|
||||
position = positions.get(kind, 0)
|
||||
AboutItem.objects.using(db).create(about_id=1, kind=kind, title=title, text=item_text, position=position)
|
||||
positions[kind] = position + 1
|
||||
|
||||
for slug, values in (("events", EVENTS), ("blog", BLOG)):
|
||||
listing, _ = ListingContent.objects.using(db).get_or_create(slug=slug)
|
||||
seed_if_untouched(listing, values, ("title", "intro", "seo_title"), db)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [("content", "0004_direct_edit_content")]
|
||||
|
||||
operations = [migrations.RunPython(seed_current_copy, migrations.RunPython.noop)]
|
||||
Reference in New Issue
Block a user