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,7 +1,21 @@
from django.contrib import admin
from django.utils import timezone
from unfold.admin import ModelAdmin, TabularInline
from .current import AboutContent, AboutItem, HomeContent, ListingContent, SiteContent
from apps.events.models import Event
from .current import (
AboutAudience,
AboutContent,
AboutGalleryImage,
AboutItem,
AboutMember,
AboutMilestone,
AboutYear,
HomeContent,
ListingContent,
SiteContent,
)
class FixedContentAdmin(ModelAdmin):
@@ -22,6 +36,7 @@ class SiteContentAdmin(FixedContentAdmin):
("Association", {"fields": ("official_name", "short_name", "faculty_subline", "description", "logo", "logo_alt")}),
("Navigation labels", {"fields": ("nav_home", "nav_about", "nav_events", "nav_blog")}),
("Contact and footer", {"fields": ("email", "phone", "telegram_url", "instagram_url", "website_url", "address", "footer_copy")}),
("eNAMAD certificate", {"fields": ("enamad_image", "enamad_image_alt", "enamad_url"), "description": "The badge stays unlinked until you add its verification URL."}),
("SEO defaults", {"fields": ("seo_title", "seo_description", "seo_image", "seo_image_alt")}),
)
@@ -30,10 +45,21 @@ class SiteContentAdmin(FixedContentAdmin):
class HomeContentAdmin(FixedContentAdmin):
fieldsets = (
("Page introduction", {"fields": ("title", "intro")}),
("Hero", {"fields": ("hero_heading", "hero_body", "hero_image", "hero_image_alt", "hero_action_label")}),
("Featured events", {"fields": ("featured_events_heading", "featured_events_intro", "featured_events_link_label")}),
("Featured blog posts", {"fields": ("featured_posts_heading", "featured_posts_intro", "featured_posts_link_label")}),
("Participation", {"fields": ("participation_heading", "participation_body", "participation_image", "participation_image_alt", "participation_action_label")}),
("Hero", {"fields": (
"hero_heading", "hero_body", "hero_image", "hero_image_alt",
"hero_action_label", "hero_action_url", "hero_secondary_action_label", "hero_secondary_action_url",
), "description": "Destinations accept an internal path such as /events or a secure https URL."}),
("Featured events", {"fields": (
"featured_events_heading", "featured_events_intro", "featured_events_link_label", "featured_events_url", "featured_event_action_label",
), "description": "Destinations accept an internal path such as /events or a secure https URL."}),
("Featured blog posts", {"fields": (
"featured_posts_heading", "featured_posts_intro", "featured_posts_link_label", "featured_posts_url", "featured_post_action_label",
), "description": "Destinations accept an internal path such as /blog or a secure https URL."}),
("Participation", {"fields": (
"participation_heading", "participation_body", "participation_image", "participation_image_alt",
"participation_action_label", "participation_action_url",
"participation_secondary_action_label", "participation_secondary_action_url",
), "description": "Destinations accept an internal path such as /auth or a secure https URL."}),
("SEO", {"fields": ("seo_title", "seo_description", "seo_image", "seo_image_alt")}),
)
@@ -43,6 +69,100 @@ class AboutItemInline(TabularInline):
extra = 0
fields = ("kind", "title", "text", "position")
def get_queryset(self, request):
return super().get_queryset(request).filter(kind="faq")
def formfield_for_choice_field(self, db_field, request, **kwargs):
field = super().formfield_for_choice_field(db_field, request, **kwargs)
if db_field.name == "kind":
field.choices = tuple(choice for choice in field.choices if choice[0] == "faq")
return field
def has_add_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
def has_change_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
def has_delete_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
class AboutAudienceInline(TabularInline):
model = AboutAudience
extra = 0
fields = ("title", "text", "icon", "position")
ordering = ("position",)
def has_add_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
def has_change_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
def has_delete_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
class AboutMilestoneInline(TabularInline):
model = AboutMilestone
extra = 0
fields = ("year_label", "title", "description", "position")
ordering = ("position",)
def has_add_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
def has_change_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
def has_delete_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
class AboutMemberInline(TabularInline):
model = AboutMember
fk_name = "year_group"
extra = 0
fields = ("name", "role", "bio", "photo", "photo_alt", "position")
ordering = ("position",)
def has_add_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
def has_change_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
def has_delete_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
class LegacyAboutMemberInline(TabularInline):
model = AboutMember
fk_name = "about"
extra = 0
fields = ("name", "role", "entry_year", "bio", "photo", "photo_alt", "position")
ordering = ("entry_year", "position")
def get_queryset(self, request):
return super().get_queryset(request).filter(year_group__isnull=True)
def has_add_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
def has_change_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
def has_delete_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
class AboutGalleryInline(TabularInline):
model = AboutGalleryImage
extra = 0
fields = ("image", "image_alt", "caption", "position")
ordering = ("position",)
def has_add_permission(self, request, obj=None):
return request.user.has_perm("content.change_aboutcontent")
@@ -55,19 +175,76 @@ class AboutItemInline(TabularInline):
@admin.register(AboutContent)
class AboutContentAdmin(FixedContentAdmin):
inlines = (AboutItemInline,)
inlines = (
AboutItemInline,
AboutAudienceInline,
AboutMilestoneInline,
LegacyAboutMemberInline,
AboutGalleryInline,
)
fieldsets = (
("Page introduction", {"fields": ("title", "intro", "identity_heading", "identity_body")}),
("Mission", {"fields": ("mission_heading", "mission_body")}),
("Mission, vision, and values", {"fields": (
"mission_heading", "mission_body", "vision_heading", "vision_body", "values_heading", "values_body",
)}),
("Activities", {"fields": ("activities_heading", "activities_body")}),
("Values", {"fields": ("values_heading", "values_body")}),
("Committees", {"fields": ("committees_heading", "committees_body")}),
("Affiliations", {"fields": ("affiliations_heading", "affiliations_body")}),
("FAQ and contact", {"fields": ("faq_heading", "contact_heading")}),
("Association history", {"fields": ("history_heading",)}),
("Who the association serves", {"fields": ("audience_heading",)}),
("Annual archive", {"fields": ("members_heading", "members_intro", "year_events_heading")}),
("Memories gallery", {"fields": ("gallery_heading", "gallery_intro")}),
("FAQ and call to action", {"fields": (
"faq_heading", "cta_heading", "cta_body", "cta_action_label", "cta_action_url",
), "description": "The CTA destination stays empty until you set an internal path or secure https URL."}),
("SEO", {"fields": ("seo_title", "seo_description", "seo_image", "seo_image_alt")}),
)
@admin.register(AboutYear)
class AboutYearAdmin(ModelAdmin):
list_display = ("year_label", "position")
list_editable = ("position",)
ordering = ("position", "pk")
search_fields = ("year_label",)
fieldsets = (
("Academic year", {"fields": ("year_label", "position", "events")}),
)
filter_horizontal = ("events",)
inlines = (AboutMemberInline,)
def has_module_permission(self, request):
return request.user.has_perm("content.change_aboutcontent")
def has_view_permission(self, request, obj=None):
return self.has_module_permission(request)
def has_add_permission(self, request):
return self.has_module_permission(request)
def has_change_permission(self, request, obj=None):
return self.has_module_permission(request)
def has_delete_permission(self, request, obj=None):
return self.has_module_permission(request)
def get_queryset(self, request):
return super().get_queryset(request).filter(about_id=1)
def save_model(self, request, obj, form, change):
if not obj.about_id:
obj.about = AboutContent.objects.get(pk=1)
super().save_model(request, obj, form, change)
def formfield_for_manytomany(self, db_field, request, **kwargs):
field = super().formfield_for_manytomany(db_field, request, **kwargs)
if db_field.name == "events":
field.queryset = Event.objects.filter(
is_deleted=False,
status__in=(Event.StatusChoices.PUBLISHED, Event.StatusChoices.COMPLETED),
start_time__lte=timezone.now(),
).order_by("-start_time", "title")
return field
@admin.register(ListingContent)
class ListingContentAdmin(FixedContentAdmin):
list_display = ("slug", "title")