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 %} {% 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="alternate" type="application/rss+xml" href="{% url 'feed' %}" />
<link rel="me" href="https://{{ ACTIVITYPUB_HOST }}/@jake" /> <link rel="me" href="https://{{ ACTIVITYPUB_HOST }}/@jake" />

View file

@ -1,4 +1,5 @@
from django.core.paginator import EmptyPage, Paginator from django.core.paginator import EmptyPage, Paginator
from django.db import models
from django.http.request import HttpRequest from django.http.request import HttpRequest
from django.http.response import Http404, HttpResponse, HttpResponseBadRequest from django.http.response import Http404, HttpResponse, HttpResponseBadRequest
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
@ -43,6 +44,13 @@ class SearchPage(RoutablePageMixin, BaseContentPage):
context["SEO_INDEX"] = False context["SEO_INDEX"] = False
return context 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/$") @route(r"^results/$")
@method_decorator(require_GET) @method_decorator(require_GET)
def results(self, request: HttpRequest) -> HttpResponse: def results(self, request: HttpRequest) -> HttpResponse:
@ -68,12 +76,7 @@ class SearchPage(RoutablePageMixin, BaseContentPage):
} }
filters, query = parse_query_string(search_query) filters, query = parse_query_string(search_query)
pages = ( pages = self.get_listing_pages().search(query, order_by_relevance=True)
Page.objects.live()
.public()
.not_type(self.__class__, *self.EXCLUDED_PAGE_TYPES)
.search(query, order_by_relevance=True)
)
paginator = Paginator(pages, self.PAGE_SIZE) paginator = Paginator(pages, self.PAGE_SIZE)
context["paginator"] = paginator 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 bs4 import BeautifulSoup
from django.test import TestCase from django.test import TestCase
from django.urls import reverse
from website.common.factories import ContentPageFactory from website.common.factories import ContentPageFactory
from website.home.models import HomePage from website.home.models import HomePage
@ -127,3 +128,30 @@ class SearchPageResultsTestCase(TestCase):
with self.assertNumQueries(7): with self.assertNumQueries(7):
response = self.client.get(self.url) response = self.client.get(self.url)
self.assertEqual(response.status_code, 400) 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("feed/", AllPagesFeed(), name="feed"),
path(".health/", include("health_check.urls")), path(".health/", include("health_check.urls")),
path("", include("website.legacy.urls")), path("", include("website.legacy.urls")),
path("", include("website.search.urls")),
path("api/", include("website.api.urls", namespace="api")), path("api/", include("website.api.urls", namespace="api")),
path( path(
"@jake", "@jake",