{{ page.title }}
+{{ page.date }}
+diff --git a/innovedus_cms/home/migrations/0003_articlepage_categorypage.py b/innovedus_cms/home/migrations/0003_articlepage_categorypage.py
new file mode 100644
index 0000000..ed3cc8a
--- /dev/null
+++ b/innovedus_cms/home/migrations/0003_articlepage_categorypage.py
@@ -0,0 +1,39 @@
+# Generated by Django 5.2.7 on 2025-10-17 04:15
+
+import django.db.models.deletion
+import wagtail.fields
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('home', '0002_create_homepage'),
+ ('wagtailcore', '0095_groupsitepermission'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='ArticlePage',
+ fields=[
+ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
+ ('date', models.DateField(verbose_name='Published date')),
+ ('intro', models.CharField(blank=True, max_length=250)),
+ ('body', wagtail.fields.StreamField([('heading', 0), ('paragraph', 1), ('image', 2), ('embed', 3)], block_lookup={0: ('wagtail.blocks.CharBlock', (), {'form_classname': 'full title'}), 1: ('wagtail.blocks.RichTextBlock', (), {'features': ['bold', 'italic', 'link']}), 2: ('wagtail.images.blocks.ImageChooserBlock', (), {}), 3: ('wagtail.embeds.blocks.EmbedBlock', (), {})})),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('wagtailcore.page',),
+ ),
+ migrations.CreateModel(
+ name='CategoryPage',
+ fields=[
+ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('wagtailcore.page',),
+ ),
+ ]
diff --git a/innovedus_cms/home/migrations/0004_latestpage_recommandedpage.py b/innovedus_cms/home/migrations/0004_latestpage_recommandedpage.py
new file mode 100644
index 0000000..50bdb38
--- /dev/null
+++ b/innovedus_cms/home/migrations/0004_latestpage_recommandedpage.py
@@ -0,0 +1,35 @@
+# Generated by Django 5.2.7 on 2025-10-17 07:28
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('home', '0003_articlepage_categorypage'),
+ ('wagtailcore', '0095_groupsitepermission'),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name='LatestPage',
+ fields=[
+ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('wagtailcore.page',),
+ ),
+ migrations.CreateModel(
+ name='RecommandedPage',
+ fields=[
+ ('page_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='wagtailcore.page')),
+ ],
+ options={
+ 'abstract': False,
+ },
+ bases=('wagtailcore.page',),
+ ),
+ ]
diff --git a/innovedus_cms/home/migrations/0005_rename_recommandedpage_recommendedpage.py b/innovedus_cms/home/migrations/0005_rename_recommandedpage_recommendedpage.py
new file mode 100644
index 0000000..738ab54
--- /dev/null
+++ b/innovedus_cms/home/migrations/0005_rename_recommandedpage_recommendedpage.py
@@ -0,0 +1,18 @@
+# Generated by Django 5.2.7 on 2025-10-17 07:36
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('home', '0004_latestpage_recommandedpage'),
+ ('wagtailcore', '0095_groupsitepermission'),
+ ]
+
+ operations = [
+ migrations.RenameModel(
+ old_name='RecommandedPage',
+ new_name='RecommendedPage',
+ ),
+ ]
diff --git a/innovedus_cms/home/migrations/0006_articlepage_cover_image_articlepage_recommended.py b/innovedus_cms/home/migrations/0006_articlepage_cover_image_articlepage_recommended.py
new file mode 100644
index 0000000..ab23371
--- /dev/null
+++ b/innovedus_cms/home/migrations/0006_articlepage_cover_image_articlepage_recommended.py
@@ -0,0 +1,25 @@
+# Generated by Django 5.2.7 on 2025-10-17 07:50
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('home', '0005_rename_recommandedpage_recommendedpage'),
+ ('wagtailimages', '0027_image_description'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='articlepage',
+ name='cover_image',
+ field=models.ForeignKey(blank=True, help_text='文章列表與分享用的首圖', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image'),
+ ),
+ migrations.AddField(
+ model_name='articlepage',
+ name='recommended',
+ field=models.BooleanField(default=False, help_text='在推薦清單顯示'),
+ ),
+ ]
diff --git a/innovedus_cms/home/models.py b/innovedus_cms/home/models.py
index 5076f57..b7d3a3a 100644
--- a/innovedus_cms/home/models.py
+++ b/innovedus_cms/home/models.py
@@ -1,7 +1,174 @@
from django.db import models
from wagtail.models import Page
+from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
+BLOCK_SIZE = 5
+PAGE_SIZE = 10
-class HomePage(Page):
- pass
+class CategoryMixin:
+ def build_category_blocks(self, request=None):
+ blocks = []
+ 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,
+ })
+ else:
+ 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:
+ page_obj = paginator.page(page_number)
+ except PageNotAnInteger:
+ page_obj = paginator.page(1)
+ except EmptyPage:
+ page_obj = paginator.page(paginator.num_pages)
+
+ blocks.append({
+ "title": self.title,
+ "items": page_obj,
+ "url": self.url,
+ })
+ return blocks
+
+ def get_latest_articles(self, request=None):
+ latestPage = LatestPage.objects.first()
+ if not request:
+ return {
+ "title": latestPage.title,
+ "items": ArticlePage.objects.live().order_by("-first_published_at")[:BLOCK_SIZE],
+ "url": latestPage.url,
+ }
+ else:
+ paginator = Paginator(ArticlePage.objects.live().order_by("-first_published_at"), PAGE_SIZE)
+ page_number = request.GET.get("page")
+
+ try:
+ page_obj = paginator.page(page_number)
+ except PageNotAnInteger:
+ page_obj = paginator.page(1)
+ except EmptyPage:
+ page_obj = paginator.page(paginator.num_pages)
+ return {
+ "title": self.title,
+ "items": page_obj,
+ "url": self.url,
+ }
+
+ def get_recommended_articles(self, request=None):
+ recommendedPage = RecommendedPage.objects.first()
+ if not request:
+ return {
+ "title": recommendedPage.title,
+ "items": ArticlePage.objects.filter(recommended=True).live()[:BLOCK_SIZE],
+ "url": recommendedPage.url,
+ }
+ else:
+ paginator = Paginator(ArticlePage.objects.filter(recommended=True).live(), PAGE_SIZE)
+ page_number = request.GET.get("page")
+
+ try:
+ page_obj = paginator.page(page_number)
+ except PageNotAnInteger:
+ page_obj = paginator.page(1)
+ except EmptyPage:
+ page_obj = paginator.page(paginator.num_pages)
+ return {
+ "title": self.title,
+ "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(),
+ ]
+
+ # 找出第一層 CategoryPage(HomePage 直屬子項)
+ categories = CategoryPage.objects.child_of(self).live().in_menu()
+
+ # 若第一層沒有分類,就嘗試抓所有 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,
+ })
+
+ 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)
+ ]
+ 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)
+ ]
+ return context
+
+class CategoryPage(Page, CategoryMixin):
+ @property
+ def has_subcategories(self):
+ return self.get_children().type(CategoryPage).live().exists()
+
+ def get_context(self, request):
+ 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
+
+class ArticlePage(Page):
+ cover_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="在推薦清單顯示")
+
+ content_panels = Page.content_panels + [
+ FieldPanel("recommended"),
+ FieldPanel("cover_image"),
+ FieldPanel("date"),
+ FieldPanel("intro"),
+ FieldPanel("body"),
+ ]
diff --git a/innovedus_cms/home/static/img/default_cover.jpg b/innovedus_cms/home/static/img/default_cover.jpg
new file mode 100644
index 0000000..61cb38d
Binary files /dev/null and b/innovedus_cms/home/static/img/default_cover.jpg differ
diff --git a/innovedus_cms/home/templates/home/article_page.html b/innovedus_cms/home/templates/home/article_page.html
new file mode 100644
index 0000000..788c1c0
--- /dev/null
+++ b/innovedus_cms/home/templates/home/article_page.html
@@ -0,0 +1,15 @@
+{% extends "base.html" %}
+{% load wagtailcore_tags wagtailimages_tags %}
+
+{% block content %}
+ {{ page.date }}
+
{{ page.title }}
+
+ {% endif %}
+ {{ article.title }}
+
+
+ {% endif %}
+ {{ article.title }}
+
+