Add opensearch description file
Not _that_ opensearch
This commit is contained in:
parent
10e8950aef
commit
5d50907ed2
7 changed files with 134 additions and 6 deletions
|
@ -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" />
|
||||||
|
|
|
@ -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
|
||||||
|
|
11
website/search/templates/search/opensearch.xml
Normal file
11
website/search/templates/search/opensearch.xml
Normal 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>
|
|
@ -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
12
website/search/urls.py
Normal 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
71
website/search/views.py
Normal 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,
|
||||||
|
)
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue