Add article and category page models, templates, and default cover image
This commit is contained in:
parent
0874255859
commit
3232de90d4
@ -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',),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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',),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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',
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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='在推薦清單顯示'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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(),
|
||||||
|
]
|
||||||
|
|
||||||
|
# 找出第一層 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"),
|
||||||
|
]
|
||||||
|
|||||||
BIN
innovedus_cms/home/static/img/default_cover.jpg
Normal file
BIN
innovedus_cms/home/static/img/default_cover.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.0 KiB |
15
innovedus_cms/home/templates/home/article_page.html
Normal file
15
innovedus_cms/home/templates/home/article_page.html
Normal 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 %}
|
||||||
9
innovedus_cms/home/templates/home/category_page.html
Normal file
9
innovedus_cms/home/templates/home/category_page.html
Normal 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 %}
|
||||||
@ -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 %}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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>
|
||||||
BIN
innovedus_cms/media/original_images/DefaultArticleCover.jpg
Normal file
BIN
innovedus_cms/media/original_images/DefaultArticleCover.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 60 KiB |
Loading…
x
Reference in New Issue
Block a user