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:
+65
-9
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user