feat: add direct-save CMS and About archive

This commit is contained in:
2026-09-28 20:29:00 +03:30
parent 94b32310af
commit 0f6220f72c
12 changed files with 957 additions and 41 deletions

View File

@@ -1,3 +1,4 @@
from datetime import datetime
from typing import Literal, Optional, Union
from ninja import Schema
@@ -24,6 +25,8 @@ class SiteContentOut(Schema):
contacts: dict[str, str]
address: str
footer_copy: str
enamad_image: Optional[MediaOut] = None
enamad_url: str
seo: SeoOut
@@ -40,16 +43,26 @@ class HomePageOut(PageBaseOut):
hero_body: str
hero_image: Optional[MediaOut] = None
hero_action_label: str
hero_action_url: str
hero_secondary_action_label: str
hero_secondary_action_url: str
featured_events_heading: str
featured_events_intro: str
featured_events_link_label: str
featured_events_url: str
featured_event_action_label: str
featured_posts_heading: str
featured_posts_intro: str
featured_posts_link_label: str
featured_posts_url: str
featured_post_action_label: str
participation_heading: str
participation_body: str
participation_image: Optional[MediaOut] = None
participation_action_label: str
participation_action_url: str
participation_secondary_action_label: str
participation_secondary_action_url: str
class AboutItemOut(Schema):
@@ -59,23 +72,82 @@ class AboutItemOut(Schema):
position: int
class AboutMemberOut(Schema):
name: str
role: str
entry_year: str
bio: str
photo: Optional[MediaOut] = None
position: int
class AboutGalleryImageOut(Schema):
image: MediaOut
caption: str
position: int
class AboutAudienceOut(Schema):
title: str
text: str
icon: str
position: int
class AboutMilestoneOut(Schema):
year_label: str
title: str
description: str
position: int
class AboutSelectedEventOut(Schema):
id: int
title: str
slug: str
start_time: datetime
featured_image: Optional[MediaOut] = None
class AboutYearOut(Schema):
year_label: str
position: int
members: list[AboutMemberOut]
selected_events: list[AboutSelectedEventOut]
class AboutPageOut(PageBaseOut):
slug: Literal["about"]
identity_heading: str
identity_body: str
mission_heading: str
mission_body: str
vision_heading: str
vision_body: str
activities_heading: str
activities_body: str
values_heading: str
values_body: str
committees_heading: str
committees_body: str
affiliations_heading: str
affiliations_body: str
faq_heading: str
contact_heading: str
members_heading: str
members_intro: str
gallery_heading: str
gallery_intro: str
past_events_heading: str
history_heading: str
audience_heading: str
year_events_heading: str
cta_heading: str
cta_body: str
cta_action_label: str
cta_action_url: str
items: list[AboutItemOut]
members: list[AboutMemberOut]
gallery_images: list[AboutGalleryImageOut]
audiences: list[AboutAudienceOut]
milestones: list[AboutMilestoneOut]
years: list[AboutYearOut]
class ListingPageOut(PageBaseOut):

View File

@@ -1,7 +1,9 @@
from django.http import HttpResponse
from django.utils import timezone
from ninja import Router
from ninja.errors import HttpError
from apps.events.models import Event
from apps.content.current import AboutContent, HomeContent, ListingContent, SiteContent
from apps.content.api.schemas import AboutPageOut, HomePageOut, ListingPageOut, PageContentOut, SiteContentOut
@@ -23,6 +25,27 @@ def _seo(request, obj):
}
def _about_member_out(request, member, year_label=None):
return {
"name": member.name,
"role": member.role,
"entry_year": year_label or member.entry_year,
"bio": member.bio,
"photo": _media(request, member.photo, member.photo_alt),
"position": member.position,
}
def _about_event_out(request, event):
return {
"id": event.pk,
"title": event.title,
"slug": event.slug,
"start_time": event.start_time,
"featured_image": _media(request, event.featured_image, f"پوستر {event.title}"),
}
@content_router.get("/site", response=SiteContentOut)
def get_site_content(request, response: HttpResponse):
site = SiteContent.objects.filter(pk=1).first()
@@ -50,6 +73,8 @@ def get_site_content(request, response: HttpResponse):
},
"address": site.address,
"footer_copy": site.footer_copy,
"enamad_image": _media(request, site.enamad_image, site.enamad_image_alt),
"enamad_url": site.enamad_url,
"seo": _seo(request, site),
}
@@ -66,32 +91,105 @@ def get_page_content(request, response: HttpResponse, slug: str):
"hero_heading": page.hero_heading, "hero_body": page.hero_body,
"hero_image": _media(request, page.hero_image, page.hero_image_alt),
"hero_action_label": page.hero_action_label,
"hero_action_url": page.hero_action_url,
"hero_secondary_action_label": page.hero_secondary_action_label,
"hero_secondary_action_url": page.hero_secondary_action_url,
"featured_events_heading": page.featured_events_heading,
"featured_events_intro": page.featured_events_intro,
"featured_events_link_label": page.featured_events_link_label,
"featured_events_url": page.featured_events_url,
"featured_event_action_label": page.featured_event_action_label,
"featured_posts_heading": page.featured_posts_heading,
"featured_posts_intro": page.featured_posts_intro,
"featured_posts_link_label": page.featured_posts_link_label,
"featured_posts_url": page.featured_posts_url,
"featured_post_action_label": page.featured_post_action_label,
"participation_heading": page.participation_heading,
"participation_body": page.participation_body,
"participation_image": _media(request, page.participation_image, page.participation_image_alt),
"participation_action_label": page.participation_action_label,
"participation_action_url": page.participation_action_url,
"participation_secondary_action_label": page.participation_secondary_action_label,
"participation_secondary_action_url": page.participation_secondary_action_url,
}
if slug == "about":
page = AboutContent.objects.filter(pk=1).first()
if page is None:
raise HttpError(404, "Page content not found")
event_statuses = (Event.StatusChoices.PUBLISHED, Event.StatusChoices.COMPLETED)
years_by_label = {}
for year in page.years.prefetch_related("members", "events").all():
label = year.year_label.strip() or "سال نامشخص"
events = year.events.filter(
is_deleted=False,
status__in=event_statuses,
start_time__lte=timezone.now(),
).order_by("-start_time", "-pk")
years_by_label[label] = {
"year_label": label,
"position": year.position,
"_pk": year.pk,
"members": [
_about_member_out(request, member, label)
for member in year.members.all()
],
"selected_events": [_about_event_out(request, event) for event in events],
}
legacy_members = page.members.filter(year_group__isnull=True).order_by("entry_year", "position", "pk")
for member in legacy_members:
label = member.entry_year.strip() or "سال نامشخص"
year_data = years_by_label.setdefault(
label,
{
"year_label": label,
"position": 0,
"_pk": 0,
"members": [],
"selected_events": [],
},
)
year_data["members"].append(_about_member_out(request, member, label))
years = sorted(
years_by_label.values(),
key=lambda year: (year["position"], year["_pk"], year["year_label"]),
)
all_members = [member for year in years for member in year["members"]]
return {
"slug": "about", "title": page.title, "intro": page.intro, "seo": _seo(request, page),
"identity_heading": page.identity_heading, "identity_body": page.identity_body,
"mission_heading": page.mission_heading, "mission_body": page.mission_body,
"vision_heading": page.vision_heading, "vision_body": page.vision_body,
"activities_heading": page.activities_heading, "activities_body": page.activities_body,
"values_heading": page.values_heading, "values_body": page.values_body,
"committees_heading": page.committees_heading, "committees_body": page.committees_body,
"affiliations_heading": page.affiliations_heading, "affiliations_body": page.affiliations_body,
"faq_heading": page.faq_heading, "contact_heading": page.contact_heading,
"items": list(page.items.values("kind", "title", "text", "position")),
"members_heading": page.members_heading, "members_intro": page.members_intro,
"gallery_heading": page.gallery_heading, "gallery_intro": page.gallery_intro,
"past_events_heading": page.past_events_heading,
"history_heading": page.history_heading,
"audience_heading": page.audience_heading,
"year_events_heading": page.year_events_heading,
"cta_heading": page.cta_heading,
"cta_body": page.cta_body,
"cta_action_label": page.cta_action_label,
"cta_action_url": page.cta_action_url,
"items": list(page.items.exclude(kind__in=("committees", "affiliations")).values("kind", "title", "text", "position")),
"members": all_members,
"gallery_images": [
{
"image": _media(request, item.image, item.image_alt),
"caption": item.caption, "position": item.position,
}
for item in page.gallery_images.all()
],
"audiences": list(page.audiences.values("title", "text", "icon", "position")),
"milestones": list(page.milestones.values("year_label", "title", "description", "position")),
"years": [
{key: value for key, value in year.items() if not key.startswith("_")}
for year in years
],
}
if slug in ("events", "blog"):