Compare commits

..

2 Commits

Author SHA1 Message Date
Warren Chen
653847df6a Add search functionality to ArticlePage and enhance search templates
- Implement search fields in ArticlePage model for indexing.
- Update hashtag search view to include site root in context.
- Enhance header with a search form for articles.
- Modify search results template to improve user experience and display.
2025-11-10 16:42:15 +09:00
Warren Chen
a98d36da14 Add hashtag search functionality and create hashtag page template 2025-11-10 15:39:43 +09:00
8 changed files with 165 additions and 58 deletions

View File

@ -7,6 +7,7 @@ from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from modelcluster.contrib.taggit import ClusterTaggableManager from modelcluster.contrib.taggit import ClusterTaggableManager
from modelcluster.fields import ParentalKey from modelcluster.fields import ParentalKey
from taggit.models import TaggedItemBase from taggit.models import TaggedItemBase
from wagtail.search import index
def _get_env_int(name, default): def _get_env_int(name, default):
value = os.environ.get(name) value = os.environ.get(name)
@ -272,6 +273,12 @@ class ArticlePage(Page):
trending = models.BooleanField("Trending", default=False, help_text="在熱門區塊顯示") trending = models.BooleanField("Trending", default=False, help_text="在熱門區塊顯示")
tags = ClusterTaggableManager(through="home.ArticlePageTag", blank=True) 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 + [ content_panels = Page.content_panels + [
FieldPanel("trending"), FieldPanel("trending"),
FieldPanel("cover_image"), FieldPanel("cover_image"),
@ -299,3 +306,25 @@ class ArticlePage(Page):
context["related_articles"] = related_articles context["related_articles"] = related_articles
return context 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))

View File

@ -19,7 +19,7 @@
<span>Hashtags:</span> <span>Hashtags:</span>
<ul> <ul>
{% for tag in tags %} {% for tag in tags %}
<li>#{{ tag }}</li> <li><a href="{% url 'hashtag_search' tag.slug %}">#{{ tag }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@ -0,0 +1,19 @@
{% 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 %}

View File

@ -0,0 +1,44 @@
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)

View File

@ -56,5 +56,15 @@
{% endif %} {% endif %}
</ul> </ul>
</nav> </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> </div>
</header> </header>

View File

@ -7,11 +7,14 @@ from wagtail import urls as wagtail_urls
from wagtail.documents import urls as wagtaildocs_urls from wagtail.documents import urls as wagtaildocs_urls
from search import views as search_views from search import views as search_views
from home import views as home_views
urlpatterns = [ urlpatterns = [
path("django-admin/", admin.site.urls), path("django-admin/", admin.site.urls),
path("admin/", include(wagtailadmin_urls)), path("admin/", include(wagtailadmin_urls)),
path("documents/", include(wagtaildocs_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"), path("search/", search_views.search, name="search"),
] ]

View File

@ -1,38 +1,38 @@
{% extends "base.html" %} {% extends "base.html" %}
{% load static wagtailcore_tags %} {% load wagtailcore_tags %}
{% block body_class %}template-searchresults{% endblock %} {% block body_class %}template-searchresults{% endblock %}
{% block title %}Search{% endblock %} {% block title %}
{% if search_query %}搜尋:{{ search_query }}{% else %}搜尋{% endif %}
{% endblock %}
{% block content %} {% block content %}
<h1>Search</h1> <nav class="breadcrumbs" aria-label="breadcrumb">
<ol>
<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> <li>
<h4><a href="{% pageurl result %}">{{ result }}</a></h4> {% if site_root %}
{% if result.search_description %} <a href="{{ site_root.url }}">首頁</a>
{{ result.search_description }} {% else %}
<a href="/">首頁</a>
{% endif %} {% endif %}
</li> </li>
{% endfor %} <li><span>搜尋</span></li>
</ul> {% if search_query %}
<li><span>{{ search_query }}</span></li>
{% endif %}
</ol>
</nav>
{% if search_results.has_previous %} <section class="search-results">
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.previous_page_number }}">Previous</a> {% if search_query %}
{% if results_count %}
{% include "home/includes/page-article-list.html" %}
{% else %}
<p>找不到與「{{ search_query }}」相關的文章。</p>
{% endif %} {% endif %}
{% else %}
{% if search_results.has_next %} <p>請輸入關鍵字後再進行搜尋。</p>
<a href="{% url 'search' %}?query={{ search_query|urlencode }}&amp;page={{ search_results.next_page_number }}">Next</a>
{% endif %}
{% elif search_query %}
No results found
{% endif %} {% endif %}
</section>
{% endblock %} {% endblock %}

View File

@ -1,46 +1,48 @@
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator from urllib.parse import urlencode
from django.core.paginator import Paginator
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from wagtail.models import Page from wagtail.models import Site
# To enable logging of search queries for use with the "Promoted search results" module from home.models import ArticlePage, PAGE_SIZE
# <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): def search(request):
search_query = request.GET.get("query", None) search_query = (request.GET.get("query") or "").strip()
page = request.GET.get("page", 1) page_number = request.GET.get("page", 1)
category_sections = []
results_page = None
results_count = 0
# Search
if search_query: if search_query:
search_results = Page.objects.live().search(search_query) search_queryset = ArticlePage.objects.live().search(search_query)
paginator = Paginator(search_queryset, PAGE_SIZE)
results_page = paginator.get_page(page_number)
results_count = paginator.count
# To log this query for use with the "Promoted search results" module: if results_count:
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) site = Site.find_for_request(request)
# query.add_hit() 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( return TemplateResponse(
request, request,
"search/search.html", "search/search.html",
{ {
"search_query": search_query, "search_query": search_query,
"search_results": search_results, "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,
}, },
) )