website/website/search/models.py

97 lines
3.2 KiB
Python

from django.core.paginator import EmptyPage, Paginator
from django.db import models
from django.http.request import HttpRequest
from django.http.response import Http404, HttpResponse, HttpResponseBadRequest
from django.template.response import TemplateResponse
from django.utils.decorators import method_decorator
from django.utils.functional import cached_property
from django.views.decorators.http import require_GET
from wagtail.contrib.routable_page.models import RoutablePageMixin, route
from wagtail.models import Page
from wagtail.search.utils import parse_query_string
from website.common.models import BaseContentPage, BaseListingPage
from website.common.utils import get_page_models
from .serializers import MIN_SEARCH_LENGTH, SearchPageParamsSerializer
class SearchPage(RoutablePageMixin, BaseContentPage):
max_count = 1
subpage_types: list = []
parent_page_types = ["home.HomePage"]
PAGE_SIZE = 12
# Exclude singleton pages from search results
EXCLUDED_PAGE_TYPES = {
*(page for page in get_page_models() if page.max_count == 1),
BaseListingPage,
}
@cached_property
def show_reading_time(self) -> bool:
return False
@cached_property
def show_table_of_contents(self) -> bool:
return False
def get_context(self, request: HttpRequest) -> dict:
context = super().get_context(request)
context["search_query"] = request.GET.get("q", "")
context["search_url"] = self.reverse_subpage("results")
context["MIN_SEARCH_LENGTH"] = MIN_SEARCH_LENGTH
context["SEO_INDEX"] = False
return context
@classmethod
def get_listing_pages(cls) -> models.QuerySet:
return (
Page.objects.live()
.public()
.not_type(cls.__class__, *cls.EXCLUDED_PAGE_TYPES)
)
@route(r"^results/$")
@method_decorator(require_GET)
def results(self, request: HttpRequest) -> HttpResponse:
if not request.htmx:
return HttpResponseBadRequest()
serializer = SearchPageParamsSerializer(data=request.GET)
if not serializer.is_valid():
return TemplateResponse(
request,
"search/enter-search-term.html",
{"MIN_SEARCH_LENGTH": MIN_SEARCH_LENGTH},
)
search_query = serializer.validated_data["q"]
page_num = serializer.validated_data["page"]
context = {
**self.get_context(request),
"search_query": search_query,
"page_num": page_num,
}
filters, query = parse_query_string(search_query)
pages = self.get_listing_pages().search(query, order_by_relevance=True)
paginator = Paginator(pages, self.PAGE_SIZE)
context["paginator"] = paginator
try:
results = paginator.page(page_num)
# HACK: Search results aren't a queryset, so we can't call `.specific` on it. This forces it to one as efficiently as possible
results.object_list = results.object_list.get_queryset().specific()
except EmptyPage as e:
raise Http404 from e
context["results"] = results
return TemplateResponse(request, "search/search_results.html", context)