Add opensearch description file

Not _that_ opensearch
This commit is contained in:
Jake Howard 2024-01-04 22:21:32 +00:00
parent 10e8950aef
commit 5d50907ed2
Signed by: jake
GPG key ID: 57AFB45680EDD477
7 changed files with 134 additions and 6 deletions

View file

@ -14,6 +14,8 @@
{% block extra_head %}{% endblock %}
<link rel="search" type="application/opensearchdescription+xml" href="{% url 'opensearch' %}" title="Orange search" />
<link rel="alternate" type="application/rss+xml" href="{% url 'feed' %}" />
<link rel="me" href="https://{{ ACTIVITYPUB_HOST }}/@jake" />

View file

@ -1,4 +1,5 @@
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
@ -43,6 +44,13 @@ class SearchPage(RoutablePageMixin, BaseContentPage):
context["SEO_INDEX"] = False
return context
def get_listing_pages(self) -> models.QuerySet:
return (
Page.objects.live()
.public()
.not_type(self.__class__, *self.EXCLUDED_PAGE_TYPES)
)
@route(r"^results/$")
@method_decorator(require_GET)
def results(self, request: HttpRequest) -> HttpResponse:
@ -68,12 +76,7 @@ class SearchPage(RoutablePageMixin, BaseContentPage):
}
filters, query = parse_query_string(search_query)
pages = (
Page.objects.live()
.public()
.not_type(self.__class__, *self.EXCLUDED_PAGE_TYPES)
.search(query, order_by_relevance=True)
)
pages = self.get_listing_pages().search(query, order_by_relevance=True)
paginator = Paginator(pages, self.PAGE_SIZE)
context["paginator"] = paginator

View file

@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
<ShortName>{{ site_title }}</ShortName>
<Description>{{ site_title }}</Description>
<InputEncoding>UTF-8</InputEncoding>
{% if favicon_url %}
<Image type="image/png">{{ favicon_url }}</Image>
{% endif %}
<Url type="text/html" template="{{ search_page_url }}?q={searchTerms}"/>
<Url type="application/x-suggestions+json" template="{{ search_suggestions_url }}?q={searchTerms}"/>
</OpenSearchDescription>

View file

@ -1,5 +1,6 @@
from bs4 import BeautifulSoup
from django.test import TestCase
from django.urls import reverse
from website.common.factories import ContentPageFactory
from website.home.models import HomePage
@ -127,3 +128,30 @@ class SearchPageResultsTestCase(TestCase):
with self.assertNumQueries(7):
response = self.client.get(self.url)
self.assertEqual(response.status_code, 400)
class OpenSearchTestCase(TestCase):
@classmethod
def setUpTestData(cls) -> None:
cls.home_page = HomePage.objects.get()
cls.page = SearchPageFactory(parent=cls.home_page)
for i in range(6):
ContentPageFactory(parent=cls.home_page, title=f"Post {i}")
def test_opensearch_description(self) -> None:
with self.assertNumQueries(11):
response = self.client.get(reverse("opensearch"))
self.assertEqual(response.status_code, 200)
self.assertContains(response, self.page.get_url())
self.assertContains(response, reverse("opensearch-suggestions"))
def test_opensearch_suggestions(self) -> None:
with self.assertNumQueries(4):
response = self.client.get(reverse("opensearch-suggestions"), {"q": "post"})
self.assertEqual(response.status_code, 200)
data = response.json()
self.assertEqual(data[0], "post")
self.assertEqual(data[1], [f"Post {i}" for i in range(5)])

12
website/search/urls.py Normal file
View file

@ -0,0 +1,12 @@
from django.urls import path
from . import views
urlpatterns = [
path("opensearch.xml", views.OpenSearchView.as_view(), name="opensearch"),
path(
"opensearch-suggestions/",
views.OpenSearchSuggestionsView.as_view(),
name="opensearch-suggestions",
),
]

71
website/search/views.py Normal file
View file

@ -0,0 +1,71 @@
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views.decorators.cache import cache_control
from django.views.generic import TemplateView, View
from wagtail.search.utils import parse_query_string
from wagtail_favicon.models import FaviconSettings
from wagtail_favicon.utils import get_rendition_url
from website.common.utils import get_site_title
from website.contrib.singleton_page.utils import SingletonPageCache
from .models import SearchPage
from .serializers import SearchParamsSerializer
@method_decorator(cache_control(max_age=60 * 60), name="dispatch")
class OpenSearchView(TemplateView):
template_name = "search/opensearch.xml"
content_type = "application/xml"
def get_context_data(self, **kwargs: dict) -> dict:
context = super().get_context_data(**kwargs)
favicon_settings = FaviconSettings.for_request(self.request)
if favicon_settings.base_favicon_image_id:
context["favicon_url"] = self.request.build_absolute_uri(
get_rendition_url(
favicon_settings.base_favicon_image, "fill-100|format-png"
)
)
context["search_page_url"] = self.request.build_absolute_uri(
SingletonPageCache.get_url(SearchPage, self.request)
)
context["search_suggestions_url"] = self.request.build_absolute_uri(
reverse("opensearch-suggestions")
)
context["site_title"] = get_site_title()
return context
@method_decorator(cache_control(max_age=60 * 60), name="dispatch")
class OpenSearchSuggestionsView(View):
def get(self, request: HttpRequest) -> JsonResponse:
serializer = SearchParamsSerializer(data=request.GET)
if not serializer.is_valid():
return JsonResponse(serializer.errors, status=400)
search_page = get_object_or_404(SearchPage)
filters, query = parse_query_string(serializer.validated_data["q"])
results = (
search_page.get_listing_pages()
.search(query, order_by_relevance=True)[:5]
.get_queryset()
)
return JsonResponse(
[
serializer.validated_data["q"],
list(results.values_list("title", flat=True)),
],
safe=False,
)

View file

@ -50,6 +50,7 @@ urlpatterns = [
path("feed/", AllPagesFeed(), name="feed"),
path(".health/", include("health_check.urls")),
path("", include("website.legacy.urls")),
path("", include("website.search.urls")),
path("api/", include("website.api.urls", namespace="api")),
path(
"@jake",