{{ page.title }}
+ {% if page.banner_image %} + {% image page.banner_image original as banner %} +{{ page.date }}
diff --git a/.gitignore b/.gitignore
index 431a750..3d5de7c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.env
__pycache__
-*.pyc
\ No newline at end of file
+*.pyc
+media/
\ No newline at end of file
diff --git a/innovedus_cms/home/blocks.py b/innovedus_cms/home/blocks.py
new file mode 100644
index 0000000..9474647
--- /dev/null
+++ b/innovedus_cms/home/blocks.py
@@ -0,0 +1,37 @@
+from django.core.exceptions import ValidationError
+from wagtail.embeds.blocks import EmbedBlock
+from wagtail.embeds import embeds as wagtail_embeds
+from wagtail import blocks
+
+
+class ValidatingEmbedBlock(EmbedBlock):
+ """
+ Embed block that validates the URL at clean-time by resolving
+ via Wagtail's embed system. Raises ValidationError if not embeddable.
+ """
+
+ def clean(self, value):
+ value = super().clean(value)
+ if value:
+ try:
+ # Attempt to resolve and cache embed; will raise on failure
+ wagtail_embeds.get_embed(value)
+ except Exception:
+ raise ValidationError(
+ "嵌入連結無法驗證,請確認為公開且可嵌入的 URL。"
+ )
+ return value
+
+
+class H2HeadingBlock(blocks.CharBlock):
+ class Meta:
+ template = "home/blocks/h2_heading.html"
+ icon = "title"
+ label = "Heading"
+
+
+class HorizontalRuleBlock(blocks.StaticBlock):
+ class Meta:
+ template = "home/blocks/horizontal_rule.html"
+ icon = "horizontalrule"
+ label = "Separator"
diff --git a/innovedus_cms/home/migrations/0007_articlepage_banner_image_alter_articlepage_body.py b/innovedus_cms/home/migrations/0007_articlepage_banner_image_alter_articlepage_body.py
new file mode 100644
index 0000000..895da35
--- /dev/null
+++ b/innovedus_cms/home/migrations/0007_articlepage_banner_image_alter_articlepage_body.py
@@ -0,0 +1,26 @@
+# Generated by Django 5.2.7 on 2025-10-29 06:25
+
+import django.db.models.deletion
+import wagtail.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('home', '0006_articlepage_cover_image_articlepage_recommended'),
+ ('wagtailimages', '0027_image_description'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='articlepage',
+ name='banner_image',
+ field=models.ForeignKey(blank=True, help_text='文章頁頂部橫幅圖片', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image'),
+ ),
+ migrations.AlterField(
+ model_name='articlepage',
+ name='body',
+ field=wagtail.fields.StreamField([('heading', 0), ('paragraph', 1), ('image', 2), ('embed', 3), ('hr', 4), ('html', 5)], block_lookup={0: ('home.blocks.H2HeadingBlock', (), {'form_classname': 'full title'}), 1: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link', 'ol', 'ul']}), 2: ('wagtail.images.blocks.ImageChooserBlock', (), {}), 3: ('home.blocks.ValidatingEmbedBlock', (), {}), 4: ('home.blocks.HorizontalRuleBlock', (), {}), 5: ('wagtail.blocks.RawHTMLBlock', (), {'help_text': '僅限信任來源的 blockquote/iframe 原始碼'})}),
+ ),
+ ]
diff --git a/innovedus_cms/home/models.py b/innovedus_cms/home/models.py
index b7d3a3a..8ce9420 100644
--- a/innovedus_cms/home/models.py
+++ b/innovedus_cms/home/models.py
@@ -6,19 +6,29 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
BLOCK_SIZE = 5
PAGE_SIZE = 10
+
class CategoryMixin:
def build_category_blocks(self, request=None):
blocks = []
- subcategories = self.get_children().type(CategoryPage).live()
+ subcategories = self.get_children().type(CategoryPage).live()
if subcategories.exists():
- for category in subcategories :
- blocks.append({
- "title": category.title,
- "items": ArticlePage.objects.child_of(category).live().order_by("-first_published_at")[:BLOCK_SIZE],
- "url": category.url,
- })
+ for category in subcategories:
+ blocks.append(
+ {
+ "title": category.title,
+ "items": ArticlePage.objects.child_of(category)
+ .live()
+ .order_by("-first_published_at")[:BLOCK_SIZE],
+ "url": category.url,
+ }
+ )
else:
- paginator = Paginator(ArticlePage.objects.child_of(self).live().order_by("-first_published_at"), PAGE_SIZE)
+ paginator = Paginator(
+ ArticlePage.objects.child_of(self)
+ .live()
+ .order_by("-first_published_at"),
+ PAGE_SIZE,
+ )
page_number = request.GET.get("page") if request else None
try:
@@ -28,23 +38,29 @@ class CategoryMixin:
except EmptyPage:
page_obj = paginator.page(paginator.num_pages)
- blocks.append({
- "title": self.title,
- "items": page_obj,
- "url": self.url,
- })
+ blocks.append(
+ {
+ "title": self.title,
+ "items": page_obj,
+ "url": self.url,
+ }
+ )
return blocks
-
+
def get_latest_articles(self, request=None):
- latestPage = LatestPage.objects.first()
+ latest_page = LatestPage.objects.first()
if not request:
return {
- "title": latestPage.title,
- "items": ArticlePage.objects.live().order_by("-first_published_at")[:BLOCK_SIZE],
- "url": latestPage.url,
+ "title": latest_page.title if latest_page else "最新文章",
+ "items": ArticlePage.objects.live().order_by("-first_published_at")[
+ :BLOCK_SIZE
+ ],
+ "url": latest_page.url if latest_page else "#",
}
else:
- paginator = Paginator(ArticlePage.objects.live().order_by("-first_published_at"), PAGE_SIZE)
+ paginator = Paginator(
+ ArticlePage.objects.live().order_by("-first_published_at"), PAGE_SIZE
+ )
page_number = request.GET.get("page")
try:
@@ -60,12 +76,12 @@ class CategoryMixin:
}
def get_recommended_articles(self, request=None):
- recommendedPage = RecommendedPage.objects.first()
+ recommended_page = RecommendedPage.objects.first()
if not request:
return {
- "title": recommendedPage.title,
+ "title": recommended_page.title if recommended_page else "推薦文章",
"items": ArticlePage.objects.filter(recommended=True).live()[:BLOCK_SIZE],
- "url": recommendedPage.url,
+ "url": recommended_page.url if recommended_page else "#",
}
else:
paginator = Paginator(ArticlePage.objects.filter(recommended=True).live(), PAGE_SIZE)
@@ -82,54 +98,54 @@ class CategoryMixin:
"items": page_obj,
"url": self.url,
}
- return blocks
+
class HomePage(Page, CategoryMixin):
def get_context(self, request):
context = super().get_context(request)
- category_blocks = [
- self.get_latest_articles(),
- self.get_recommended_articles(),
- ]
+ category_blocks = [self.get_latest_articles(), self.get_recommended_articles()]
- # 找出第一層 CategoryPage(HomePage 直屬子項)
+ # 找出第一層 CategoryPage(HomePage 直屬子頁)
categories = CategoryPage.objects.child_of(self).live().in_menu()
- # 若第一層沒有分類,就嘗試抓所有 descendant CategoryPage
+ # 若第一層沒有,抓 descendant CategoryPage
if not categories.exists():
categories = CategoryPage.objects.descendant_of(self).live().in_menu()
-
+
for category in categories:
subcategories = category.get_children().type(CategoryPage).live()
- category_blocks.append({
- "title": category.title,
- "type": "category",
- "items": subcategories or ArticlePage.objects.child_of(category).live()[:BLOCK_SIZE],
- "url": category.url,
- })
+ category_blocks.append(
+ {
+ "title": category.title,
+ "type": "category",
+ "items": ArticlePage.objects.child_of(category).live()[:BLOCK_SIZE],
+ "url": category.url,
+ }
+ )
context["category_blocks"] = category_blocks
return context
+
class LatestPage(Page, CategoryMixin):
template = "home/category_page.html"
+
def get_context(self, request):
context = super().get_context(request)
- context["category_blocks"] = [
- self.get_latest_articles(request)
- ]
+ context["category_blocks"] = [self.get_latest_articles(request)]
return context
-
+
+
class RecommendedPage(Page, CategoryMixin):
template = "home/category_page.html"
+
def get_context(self, request):
context = super().get_context(request)
- context["category_blocks"] = [
- self.get_recommended_articles(request)
- ]
+ context["category_blocks"] = [self.get_recommended_articles(request)]
return context
+
class CategoryPage(Page, CategoryMixin):
@property
def has_subcategories(self):
@@ -139,36 +155,53 @@ class CategoryPage(Page, CategoryMixin):
context = super().get_context(request)
context["category_blocks"] = self.build_category_blocks(request)
return context
-
-# from wagtail.fields import RichTextField
+
+
from wagtail.admin.panels import FieldPanel
from wagtail import blocks
-from wagtail.embeds.blocks import EmbedBlock
from wagtail.images.blocks import ImageChooserBlock
from wagtail.fields import StreamField
+from .blocks import ValidatingEmbedBlock, H2HeadingBlock, HorizontalRuleBlock
+
class ArticlePage(Page):
cover_image = models.ForeignKey(
"wagtailimages.Image",
- null=True, blank=True,
+ null=True,
+ blank=True,
on_delete=models.SET_NULL,
related_name="+",
- help_text="文章列表與分享用的首圖"
+ help_text="列表封面圖",
+ )
+ banner_image = models.ForeignKey(
+ "wagtailimages.Image",
+ null=True,
+ blank=True,
+ on_delete=models.SET_NULL,
+ related_name="+",
+ help_text="文章內文橫幅圖片",
)
date = models.DateField("Published date")
intro = models.CharField(max_length=250, blank=True)
- body = StreamField([
- ("heading", blocks.CharBlock(form_classname="full title")),
- ("paragraph", blocks.RichTextBlock(features=["bold", "italic", "link"])),
- ("image", ImageChooserBlock()),
- ("embed", EmbedBlock()),
- ], use_json_field=True)
- recommended = models.BooleanField(default=False, help_text="在推薦清單顯示")
+ body = StreamField(
+ [
+ ("heading", H2HeadingBlock(form_classname="full title")),
+ ("paragraph", blocks.RichTextBlock(features=["bold", "italic", "link"])),
+ ("image", ImageChooserBlock()),
+ ("embed", ValidatingEmbedBlock()),
+ ("hr", HorizontalRuleBlock()),
+ ("html", blocks.RawHTMLBlock(help_text="僅限信任來源的 blockquote/iframe 原始碼")),
+ ],
+ use_json_field=True,
+ )
+ recommended = models.BooleanField(default=False, help_text="在推薦區塊顯示")
content_panels = Page.content_panels + [
FieldPanel("recommended"),
FieldPanel("cover_image"),
+ FieldPanel("banner_image"),
FieldPanel("date"),
FieldPanel("intro"),
FieldPanel("body"),
]
+
diff --git a/innovedus_cms/home/templates/home/article_page.html b/innovedus_cms/home/templates/home/article_page.html
index 788c1c0..3a1307b 100644
--- a/innovedus_cms/home/templates/home/article_page.html
+++ b/innovedus_cms/home/templates/home/article_page.html
@@ -3,13 +3,15 @@
{% block content %}
{{ page.date }}
{{ page.title }}
+ {% if page.banner_image %}
+ {% image page.banner_image original as banner %}
+
+ {% endif %}
+
{% endif %}
{{ article.title }}
diff --git a/innovedus_cms/home/templates/home/includes/category_full_list.html b/innovedus_cms/home/templates/home/includes/category_full_list.html
index 296d914..6538144 100644
--- a/innovedus_cms/home/templates/home/includes/category_full_list.html
+++ b/innovedus_cms/home/templates/home/includes/category_full_list.html
@@ -1,4 +1,4 @@
-{% load wagtailimages_tags %}
+{% load wagtailimages_tags static %}
+
{% endif %}
{{ article.title }}
@@ -26,14 +26,15 @@
{% if category.items.paginator.num_pages > 1 %}
{% endif %}
{% endwith %}