Replace content publishing workflow with direct Unfold editing

This commit is contained in:
2026-09-27 11:09:25 +03:30
parent 148b7038a9
commit f6b4ef7dd8
14 changed files with 680 additions and 1232 deletions

View 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),
]