Add article and category page models, templates, and default cover image

This commit is contained in:
Warren Chen 2025-10-17 17:47:21 +09:00
parent 0874255859
commit 3232de90d4
12 changed files with 375 additions and 10 deletions

View File

@ -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',),
),
]

View File

@ -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',),
),
]

View File

@ -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',
),
]

View File

@ -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='在推薦清單顯示'),
),
]

View File

@ -1,7 +1,174 @@
from django.db import models from django.db import models
from wagtail.models import Page from wagtail.models import Page
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
BLOCK_SIZE = 5
PAGE_SIZE = 10
class HomePage(Page): class CategoryMixin:
pass 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(),
]
# 找出第一層 CategoryPageHomePage 直屬子項)
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"),
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

View File

@ -0,0 +1,15 @@
{% extends "base.html" %}
{% load wagtailcore_tags wagtailimages_tags %}
{% block content %}
<article>
{% image page.cover_image original as cover %}
<img src="{{ cover.url }}" alt="{{ page.title }}">
<h1>{{ page.title }}</h1>
<p class="date">{{ page.date }}</p>
<div class="intro">{{ page.intro }}</div>
<div class="body">
{{ page.body }}
</div>
</article>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% load wagtailcore_tags %}
{% block content %}
{% if page.has_subcategories %}
{% include "home/includes/category_block_list.html" %}
{% else %}
{% include "home/includes/category_full_list.html" %}
{% endif %}
{% endblock %}

View File

@ -5,17 +5,10 @@
{% block extra_css %} {% block extra_css %}
{% comment %}
Delete the line below if you're just getting started and want to remove the welcome screen!
{% endcomment %}
<link rel="stylesheet" href="{% static 'css/welcome_page.css' %}">
{% endblock extra_css %} {% endblock extra_css %}
{% block content %} {% block content %}
{% comment %} {% include "home/includes/category_block_list.html" with category_blocks=category_blocks %}
Delete the line below if you're just getting started and want to remove the welcome screen!
{% endcomment %}
{% include 'home/welcome_page.html' %}
{% endblock content %} {% endblock content %}

View File

@ -0,0 +1,25 @@
{% load wagtailimages_tags %}
<div class="category-block-list">
{% for category in category_blocks %}
<section class="category-section">
<h2><a href="{{ category.url }}">{{ category.title }}</a></h2>
<ul>
{% for article in category.items %}
<li>
<a href="{{ article.url }}">
{% if article.cover_image %}
{% image article.cover_image max-200x200 as cover %}
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
{% else %}
<img src="/static/img/default_cover.jpg" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
{% endif %}
{{ article.title }}
</a>
</li>
{% endfor %}
</ul>
</section>
{% endfor %}
</div>

View File

@ -0,0 +1,39 @@
{% load wagtailimages_tags %}
<div class="category-full-list">
{% with category=category_blocks.0 %}
<h2><a href="{{ category.url }}">{{ category.title }}</a></h2>
<ul>
{% for article in category.items %}
<article>
<li>
<a href="{{ article.url }}">
{% if article.cover_image %}
{% image article.cover_image max-200x200 as cover %}
<img src="{{ cover.url }}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
{% else %}
<img src="/static/img/default_cover.jpg" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:contain;display:block;"/>
{% endif %}
{{ article.title }}
</a>
</li>
<!-- <p>{{ article.search_description }}</p> -->
</article>
{% endfor %}
</ul>
{% if category.items.paginator.num_pages > 1 %}
<div class="pagination">
{% if category.items.has_previous %}
<a href="?page={{ category.items.previous_page_number }}">← 上一頁</a>
{% endif %}
<span>第 {{ category.items.number }} / {{ category.items.paginator.num_pages }} 頁</span>
{% if category.items.has_next %}
<a href="?page={{ category.items.next_page_number }}">下一頁 →</a>
{% endif %}
</div>
{% endif %}
{% endwith %}
</div>

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB