Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 78f29a14cd | |||
|
|
a72e23e9e0 | ||
| 0ad93cfb78 |
3
.gitignore
vendored
3
.gitignore
vendored
@ -1,4 +1,3 @@
|
||||
.env
|
||||
__pycache__
|
||||
*.pyc
|
||||
media/
|
||||
*.pyc
|
||||
@ -1,8 +1,3 @@
|
||||
fly.toml
|
||||
.git/
|
||||
.venv
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.sqlite3
|
||||
media/
|
||||
*.log
|
||||
|
||||
@ -2,29 +2,20 @@ ARG PYTHON_VERSION=3.13-slim
|
||||
|
||||
FROM python:${PYTHON_VERSION}
|
||||
|
||||
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PIP_NO_CACHE_DIR=on \
|
||||
DJANGO_SETTINGS_MODULE=mysite.settings.production
|
||||
ENV PYTHONDONTWRITEBYTECODE 1
|
||||
ENV PYTHONUNBUFFERED 1
|
||||
|
||||
RUN mkdir -p /code
|
||||
|
||||
WORKDIR /code
|
||||
|
||||
# Create an unprivileged user to run the app
|
||||
RUN adduser --disabled-password --gecos '' app
|
||||
|
||||
COPY requirements.txt /tmp/requirements.txt
|
||||
RUN set -ex && \
|
||||
pip install --upgrade pip && \
|
||||
pip install -r /tmp/requirements.txt && \
|
||||
rm -rf /root/.cache/
|
||||
|
||||
COPY . /code
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh && chown -R app:app /code
|
||||
USER app
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["gunicorn","--bind",":8000","--workers","2","mysite.wsgi"]
|
||||
|
||||
@ -1,3 +0,0 @@
|
||||
from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
@ -1,6 +0,0 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class BaseConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'base'
|
||||
@ -1,55 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-10-15 03:30
|
||||
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
import wagtail.fields
|
||||
import wagtail.models.preview
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('wagtailcore', '0095_groupsitepermission'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='NavigationSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('linkedin_url', models.URLField(blank=True, verbose_name='LinkedIn URL')),
|
||||
('github_url', models.URLField(blank=True, verbose_name='GitHub URL')),
|
||||
('mastodon_url', models.URLField(blank=True, verbose_name='Mastodon URL')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FooterText',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('translation_key', models.UUIDField(default=uuid.uuid4, editable=False)),
|
||||
('live', models.BooleanField(default=True, editable=False, verbose_name='live')),
|
||||
('has_unpublished_changes', models.BooleanField(default=False, editable=False, verbose_name='has unpublished changes')),
|
||||
('first_published_at', models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='first published at')),
|
||||
('last_published_at', models.DateTimeField(editable=False, null=True, verbose_name='last published at')),
|
||||
('go_live_at', models.DateTimeField(blank=True, null=True, verbose_name='go live date/time')),
|
||||
('expire_at', models.DateTimeField(blank=True, null=True, verbose_name='expiry date/time')),
|
||||
('expired', models.BooleanField(default=False, editable=False, verbose_name='expired')),
|
||||
('body', wagtail.fields.RichTextField()),
|
||||
('latest_revision', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.revision', verbose_name='latest revision')),
|
||||
('live_revision', models.ForeignKey(blank=True, editable=False, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailcore.revision', verbose_name='live revision')),
|
||||
('locale', models.ForeignKey(editable=False, on_delete=django.db.models.deletion.PROTECT, related_name='+', to='wagtailcore.locale', verbose_name='locale')),
|
||||
],
|
||||
options={
|
||||
'verbose_name_plural': 'Footer Text',
|
||||
'abstract': False,
|
||||
'unique_together': {('translation_key', 'locale')},
|
||||
},
|
||||
bases=(wagtail.models.preview.PreviewableMixin, models.Model),
|
||||
),
|
||||
]
|
||||
@ -1,59 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-26 08:11
|
||||
|
||||
import django.db.models.deletion
|
||||
import wagtail.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('base', '0001_initial'),
|
||||
('wagtailimages', '0027_image_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SocialMediaSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('links', wagtail.fields.StreamField([('link', 2)], block_lookup={0: ('wagtail.blocks.ChoiceBlock', [], {'choices': [('facebook', 'Facebook'), ('twitter', 'Twitter'), ('instagram', 'Instagram'), ('thread', 'Thread'), ('linkedin', 'LinkedIn'), ('youtube', 'YouTube')]}), 1: ('wagtail.blocks.URLBlock', (), {}), 2: ('wagtail.blocks.StructBlock', [[('platform', 0), ('url', 1)]], {})})),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='navigationsettings',
|
||||
options={'verbose_name': 'Footer Navigation'},
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='navigationsettings',
|
||||
name='github_url',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='navigationsettings',
|
||||
name='linkedin_url',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='navigationsettings',
|
||||
name='mastodon_url',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='navigationsettings',
|
||||
name='footer_links',
|
||||
field=wagtail.fields.StreamField([('section', 5)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {'required': False}), 1: ('wagtail.blocks.CharBlock', (), {}), 2: ('wagtail.blocks.URLBlock', (), {}), 3: ('wagtail.blocks.StructBlock', [[('label', 1), ('url', 2)]], {}), 4: ('wagtail.blocks.ListBlock', (3,), {}), 5: ('wagtail.blocks.StructBlock', [[('title', 0), ('links', 4)]], {})}, null=True),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='HeaderSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('site_name', models.CharField(blank=True, max_length=255)),
|
||||
('extra_links', wagtail.fields.StreamField([('link', 2)], blank=True, block_lookup={0: ('wagtail.blocks.CharBlock', (), {}), 1: ('wagtail.blocks.URLBlock', (), {}), 2: ('wagtail.blocks.StructBlock', [[('label', 0), ('url', 1)]], {})}, null=True)),
|
||||
('logo', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Header Settings',
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -1,131 +0,0 @@
|
||||
from django.db import models
|
||||
from wagtail.admin.panels import (
|
||||
FieldPanel,
|
||||
MultiFieldPanel,
|
||||
|
||||
# import PublishingPanel:
|
||||
PublishingPanel,
|
||||
)
|
||||
|
||||
# import RichTextField:
|
||||
from wagtail.fields import RichTextField
|
||||
|
||||
# import DraftStateMixin, PreviewableMixin, RevisionMixin, TranslatableMixin:
|
||||
from wagtail.models import (
|
||||
DraftStateMixin,
|
||||
PreviewableMixin,
|
||||
RevisionMixin,
|
||||
TranslatableMixin,
|
||||
)
|
||||
|
||||
from wagtail.contrib.settings.models import (
|
||||
BaseGenericSetting,
|
||||
register_setting,
|
||||
)
|
||||
|
||||
from wagtail.snippets.models import register_snippet
|
||||
from wagtail.fields import StreamField
|
||||
from wagtail import blocks
|
||||
|
||||
@register_setting
|
||||
class HeaderSettings(BaseGenericSetting):
|
||||
logo = models.ForeignKey(
|
||||
"wagtailimages.Image",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
)
|
||||
site_name = models.CharField(max_length=255, blank=True)
|
||||
extra_links = StreamField([
|
||||
("link", blocks.StructBlock([
|
||||
("label", blocks.CharBlock()),
|
||||
("url", blocks.URLBlock())
|
||||
]))
|
||||
], use_json_field=True, blank=True, null=True)
|
||||
|
||||
panels = [
|
||||
MultiFieldPanel(
|
||||
[
|
||||
FieldPanel("logo"),
|
||||
FieldPanel("site_name"),
|
||||
FieldPanel("extra_links"),
|
||||
],
|
||||
heading="Header Settings",
|
||||
),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Header Settings"
|
||||
|
||||
@register_setting
|
||||
class NavigationSettings(BaseGenericSetting):
|
||||
footer_links = StreamField([
|
||||
("section", blocks.StructBlock([
|
||||
("title", blocks.CharBlock(required=False)),
|
||||
("links", blocks.ListBlock(blocks.StructBlock([
|
||||
("label", blocks.CharBlock()),
|
||||
("url", blocks.URLBlock())
|
||||
]))),
|
||||
]))
|
||||
], use_json_field=True, blank=True, null=True)
|
||||
|
||||
panels = [
|
||||
FieldPanel("footer_links"),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
verbose_name = "Footer Navigation"
|
||||
|
||||
class SocialLinkBlock(blocks.StructBlock):
|
||||
SOCIAL_MEDIA_CHOICES = [
|
||||
("facebook", "Facebook"),
|
||||
("twitter", "Twitter"),
|
||||
("instagram", "Instagram"),
|
||||
("thread", "Thread"),
|
||||
("linkedin", "LinkedIn"),
|
||||
("youtube", "YouTube"),
|
||||
]
|
||||
|
||||
platform = blocks.ChoiceBlock(choices=SOCIAL_MEDIA_CHOICES)
|
||||
url = blocks.URLBlock()
|
||||
|
||||
class Meta:
|
||||
icon = "link"
|
||||
label = "Social Link"
|
||||
|
||||
@register_setting
|
||||
class SocialMediaSettings(BaseGenericSetting):
|
||||
links = StreamField([
|
||||
("link", SocialLinkBlock()),
|
||||
], use_json_field=True)
|
||||
|
||||
panels = [FieldPanel("links")]
|
||||
|
||||
@register_snippet
|
||||
class FooterText(
|
||||
DraftStateMixin,
|
||||
RevisionMixin,
|
||||
PreviewableMixin,
|
||||
TranslatableMixin,
|
||||
models.Model,
|
||||
):
|
||||
|
||||
body = RichTextField()
|
||||
|
||||
panels = [
|
||||
FieldPanel("body"),
|
||||
PublishingPanel(),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return "Footer text"
|
||||
|
||||
def get_preview_template(self, request, mode_name):
|
||||
return "base.html"
|
||||
|
||||
def get_preview_context(self, request, mode_name):
|
||||
return {"footer_text": self.body}
|
||||
|
||||
class Meta(TranslatableMixin.Meta):
|
||||
verbose_name_plural = "Footer Text"
|
||||
@ -1,5 +0,0 @@
|
||||
{% load wagtailcore_tags %}
|
||||
|
||||
<div>
|
||||
{{ footer_text|richtext }}
|
||||
</div>
|
||||
@ -1,18 +0,0 @@
|
||||
from django import template
|
||||
|
||||
from base.models import FooterText
|
||||
|
||||
register = template.Library()
|
||||
|
||||
|
||||
@register.inclusion_tag("base/includes/footer_text.html", takes_context=True)
|
||||
def get_footer_text(context):
|
||||
footer_text = context.get("footer_text", "")
|
||||
|
||||
if not footer_text:
|
||||
instance = FooterText.objects.filter(live=True).first()
|
||||
footer_text = instance.body if instance else ""
|
||||
|
||||
return {
|
||||
"footer_text": footer_text,
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@ -1,3 +0,0 @@
|
||||
from django.shortcuts import render
|
||||
|
||||
# Create your views here.
|
||||
@ -1,8 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
set -e
|
||||
|
||||
# Run pending migrations and collect static assets before starting the app
|
||||
python manage.py migrate --noinput
|
||||
python manage.py collectstatic --noinput
|
||||
|
||||
exec "$@"
|
||||
@ -1,58 +0,0 @@
|
||||
from django.core.exceptions import ValidationError
|
||||
from wagtail.embeds.blocks import EmbedBlock
|
||||
from wagtail.embeds import embeds as wagtail_embeds
|
||||
from wagtail import blocks
|
||||
from urllib.parse import urlparse
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
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:
|
||||
# Inherit base validation (already run via super().clean), and add
|
||||
# optional network validation for selected providers.
|
||||
url_str = value if isinstance(value, str) else getattr(value, "url", None)
|
||||
if not url_str:
|
||||
return value
|
||||
host = (urlparse(url_str).hostname or "").lower()
|
||||
|
||||
strict_hosts = getattr(
|
||||
settings,
|
||||
"EMBED_STRICT_HOSTS",
|
||||
("instagram.com", "facebook.com"),
|
||||
)
|
||||
validate_all = getattr(settings, "EMBED_STRICT_VALIDATE_ALL", False)
|
||||
|
||||
def _matches(h: str) -> bool:
|
||||
return host == h or host.endswith("." + h)
|
||||
|
||||
must_validate = bool(validate_all or any(_matches(h) for h in strict_hosts))
|
||||
if must_validate:
|
||||
try:
|
||||
# Attempt to resolve and cache embed; will raise on failure
|
||||
wagtail_embeds.get_embed(url_str)
|
||||
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"
|
||||
@ -1,11 +0,0 @@
|
||||
from home.models import LatestPage, TrendingPage
|
||||
|
||||
|
||||
def navigation_pages(request):
|
||||
"""
|
||||
Provide Latest/Trending page references for site-wide navigation.
|
||||
"""
|
||||
return {
|
||||
"nav_latest_page": LatestPage.objects.live().first(),
|
||||
"nav_trending_page": TrendingPage.objects.live().first(),
|
||||
}
|
||||
@ -1,83 +0,0 @@
|
||||
# Generated by Django 5.2.7 on 2025-11-26 08:11
|
||||
|
||||
import django.db.models.deletion
|
||||
import home.models
|
||||
import modelcluster.contrib.taggit
|
||||
import modelcluster.fields
|
||||
import wagtail.fields
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('home', '0002_create_homepage'),
|
||||
('taggit', '0006_rename_taggeditem_content_type_object_id_taggit_tagg_content_8fc721_idx'),
|
||||
('wagtailcore', '0095_groupsitepermission'),
|
||||
('wagtailimages', '0027_image_description'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
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', home.models.CategoryMixin),
|
||||
),
|
||||
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', home.models.CategoryMixin),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='TrendingPage',
|
||||
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', home.models.CategoryMixin),
|
||||
),
|
||||
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.DateTimeField(verbose_name='Published date')),
|
||||
('intro', models.CharField(blank=True, max_length=250)),
|
||||
('body', 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']}), 2: ('wagtail.images.blocks.ImageChooserBlock', (), {}), 3: ('home.blocks.ValidatingEmbedBlock', (), {}), 4: ('home.blocks.HorizontalRuleBlock', (), {}), 5: ('wagtail.blocks.RawHTMLBlock', (), {'help_text': '僅限信任來源的 blockquote/iframe 原始碼'})})),
|
||||
('trending', models.BooleanField(default=False, help_text='在熱門區塊顯示', verbose_name='Trending')),
|
||||
('banner_image', models.ForeignKey(blank=True, help_text='文章內文橫幅圖片', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
|
||||
('cover_image', models.ForeignKey(blank=True, help_text='列表封面圖', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='wagtailimages.image')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
bases=('wagtailcore.page',),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ArticlePageTag',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('content_object', modelcluster.fields.ParentalKey(on_delete=django.db.models.deletion.CASCADE, related_name='tagged_items', to='home.articlepage')),
|
||||
('tag', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='%(app_label)s_%(class)s_items', to='taggit.tag')),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='articlepage',
|
||||
name='tags',
|
||||
field=modelcluster.contrib.taggit.ClusterTaggableManager(blank=True, help_text='A comma-separated list of tags.', through='home.ArticlePageTag', to='taggit.Tag', verbose_name='Tags'),
|
||||
),
|
||||
]
|
||||
@ -1,330 +1,7 @@
|
||||
import os
|
||||
|
||||
from django.db import models
|
||||
|
||||
from wagtail.models import Page
|
||||
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
|
||||
from modelcluster.contrib.taggit import ClusterTaggableManager
|
||||
from modelcluster.fields import ParentalKey
|
||||
from taggit.models import TaggedItemBase
|
||||
from wagtail.search import index
|
||||
|
||||
def _get_env_int(name, default):
|
||||
value = os.environ.get(name)
|
||||
if value is None:
|
||||
return default
|
||||
try:
|
||||
return int(value)
|
||||
except ValueError:
|
||||
return default
|
||||
|
||||
BLOCK_SIZE = _get_env_int("HOMEPAGE_BLOCK_SIZE", 5) # Default to 5 articles in block layout
|
||||
HORIZON_SIZE = _get_env_int("HOMEPAGE_HORIZON_SIZE", 8) # Default to 8 articles in horizon layout
|
||||
PAGE_SIZE = _get_env_int("HOMEPAGE_PAGE_SIZE", 10) # Default to 10 articles per page for pagination
|
||||
|
||||
# Mixin for Category-related functionality
|
||||
class CategoryMixin:
|
||||
# Build category blocks
|
||||
def build_category_blocks(self, request=None):
|
||||
blocks = []
|
||||
subcategories = self.get_children().type(CategoryPage).live()
|
||||
if subcategories.exists():
|
||||
# If there are subcategories, create blocks for each
|
||||
for category in subcategories:
|
||||
blocks.append(
|
||||
{
|
||||
"title": category.title,
|
||||
"items": ArticlePage.objects.child_of(category)
|
||||
.live()
|
||||
.order_by("-date")[:HORIZON_SIZE],
|
||||
"url": category.url,
|
||||
"layout": "horizon",
|
||||
}
|
||||
)
|
||||
else:
|
||||
# If no subcategories, paginate articles under this category
|
||||
paginator = Paginator(
|
||||
ArticlePage.objects.child_of(self)
|
||||
.live()
|
||||
.order_by("-date"),
|
||||
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
|
||||
|
||||
# Build breadcrumbs
|
||||
def build_breadcrumbs(self):
|
||||
site = self.get_site()
|
||||
site_root = site.root_page if site else None
|
||||
if site_root:
|
||||
ancestors = self.get_ancestors().specific().filter(depth__gt=site_root.depth)
|
||||
else:
|
||||
ancestors = self.get_ancestors().specific()
|
||||
breadcrumbs = list(ancestors) + [self]
|
||||
return breadcrumbs, site_root
|
||||
|
||||
# Get latest articles
|
||||
def get_latest_articles(self, request=None):
|
||||
latest_page = LatestPage.objects.first()
|
||||
if not request:
|
||||
# No request means no pagination (e.g., homepage)
|
||||
return {
|
||||
"title": latest_page.title,
|
||||
"items": ArticlePage.objects.live().order_by("-date")[
|
||||
:BLOCK_SIZE
|
||||
],
|
||||
"url": latest_page.url,
|
||||
}
|
||||
else:
|
||||
# Paginated view
|
||||
paginator = Paginator(
|
||||
ArticlePage.objects.live().order_by("-date"), 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_trending_articles(self, request=None, exclude_ids=None):
|
||||
trending_page = TrendingPage.objects.first()
|
||||
articles_qs = ArticlePage.objects.filter(trending=True).live().order_by(
|
||||
"-date"
|
||||
)
|
||||
|
||||
# Exclude specified article IDs
|
||||
if exclude_ids:
|
||||
articles_qs = articles_qs.exclude(id__in=exclude_ids)
|
||||
|
||||
if not request:
|
||||
# No request means no pagination (e.g., homepage)
|
||||
return {
|
||||
"title": trending_page.title,
|
||||
"items": articles_qs[:HORIZON_SIZE],
|
||||
"url": trending_page.url,
|
||||
}
|
||||
else:
|
||||
# Paginated view
|
||||
paginator = Paginator(articles_qs, 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,
|
||||
}
|
||||
|
||||
|
||||
class HomePage(Page, CategoryMixin):
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
|
||||
sections = {
|
||||
"top_section": [],
|
||||
"category_sections": [],
|
||||
}
|
||||
|
||||
latest_section = self.get_latest_articles().copy()
|
||||
latest_section["layout"] = "block"
|
||||
sections["top_section"].append(latest_section)
|
||||
|
||||
# Exclude latest articles from trending section
|
||||
latest_items = latest_section.get("items", [])
|
||||
if hasattr(latest_items, "values_list"):
|
||||
latest_ids = list(latest_items.values_list("id", flat=True))
|
||||
else:
|
||||
latest_ids = [item.id for item in latest_items]
|
||||
|
||||
trending_section = self.get_trending_articles(
|
||||
exclude_ids=latest_ids
|
||||
).copy()
|
||||
trending_section["layout"] = "horizon"
|
||||
sections["top_section"].append(trending_section)
|
||||
|
||||
# Build category sections
|
||||
categories = CategoryPage.objects.child_of(self).live().in_menu()
|
||||
for category in categories:
|
||||
sections["category_sections"].append(
|
||||
{
|
||||
"title": category.title,
|
||||
"url": category.url,
|
||||
"items": ArticlePage.objects.descendant_of(category)
|
||||
.live()
|
||||
.order_by("-date")[:HORIZON_SIZE],
|
||||
"layout": "horizon",
|
||||
}
|
||||
)
|
||||
|
||||
context["sections"] = sections
|
||||
return context
|
||||
|
||||
|
||||
class LatestPage(Page, CategoryMixin):
|
||||
template = "home/category_page.html"
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
context["category_sections"] = [self.get_latest_articles(request)]
|
||||
breadcrumbs, site_root = self.build_breadcrumbs()
|
||||
context["breadcrumbs"] = breadcrumbs
|
||||
context["breadcrumb_root"] = site_root
|
||||
return context
|
||||
|
||||
|
||||
class TrendingPage(Page, CategoryMixin):
|
||||
template = "home/category_page.html"
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
context["category_sections"] = [self.get_trending_articles(request)]
|
||||
breadcrumbs, site_root = self.build_breadcrumbs()
|
||||
context["breadcrumbs"] = breadcrumbs
|
||||
context["breadcrumb_root"] = site_root
|
||||
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_sections"] = self.build_category_blocks(request)
|
||||
breadcrumbs, site_root = self.build_breadcrumbs()
|
||||
context["breadcrumbs"] = breadcrumbs
|
||||
context["breadcrumb_root"] = site_root
|
||||
return context
|
||||
|
||||
|
||||
from wagtail.admin.panels import FieldPanel
|
||||
from wagtail import blocks
|
||||
from wagtail.images.blocks import ImageChooserBlock
|
||||
from wagtail.fields import StreamField
|
||||
from .blocks import ValidatingEmbedBlock, H2HeadingBlock, HorizontalRuleBlock
|
||||
|
||||
# HashTag for Article
|
||||
class ArticlePageTag(TaggedItemBase):
|
||||
content_object = ParentalKey(
|
||||
"home.ArticlePage",
|
||||
related_name="tagged_items",
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
|
||||
class ArticlePage(Page):
|
||||
cover_image = models.ForeignKey(
|
||||
"wagtailimages.Image",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
help_text="列表封面圖",
|
||||
)
|
||||
banner_image = models.ForeignKey(
|
||||
"wagtailimages.Image",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="+",
|
||||
help_text="文章內文橫幅圖片",
|
||||
)
|
||||
date = models.DateTimeField("Published date")
|
||||
intro = models.CharField(max_length=250, blank=True)
|
||||
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,
|
||||
)
|
||||
trending = models.BooleanField("Trending", default=False, help_text="在熱門區塊顯示")
|
||||
tags = ClusterTaggableManager(through="home.ArticlePageTag", blank=True)
|
||||
|
||||
search_fields = Page.search_fields + [
|
||||
index.SearchField("intro", partial_match=True),
|
||||
index.SearchField("body_search_text", partial_match=True),
|
||||
index.SearchField("tag_names_search_text", partial_match=True),
|
||||
]
|
||||
|
||||
content_panels = Page.content_panels + [
|
||||
FieldPanel("trending"),
|
||||
FieldPanel("cover_image"),
|
||||
FieldPanel("banner_image"),
|
||||
FieldPanel("date"),
|
||||
FieldPanel("intro"),
|
||||
FieldPanel("body"),
|
||||
FieldPanel("tags"),
|
||||
]
|
||||
|
||||
def get_context(self, request):
|
||||
context = super().get_context(request)
|
||||
|
||||
tag_ids = list(self.tags.values_list("id", flat=True))
|
||||
if tag_ids:
|
||||
related_articles = (
|
||||
ArticlePage.objects.live()
|
||||
.exclude(id=self.id)
|
||||
.filter(tags__id__in=tag_ids)
|
||||
.distinct()
|
||||
.order_by("-date")[:4]
|
||||
)
|
||||
else:
|
||||
related_articles = ArticlePage.objects.none()
|
||||
|
||||
context["related_articles"] = related_articles
|
||||
return context
|
||||
|
||||
@property
|
||||
def body_search_text(self):
|
||||
if not self.body:
|
||||
return ""
|
||||
|
||||
excluded_types = {"image", "embed", "hr", "html"}
|
||||
chunks = []
|
||||
|
||||
for block in self.body:
|
||||
if block.block_type in excluded_types:
|
||||
continue
|
||||
# Each block decides how to expose searchable text
|
||||
block_content = block.block.get_searchable_content(block.value)
|
||||
if block_content:
|
||||
chunks.extend(block_content)
|
||||
|
||||
return " ".join(text for text in chunks if isinstance(text, str))
|
||||
|
||||
@property
|
||||
def tag_names_search_text(self):
|
||||
return " ".join(self.tags.values_list("name", flat=True))
|
||||
class HomePage(Page):
|
||||
pass
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 9.0 KiB |
@ -1,35 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load wagtailcore_tags wagtailimages_tags %}
|
||||
|
||||
{% block content %}
|
||||
<article>
|
||||
<h1>{{ page.title }}</h1>
|
||||
{% if page.banner_image %}
|
||||
{% image page.banner_image original as banner %}
|
||||
<img src="{{ banner.url }}" alt="{{ page.title }}">
|
||||
{% endif %}
|
||||
<p class="date">{{ page.date }}</p>
|
||||
<div class="intro">{{ page.intro }}</div>
|
||||
<div class="body">
|
||||
{{ page.body }}
|
||||
</div>
|
||||
{% with tags=page.tags.all %}
|
||||
{% if tags %}
|
||||
<div class="tags">
|
||||
<span>Hashtags:</span>
|
||||
<ul>
|
||||
{% for tag in tags %}
|
||||
<li><a href="{% url 'hashtag_search' tag.slug %}">#{{ tag }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
{% if related_articles %}
|
||||
<section class="related-articles">
|
||||
<h2>相關文章</h2>
|
||||
{% include "home/includes/article_list.html" with items=related_articles %}
|
||||
</section>
|
||||
{% endif %}
|
||||
</article>
|
||||
{% endblock %}
|
||||
@ -1 +0,0 @@
|
||||
<h2 class="article-heading">{{ value }}</h2>
|
||||
@ -1 +0,0 @@
|
||||
<hr class="article-hr">
|
||||
@ -1,31 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load wagtailcore_tags %}
|
||||
{% block content %}
|
||||
{% if breadcrumbs %}
|
||||
<nav class="breadcrumbs" aria-label="breadcrumb">
|
||||
<ol>
|
||||
{% if breadcrumb_root %}
|
||||
<li><a href="{{ breadcrumb_root.url }}">首頁</a></li>
|
||||
{% endif %}
|
||||
{% for crumb in breadcrumbs %}
|
||||
{% if not breadcrumb_root or crumb.id != breadcrumb_root.id %}
|
||||
<li>
|
||||
{% if crumb.id == page.id %}
|
||||
<span>{{ crumb.title }}</span>
|
||||
{% else %}
|
||||
<a href="{{ crumb.url }}">{{ crumb.title }}</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
{% endif %}
|
||||
{% if page.has_subcategories %}
|
||||
{% for section in category_sections %}
|
||||
{% include "home/includes/category_session.html" with section=section %}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% include "home/includes/page-article-list.html" with category=category_sections.0 %}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
@ -1,19 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% load wagtailcore_tags %}
|
||||
{% block content %}
|
||||
<nav class="breadcrumbs" aria-label="breadcrumb">
|
||||
<ol>
|
||||
<li>
|
||||
{% if site_root %}
|
||||
<a href="{{ site_root.url }}">首頁</a>
|
||||
{% else %}
|
||||
<a href="/">首頁</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li><span>標籤</span></li>
|
||||
<li><span>#{{ tag.name }}</span></li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
{% include "home/includes/page-article-list.html" %}
|
||||
{% endblock %}
|
||||
@ -1,20 +1,21 @@
|
||||
{% extends "base.html" %}
|
||||
{% block body_class %}template-homepage{% endblock %}
|
||||
{% block content %}
|
||||
{% with top_section=sections.top_section %}
|
||||
<h2>
|
||||
<a href="{{ top_section.0.url }}">最新文章</a>
|
||||
</h2>
|
||||
{% for section in top_section %}
|
||||
{% if section.layout == "block" %}
|
||||
{% include "home/includes/block_list.html" with items=section.items %}
|
||||
{% elif section.layout == "horizon" %}
|
||||
{% include "home/includes/horizontal_list.html" with items=section.items %}
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
{% load static %}
|
||||
|
||||
{% for section in sections.category_sections %}
|
||||
{% include "home/includes/category_session.html" with section=section %}
|
||||
{% endfor %}
|
||||
{% endblock content %}
|
||||
{% block body_class %}template-homepage{% endblock %}
|
||||
|
||||
{% 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 %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
{% comment %}
|
||||
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 %}
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
{% load wagtailimages_tags static %}
|
||||
|
||||
<ul class="article-list">
|
||||
{% for article in items %}
|
||||
<li>
|
||||
<article>
|
||||
<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>
|
||||
</article>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="empty">目前沒有文章</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -1,19 +0,0 @@
|
||||
{% load wagtailimages_tags static %}
|
||||
|
||||
<ul class="block-list">
|
||||
{% for article in 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>
|
||||
{% empty %}
|
||||
<li class="empty">目前沒有文章</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -1,17 +0,0 @@
|
||||
{% load wagtailimages_tags static %}
|
||||
|
||||
<section class="article-section article-section--{{ section.layout }}">
|
||||
<h2>
|
||||
{% if section.url %}
|
||||
<a href="{{ section.url }}">{{ section.title }}</a>
|
||||
{% else %}
|
||||
{{ section.title }}
|
||||
{% endif %}
|
||||
</h2>
|
||||
{% if section.layout == "block" %}
|
||||
{% include "home/includes/block_list.html" with items=section.items %}
|
||||
{% elif section.layout == "horizon" %}
|
||||
{% include "home/includes/horizontal_list.html" with items=section.items %}
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
{% load wagtailimages_tags static %}
|
||||
|
||||
<ul class="horizontal-list">
|
||||
{% for article in 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:cover;display:block;"/>
|
||||
{% else %}
|
||||
<img src="{% static 'img/default_cover.jpg' %}" alt="{{ article.title }}" height="200" width="200" style="width:200px;height:200px;object-fit:cover;display:block;"/>
|
||||
{% endif %}
|
||||
<span>{{ article.title }}</span>
|
||||
</a>
|
||||
</li>
|
||||
{% empty %}
|
||||
<li class="empty">目前沒有文章</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
@ -1,22 +0,0 @@
|
||||
{% load wagtailimages_tags static %}
|
||||
|
||||
<div class="page-article-list">
|
||||
{% with category=category_sections.0 %}
|
||||
<h2><a href="{{ category.url }}">{{ category.title }}</a></h2>
|
||||
|
||||
{% include "home/includes/article_list.html" with items=category.items %}
|
||||
|
||||
{% 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>
|
||||
52
innovedus_cms/home/templates/home/welcome_page.html
Normal file
52
innovedus_cms/home/templates/home/welcome_page.html
Normal file
@ -0,0 +1,52 @@
|
||||
{% load i18n wagtailcore_tags %}
|
||||
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<a href="https://wagtail.org/">
|
||||
<svg class="figure-logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 342.5 126.2"><title>{% trans "Visit the Wagtail website" %}</title><path fill="#FFF" d="M84 1.9v5.7s-10.2-3.8-16.8 3.1c-4.8 5-5.2 10.6-3 18.1 21.6 0 25 12.1 25 12.1L87 27l6.8-8.3c0-9.8-8.1-16.3-9.8-16.8z"/><circle cx="85.9" cy="15.9" r="2.6"/><path d="M89.2 40.9s-3.3-16.6-24.9-12.1c-2.2-7.5-1.8-13 3-18.1C73.8 3.8 84 7.6 84 7.6V1.9C80.4.3 77 0 73.2 0 59.3 0 51.6 10.4 48.3 17.4L9.2 89.3l11-2.1-20.2 39 14.1-2.5L24.9 93c30.6 0 69.8-11 64.3-52.1z"/><path d="M102.4 27l-8.6-8.3L87 27z"/><path fill="#FFF" d="M30 84.1s1-.2 2.8-.6c1.8-.4 4.3-1 7.3-1.8 1.5-.4 3.1-.9 4.8-1.5 1.7-.6 3.5-1.2 5.2-2 1.8-.7 3.6-1.6 5.4-2.6 1.8-1 3.5-2.1 5.1-3.4.4-.3.8-.6 1.2-1l1.2-1c.7-.7 1.5-1.4 2.2-2.2.7-.7 1.3-1.5 1.9-2.3l.9-1.2.4-.6.4-.6c.2-.4.5-.8.7-1.2.2-.4.4-.8.7-1.2l.3-.6.3-.6c.2-.4.4-.8.5-1.2l.9-2.4c.2-.8.5-1.6.7-2.3.2-.7.3-1.5.5-2.1.1-.7.2-1.3.3-2 .1-.6.2-1.2.2-1.7.1-.5.1-1 .2-1.5.1-1.8.1-2.8.1-2.8l1.6.1s-.1 1.1-.2 2.9c-.1.5-.1 1-.2 1.5-.1.6-.1 1.2-.3 1.8-.1.6-.3 1.3-.4 2-.2.7-.4 1.4-.6 2.2-.2.8-.5 1.5-.8 2.4-.3.8-.6 1.6-1 2.5l-.6 1.2-.3.6-.3.6c-.2.4-.5.8-.7 1.3-.3.4-.5.8-.8 1.2-.1.2-.3.4-.4.6l-.4.6-.9 1.2c-.7.8-1.3 1.6-2.1 2.3-.7.8-1.5 1.4-2.3 2.2l-1.2 1c-.4.3-.8.6-1.3.9-1.7 1.2-3.5 2.3-5.3 3.3-1.8.9-3.7 1.8-5.5 2.5-1.8.7-3.6 1.3-5.3 1.8-1.7.5-3.3 1-4.9 1.3-3 .7-5.6 1.3-7.4 1.6-1.6.6-2.6.8-2.6.8z"/><g fill="#231F20"><path d="M127 83.9h-8.8l-12.6-36.4h7.9l9 27.5 9-27.5h7.9l9 27.5 9-27.5h7.9L153 83.9h-8.8L135.6 59 127 83.9zM200.1 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM241.7 47.5v31.7c0 6.4-1.7 11.3-5.2 14.5-3.5 3.2-8 4.8-13.4 4.8-5.5 0-10.4-1.7-14.8-5.1l3.6-5.8c3.6 2.7 7.1 4 10.8 4 3.6 0 6.5-.9 8.6-2.8 2.1-1.9 3.2-4.9 3.2-9v-4.7c-1.1 2.1-2.8 3.9-4.9 5.1-2.1 1.3-4.5 1.9-7.1 1.9-4.8 0-8.8-1.7-11.9-5.1-3.1-3.4-4.7-7.6-4.7-12.6s1.6-9.2 4.7-12.6c3.1-3.4 7.1-5.1 11.9-5.1 4.8 0 8.7 2 11.7 6v-5.4h7.5zm-28.4 16.8c0 3 .9 5.6 2.8 7.7 1.8 2.2 4.3 3.2 7.5 3.2 3.1 0 5.7-1 7.6-3.1 1.9-2.1 2.9-4.7 2.9-7.8 0-3.1-1-5.8-2.9-7.9-2-2.2-4.5-3.2-7.6-3.2-3.1 0-5.6 1.1-7.4 3.4-2 2.1-2.9 4.7-2.9 7.7zM260.9 53.6v18.5c0 1.7.5 3.1 1.4 4.1.9 1 2.2 1.5 3.8 1.5 1.6 0 3.2-.8 4.7-2.4l3.1 5.4c-2.7 2.4-5.7 3.6-8.9 3.6-3.3 0-6-1.1-8.3-3.4-2.3-2.3-3.5-5.3-3.5-9.1V53.6h-4.6v-6.2h4.6V36.1h7.7v11.4h9.6v6.2h-9.6zM309.5 83.9h-7V79c-3 3.6-7 5.4-12.1 5.4-3.8 0-6.9-1.1-9.4-3.2s-3.7-5-3.7-8.6c0-3.6 1.3-6.3 4-8 2.6-1.8 6.2-2.7 10.7-2.7h9.9v-1.4c0-4.8-2.7-7.3-8.1-7.3-3.4 0-6.9 1.2-10.5 3.7l-3.4-4.8c4.4-3.5 9.4-5.3 15.1-5.3 4.3 0 7.8 1.1 10.5 3.2 2.7 2.2 4.1 5.6 4.1 10.2v23.7zm-7.7-13.6v-3.1h-8.6c-5.5 0-8.3 1.7-8.3 5.2 0 1.8.7 3.1 2.1 4.1 1.4.9 3.3 1.4 5.7 1.4 2.4 0 4.6-.7 6.4-2.1 1.8-1.3 2.7-3.1 2.7-5.5zM319.3 40.2c-1-1-1.4-2.1-1.4-3.4 0-1.3.5-2.5 1.4-3.4 1-1 2.1-1.4 3.4-1.4 1.3 0 2.5.5 3.4 1.4 1 1 1.4 2.1 1.4 3.4 0 1.3-.5 2.5-1.4 3.4s-2.1 1.4-3.4 1.4c-1.3.1-2.4-.4-3.4-1.4zm7.2 43.7h-7.7V47.5h7.7v36.4zM342.5 83.9h-7.7V33.1h7.7v50.8z"/></g></svg>
|
||||
</a>
|
||||
</div>
|
||||
<div class="header-link">
|
||||
{% comment %}
|
||||
This works for all cases but prerelease versions:
|
||||
{% endcomment %}
|
||||
<a href="{% wagtail_documentation_path %}/releases/{% wagtail_release_notes_path %}">
|
||||
{% trans "View the release notes" %}
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
<main class="main">
|
||||
<div class="figure">
|
||||
<svg class="figure-space" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300" aria-hidden="true">
|
||||
<path class="egg" fill="currentColor" d="M150 250c-42.741 0-75-32.693-75-90s42.913-110 75-110c32.088 0 75 52.693 75 110s-32.258 90-75 90z"/>
|
||||
<ellipse fill="#ddd" cx="150" cy="270" rx="40" ry="7"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="main-text">
|
||||
<h1>{% trans "Welcome to your new Wagtail site!" %}</h1>
|
||||
<p>{% trans 'Please feel free to <a href="https://github.com/wagtail/wagtail/wiki/Slack">join our community on Slack</a>, or get started with one of the links below.' %}</p>
|
||||
</div>
|
||||
</main>
|
||||
<footer class="footer" role="contentinfo">
|
||||
<a class="option option-one" href="{% wagtail_documentation_path %}/">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 21c0 .5.4 1 1 1h4c.6 0 1-.5 1-1v-1H9v1zm3-19C8.1 2 5 5.1 5 9c0 2.4 1.2 4.5 3 5.7V17c0 .5.4 1 1 1h6c.6 0 1-.5 1-1v-2.3c1.8-1.3 3-3.4 3-5.7 0-3.9-3.1-7-7-7zm2.9 11.1l-.9.6V16h-4v-2.3l-.9-.6C7.8 12.2 7 10.6 7 9c0-2.8 2.2-5 5-5s5 2.2 5 5c0 1.6-.8 3.2-2.1 4.1z"/></svg>
|
||||
<div>
|
||||
<h2>{% trans "Wagtail Documentation" %}</h2>
|
||||
<p>{% trans "Topics, references, & how-tos" %}</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="option option-two" href="{% wagtail_documentation_path %}/getting_started/tutorial.html">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
|
||||
<div>
|
||||
<h2>{% trans "Tutorial" %}</h2>
|
||||
<p>{% trans "Build your first Wagtail site" %}</p>
|
||||
</div>
|
||||
</a>
|
||||
<a class="option option-three" href="{% url 'wagtailadmin_home' %}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" aria-hidden="true"><path d="M0 0h24v24H0z" fill="none"/><path d="M16.5 13c-1.2 0-3.07.34-4.5 1-1.43-.67-3.3-1-4.5-1C5.33 13 1 14.08 1 16.25V19h22v-2.75c0-2.17-4.33-3.25-6.5-3.25zm-4 4.5h-10v-1.25c0-.54 2.56-1.75 5-1.75s5 1.21 5 1.75v1.25zm9 0H14v-1.25c0-.46-.2-.86-.52-1.22.88-.3 1.96-.53 3.02-.53 2.44 0 5 1.21 5 1.75v1.25zM7.5 12c1.93 0 3.5-1.57 3.5-3.5S9.43 5 7.5 5 4 6.57 4 8.5 5.57 12 7.5 12zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2zm9 5.5c1.93 0 3.5-1.57 3.5-3.5S18.43 5 16.5 5 13 6.57 13 8.5s1.57 3.5 3.5 3.5zm0-5.5c1.1 0 2 .9 2 2s-.9 2-2 2-2-.9-2-2 .9-2 2-2z"/></svg>
|
||||
<div>
|
||||
<h2>{% trans "Admin Interface" %}</h2>
|
||||
<p>{% trans "Create your superuser first!" %}</p>
|
||||
</div>
|
||||
</a>
|
||||
</footer>
|
||||
@ -1 +0,0 @@
|
||||
|
||||
@ -1,44 +0,0 @@
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from taggit.models import Tag
|
||||
from wagtail.models import Site
|
||||
|
||||
from .models import ArticlePage, PAGE_SIZE
|
||||
|
||||
|
||||
def hashtag_search(request, slug):
|
||||
tag = get_object_or_404(Tag, slug=slug)
|
||||
articles = (
|
||||
ArticlePage.objects.live()
|
||||
.filter(tags__slug=slug)
|
||||
.order_by("-date")
|
||||
)
|
||||
|
||||
paginator = Paginator(articles, 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)
|
||||
|
||||
site = Site.find_for_request(request)
|
||||
site_root = site.root_page if site else None
|
||||
|
||||
context = {
|
||||
"tag": tag,
|
||||
"category_sections": [
|
||||
{
|
||||
"title": f"#{tag.name}",
|
||||
"items": page_obj,
|
||||
"url": request.path,
|
||||
}
|
||||
],
|
||||
"site_root": site_root,
|
||||
"page": site_root.specific if site_root else None,
|
||||
}
|
||||
|
||||
return render(request, "home/hashtag_page.html", context)
|
||||
@ -3,10 +3,6 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
from dotenv import load_dotenv
|
||||
|
||||
env_file = os.environ.get("ENV_FILE", "../.env")
|
||||
load_dotenv(env_file)
|
||||
|
||||
def main():
|
||||
"""Run administrative tasks."""
|
||||
|
||||
@ -16,15 +16,6 @@ import os
|
||||
PROJECT_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
BASE_DIR = os.path.dirname(PROJECT_DIR)
|
||||
|
||||
def env_list(name, default):
|
||||
"""
|
||||
Return a list from a comma-separated env var; fall back to provided default list.
|
||||
"""
|
||||
value = os.environ.get(name)
|
||||
if value:
|
||||
return [item.strip() for item in value.split(",") if item.strip()]
|
||||
return default
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/
|
||||
@ -37,7 +28,6 @@ INSTALLED_APPS = [
|
||||
"search",
|
||||
"wagtail.contrib.forms",
|
||||
"wagtail.contrib.redirects",
|
||||
"wagtail.contrib.settings",
|
||||
"wagtail.embeds",
|
||||
"wagtail.sites",
|
||||
"wagtail.users",
|
||||
@ -56,7 +46,6 @@ INSTALLED_APPS = [
|
||||
"django.contrib.sessions",
|
||||
"django.contrib.messages",
|
||||
"django.contrib.staticfiles",
|
||||
"base",
|
||||
]
|
||||
|
||||
MIDDLEWARE = [
|
||||
@ -85,8 +74,6 @@ TEMPLATES = [
|
||||
"django.template.context_processors.request",
|
||||
"django.contrib.auth.context_processors.auth",
|
||||
"django.contrib.messages.context_processors.messages",
|
||||
"wagtail.contrib.settings.context_processors.settings",
|
||||
"home.context_processors.navigation_pages",
|
||||
],
|
||||
},
|
||||
},
|
||||
@ -134,28 +121,6 @@ USE_I18N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
# --- Wagtail embeds (Instagram/FB via Graph oEmbed) ---
|
||||
# Reads a token from env and adds an extra oEmbed finder for IG/FB.
|
||||
# Keeps the default oEmbed finder first for YouTube/Vimeo/etc.
|
||||
WAGTAIL_EMBED_FINDERS = [
|
||||
{"class": "wagtail.embeds.finders.oembed"},
|
||||
{
|
||||
"class": "wagtail.embeds.finders.oembed",
|
||||
"options": {
|
||||
"providers": [
|
||||
{"endpoint": "https://graph.facebook.com/v11.0/instagram_oembed", "urls": ["https://www.instagram.com/*"]},
|
||||
{"endpoint": "https://graph.facebook.com/v11.0/oembed_post", "urls": ["https://www.facebook.com/*"]},
|
||||
{"endpoint": "https://graph.facebook.com/v11.0/oembed_page", "urls": ["https://www.facebook.com/*"]},
|
||||
{"endpoint": "https://graph.facebook.com/v11.0/oembed_video", "urls": ["https://www.facebook.com/*"]},
|
||||
],
|
||||
"params": {
|
||||
"access_token": os.environ.get("IG_OEMBED_ACCESS_TOKEN", ""),
|
||||
"omitscript": True,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
LANGUAGES = [
|
||||
@ -179,30 +144,19 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static")
|
||||
STATIC_URL = "/static/"
|
||||
|
||||
MEDIA_ROOT = os.path.join(BASE_DIR, "media")
|
||||
MEDIA_URL = f'{os.environ.get("AWS_S3_ENDPOINT_URL")}/{os.environ.get("AWS_STORAGE_BUCKET_NAME")}/'
|
||||
MEDIA_URL = "/media/"
|
||||
|
||||
# Default storage settings
|
||||
# See https://docs.djangoproject.com/en/5.2/ref/settings/#std-setting-STORAGES
|
||||
STORAGES = {
|
||||
"default": {
|
||||
"BACKEND": "storages.backends.s3boto3.S3Boto3Storage",
|
||||
"OPTIONS": {
|
||||
"endpoint_url": os.environ.get("AWS_S3_ENDPOINT_URL"),
|
||||
"access_key": os.environ.get("AWS_ACCESS_KEY_ID"),
|
||||
"secret_key": os.environ.get("AWS_SECRET_ACCESS_KEY"),
|
||||
"bucket_name": os.environ.get("AWS_STORAGE_BUCKET_NAME"),
|
||||
"region_name": os.environ.get("AWS_S3_REGION_NAME", default="us-east-1"),
|
||||
"addressing_style": "path",
|
||||
},
|
||||
"BACKEND": "django.core.files.storage.FileSystemStorage",
|
||||
},
|
||||
"staticfiles": {
|
||||
"BACKEND": "django.contrib.staticfiles.storage.StaticFilesStorage",
|
||||
},
|
||||
}
|
||||
|
||||
# Avoid overwriting user uploads when using S3 storage unless explicitly enabled via env
|
||||
AWS_S3_FILE_OVERWRITE = os.environ.get("AWS_S3_FILE_OVERWRITE", "False").lower() == "true"
|
||||
|
||||
# Django sets a maximum of 1000 fields per form by default, but particularly complex page models
|
||||
# can exceed this limit within Wagtail's page editor.
|
||||
DATA_UPLOAD_MAX_NUMBER_FIELDS = 10_000
|
||||
@ -230,10 +184,8 @@ WAGTAILADMIN_BASE_URL = "http://example.com"
|
||||
# see https://docs.wagtail.org/en/stable/advanced_topics/deploying.html#user-uploaded-files
|
||||
WAGTAILDOCS_EXTENSIONS = ['csv', 'docx', 'key', 'odt', 'pdf', 'pptx', 'rtf', 'txt', 'xlsx', 'zip']
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = env_list(
|
||||
"CSRF_TRUSTED_ORIGINS"
|
||||
)
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'https://innovedus-cms.fly.dev',
|
||||
]
|
||||
|
||||
ALLOWED_HOSTS = env_list(
|
||||
"ALLOWED_HOSTS"
|
||||
)
|
||||
ALLOWED_HOSTS = ['innovedus-cms.fly.dev']
|
||||
@ -34,16 +34,10 @@
|
||||
<body class="{% block body_class %}{% endblock %}">
|
||||
{% wagtailuserbar %}
|
||||
|
||||
{% include "includes/header.html" %}
|
||||
|
||||
{% block content %}{% endblock %}
|
||||
|
||||
{% include "includes/footer.html" %}
|
||||
|
||||
{# Global javascript #}
|
||||
<script type="text/javascript" src="{% static 'js/mysite.js' %}"></script>
|
||||
{# Instagram embed script to render IG oEmbeds #}
|
||||
<script async src="https://www.instagram.com/embed.js"></script>
|
||||
|
||||
{% block extra_js %}
|
||||
{# Override this in templates to add extra javascript #}
|
||||
|
||||
@ -1,32 +0,0 @@
|
||||
{% load navigation_tags %}
|
||||
|
||||
<footer>
|
||||
{% if settings.base.NavigationSettings.footer_links %}
|
||||
<div class="footer-sections">
|
||||
{% for section in settings.base.NavigationSettings.footer_links %}
|
||||
<div class="footer-section">
|
||||
{% if section.value.title %}
|
||||
<h3>{{ section.value.title }}</h3>
|
||||
{% endif %}
|
||||
<ul>
|
||||
{% for link in section.value.links %}
|
||||
<li><a href="{{ link.url }}" target="_blank">{{ link.label }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% with social_links=settings.base.SocialMediaSettings.links %}
|
||||
{% if social_links %}
|
||||
<p>Follow us
|
||||
{% for item in social_links %}
|
||||
<a href="{{ item.value.url }}" target="_blank" alt="{{ item.value.platform }}">{{ item.value.platform }}</a>
|
||||
{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
|
||||
{% get_footer_text %}
|
||||
</footer>
|
||||
@ -1,70 +0,0 @@
|
||||
{% load wagtailsettings_tags wagtailimages_tags %}
|
||||
{% get_settings use_default_site=True as settings %}
|
||||
|
||||
<header class="site-header">
|
||||
<div class="header-inner">
|
||||
{% if settings.base.HeaderSettings.logo %}
|
||||
<a href="/" class="logo">
|
||||
{% image settings.base.HeaderSettings.logo fill-60x60 %}
|
||||
{% if settings.base.HeaderSettings.site_name %}
|
||||
<span class="site-name">{{ settings.base.HeaderSettings.site_name }}</span>
|
||||
{% endif %}
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<nav class="main-nav">
|
||||
<ul>
|
||||
{% with site_root=page.get_site.root_page %}
|
||||
{# Top-level menu: direct children of site root #}
|
||||
<li>
|
||||
<a href="#">
|
||||
最新文章
|
||||
</a>
|
||||
{% if nav_latest_page or nav_trending_page %}
|
||||
<ul class="submenu">
|
||||
{% if nav_latest_page %}
|
||||
<li><a href="{{ nav_latest_page.url }}">{{ nav_latest_page.title }}</a></li>
|
||||
{% endif %}
|
||||
{% if nav_trending_page %}
|
||||
<li><a href="{{ nav_trending_page.url }}">{{ nav_trending_page.title }}</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</li>
|
||||
{% for menu_page in site_root.get_children.live.in_menu %}
|
||||
<li>
|
||||
<a href="{{ menu_page.url }}">{{ menu_page.title }}</a>
|
||||
{# Second-level: direct children of each top-level page #}
|
||||
{% with submenu=menu_page.get_children.live.in_menu %}
|
||||
{% if submenu %}
|
||||
<ul class="submenu">
|
||||
{% for subpage in submenu %}
|
||||
<li><a href="{{ subpage.url }}">{{ subpage.title }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
{% endwith %}
|
||||
|
||||
{# Optional extra links from settings #}
|
||||
{% if settings.base.HeaderSettings.main_links %}
|
||||
{% for item in settings.base.HeaderSettings.main_links %}
|
||||
<li><a href="{{ item.value.url }}">{{ item.value.label }}</a></li>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</ul>
|
||||
</nav>
|
||||
|
||||
<form class="header-search" action="{% url 'search' %}" method="get" role="search">
|
||||
<input
|
||||
type="search"
|
||||
name="query"
|
||||
placeholder="搜尋文章"
|
||||
value="{{ request.GET.query|default:'' }}"
|
||||
aria-label="搜尋文章">
|
||||
<button type="submit">搜尋</button>
|
||||
</form>
|
||||
</div>
|
||||
</header>
|
||||
@ -7,14 +7,11 @@ from wagtail import urls as wagtail_urls
|
||||
from wagtail.documents import urls as wagtaildocs_urls
|
||||
|
||||
from search import views as search_views
|
||||
from home import views as home_views
|
||||
|
||||
urlpatterns = [
|
||||
path("django-admin/", admin.site.urls),
|
||||
path("admin/", include(wagtailadmin_urls)),
|
||||
path("documents/", include(wagtaildocs_urls)),
|
||||
# use <str:slug> so Unicode tag slugs (e.g. 台北美食) still resolve
|
||||
path("tags/<str:slug>/", home_views.hashtag_search, name="hashtag_search"),
|
||||
path("search/", search_views.search, name="search"),
|
||||
]
|
||||
|
||||
|
||||
@ -2,6 +2,4 @@ Django>=5.2,<5.3
|
||||
wagtail>=7.1,<7.2
|
||||
gunicorn
|
||||
dj-database-url
|
||||
psycopg[binary]
|
||||
python-dotenv
|
||||
django-storages[boto3]
|
||||
psycopg[binary]
|
||||
@ -1,38 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
{% load wagtailcore_tags %}
|
||||
{% load static wagtailcore_tags %}
|
||||
|
||||
{% block body_class %}template-searchresults{% endblock %}
|
||||
|
||||
{% block title %}
|
||||
{% if search_query %}搜尋:{{ search_query }}{% else %}搜尋{% endif %}
|
||||
{% endblock %}
|
||||
{% block title %}Search{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<nav class="breadcrumbs" aria-label="breadcrumb">
|
||||
<ol>
|
||||
<li>
|
||||
{% if site_root %}
|
||||
<a href="{{ site_root.url }}">首頁</a>
|
||||
{% else %}
|
||||
<a href="/">首頁</a>
|
||||
{% endif %}
|
||||
</li>
|
||||
<li><span>搜尋</span></li>
|
||||
{% if search_query %}
|
||||
<li><span>{{ search_query }}</span></li>
|
||||
{% endif %}
|
||||
</ol>
|
||||
</nav>
|
||||
<h1>Search</h1>
|
||||
|
||||
<section class="search-results">
|
||||
{% if search_query %}
|
||||
{% if results_count %}
|
||||
{% include "home/includes/page-article-list.html" %}
|
||||
{% else %}
|
||||
<p>找不到與「{{ search_query }}」相關的文章。</p>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<p>請輸入關鍵字後再進行搜尋。</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
<form action="{% url 'search' %}" method="get">
|
||||
<input type="text" name="query"{% if search_query %} value="{{ search_query }}"{% endif %}>
|
||||
<input type="submit" value="Search" class="button">
|
||||
</form>
|
||||
|
||||
{% if search_results %}
|
||||
<ul>
|
||||
{% for result in search_results %}
|
||||
<li>
|
||||
<h4><a href="{% pageurl result %}">{{ result }}</a></h4>
|
||||
{% if result.search_description %}
|
||||
{{ result.search_description }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% if search_results.has_previous %}
|
||||
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.previous_page_number }}">Previous</a>
|
||||
{% endif %}
|
||||
|
||||
{% if search_results.has_next %}
|
||||
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&page={{ search_results.next_page_number }}">Next</a>
|
||||
{% endif %}
|
||||
{% elif search_query %}
|
||||
No results found
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -1,54 +1,46 @@
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from django.core.paginator import Paginator
|
||||
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
|
||||
from django.template.response import TemplateResponse
|
||||
from django.db.models import Q
|
||||
|
||||
from wagtail.models import Site
|
||||
from wagtail.models import Page
|
||||
|
||||
from home.models import ArticlePage, PAGE_SIZE
|
||||
# To enable logging of search queries for use with the "Promoted search results" module
|
||||
# <https://docs.wagtail.org/en/stable/reference/contrib/searchpromotions.html>
|
||||
# uncomment the following line and the lines indicated in the search function
|
||||
# (after adding wagtail.contrib.search_promotions to INSTALLED_APPS):
|
||||
|
||||
# from wagtail.contrib.search_promotions.models import Query
|
||||
|
||||
|
||||
def search(request):
|
||||
search_query = (request.GET.get("query") or "").strip()
|
||||
page_number = request.GET.get("page", 1)
|
||||
category_sections = []
|
||||
results_page = None
|
||||
results_count = 0
|
||||
search_query = request.GET.get("query", None)
|
||||
page = request.GET.get("page", 1)
|
||||
|
||||
# Search
|
||||
if search_query:
|
||||
primary_qs = ArticlePage.objects.live().search(search_query)
|
||||
results_count = primary_qs.count()
|
||||
search_results = Page.objects.live().search(search_query)
|
||||
|
||||
if not results_count:
|
||||
fallback_filter = Q(intro__icontains=search_query) | Q(body__icontains=search_query)
|
||||
primary_qs = ArticlePage.objects.live().filter(fallback_filter).order_by("-date")
|
||||
results_count = primary_qs.count()
|
||||
# To log this query for use with the "Promoted search results" module:
|
||||
|
||||
if results_count:
|
||||
paginator = Paginator(primary_qs, PAGE_SIZE)
|
||||
results_page = paginator.get_page(page_number)
|
||||
query_string = urlencode({"query": search_query})
|
||||
category_sections = [
|
||||
{
|
||||
"title": f"搜尋:{search_query}",
|
||||
"items": results_page,
|
||||
"url": f"{request.path}?{query_string}",
|
||||
}
|
||||
]
|
||||
# query = Query.get(search_query)
|
||||
# query.add_hit()
|
||||
|
||||
site = Site.find_for_request(request)
|
||||
site_root = site.root_page if site else None
|
||||
else:
|
||||
search_results = Page.objects.none()
|
||||
|
||||
# Pagination
|
||||
paginator = Paginator(search_results, 10)
|
||||
try:
|
||||
search_results = paginator.page(page)
|
||||
except PageNotAnInteger:
|
||||
search_results = paginator.page(1)
|
||||
except EmptyPage:
|
||||
search_results = paginator.page(paginator.num_pages)
|
||||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
"search/search.html",
|
||||
{
|
||||
"search_query": search_query,
|
||||
"category_sections": category_sections,
|
||||
"results_page": results_page,
|
||||
"results_count": results_count,
|
||||
"site_root": site_root,
|
||||
"page": site_root.specific if site_root else None,
|
||||
"search_results": search_results,
|
||||
},
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user