feat: rebuild site content management

Replace legacy FAQ, downloads, and release flows with project and blog publishing. Add configurable branding and theme fields plus clean-start Compose seeding support.
This commit is contained in:
mohamad
2026-09-24 02:32:27 +03:30
parent 4fc4282f0f
commit 588d95f889
43 changed files with 703 additions and 1193 deletions
+65 -9
View File
@@ -1,5 +1,7 @@
from django.core.exceptions import ValidationError
from django.db import models
from django.urls import reverse
from django.utils import timezone
from django.utils.text import slugify
from apps.core.utils import CONTENT_FORMAT_CHOICES, FORMAT_MARKDOWN, FORMAT_PLAIN, render_content
@@ -13,8 +15,10 @@ RESERVED_PAGE_SLUGS = frozenset({
"admin",
"about",
"contact",
"blog",
"faq",
"home",
"projects",
"products",
"static",
"media",
@@ -55,8 +59,8 @@ class HomepageSection(VideoBlockMixin, models.Model):
TYPE_CHOICES = [
(TYPE_FEATURES, "Features"),
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
(TYPE_PRODUCTS, "Products"),
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
(TYPE_PRODUCTS, "Projects"),
(TYPE_PRODUCTS_CATALOG, "Projects with sub-projects"),
(TYPE_PROBLEMS, "Problems / Value Proposition"),
(TYPE_ABOUT_STRIP, "About Strip"),
(TYPE_SUPPORTERS, "Supporters"),
@@ -310,6 +314,62 @@ class CustomPage(models.Model):
super().save(*args, **kwargs)
class BlogPost(models.Model):
STATUS_DRAFT = "draft"
STATUS_PUBLISHED = "published"
STATUS_CHOICES = [
(STATUS_DRAFT, "Draft"),
(STATUS_PUBLISHED, "Published"),
]
title = models.CharField(max_length=240)
slug = models.SlugField(max_length=240, unique=True, blank=True)
excerpt = models.CharField(
max_length=400,
blank=True,
help_text="Short summary shown on the blog listing and in search previews.",
)
content = models.TextField()
content_format = models.CharField(
max_length=20,
choices=CONTENT_FORMAT_CHOICES,
default=FORMAT_MARKDOWN,
)
cover_image = models.ImageField(upload_to="blog/", blank=True, null=True)
cover_image_alt = models.CharField(max_length=240, blank=True)
author_name = models.CharField(max_length=120, blank=True)
status = models.CharField(
max_length=20, choices=STATUS_CHOICES, default=STATUS_DRAFT
)
published_at = models.DateTimeField(default=timezone.now)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ["-published_at", "-pk"]
verbose_name = "Blog post"
verbose_name_plural = "Blog posts"
def __str__(self):
return self.title
@property
def rendered_content(self):
return render_content(self.content, self.content_format)
@property
def is_public(self):
return self.status == self.STATUS_PUBLISHED and self.published_at <= timezone.now()
def save(self, *args, **kwargs):
if not self.slug:
self.slug = slugify(self.title)
super().save(*args, **kwargs)
def get_absolute_url(self):
return reverse("pages:blog_detail", kwargs={"slug": self.slug})
class CustomPageSection(VideoBlockMixin, models.Model):
TYPE_HERO = "hero"
TYPE_INTRO = "intro"
@@ -323,7 +383,6 @@ class CustomPageSection(VideoBlockMixin, models.Model):
TYPE_PROBLEMS = "problems"
TYPE_SUPPORTERS = "supporters"
TYPE_ABOUT_STRIP = "about_strip"
TYPE_FAQ = "faq"
TYPE_VIDEO = "video"
TYPE_CHOICES = [
@@ -334,12 +393,11 @@ class CustomPageSection(VideoBlockMixin, models.Model):
(TYPE_CUSTOM, "Custom Content"),
(TYPE_FEATURES, "Features Grid"),
(TYPE_SCREENSHOTS, "Screenshots Gallery"),
(TYPE_PRODUCTS, "Products Grid"),
(TYPE_PRODUCTS_CATALOG, "Products with Sub-products"),
(TYPE_PRODUCTS, "Projects Grid"),
(TYPE_PRODUCTS_CATALOG, "Projects with sub-projects"),
(TYPE_PROBLEMS, "Problems / Value Proposition"),
(TYPE_SUPPORTERS, "Supporters"),
(TYPE_ABOUT_STRIP, "About Strip"),
(TYPE_FAQ, "FAQ Accordion"),
(TYPE_VIDEO, "Video"),
]
@@ -377,11 +435,9 @@ class CustomPageSection(VideoBlockMixin, models.Model):
class PageVideo(VideoBlockMixin, models.Model):
PAGE_CONTACT = "contact"
PAGE_FAQ = "faq"
PAGE_CHOICES = [
(PAGE_CONTACT, "Contact"),
(PAGE_FAQ, "FAQ"),
(PAGE_CONTACT, "Become a Member"),
]
page = models.CharField(max_length=20, choices=PAGE_CHOICES)