Added "find" API view and ability to find pages by HTML path

This implements a new "find" view for all endpoints which can be used
for finding an individual object based on the URL parameters passed to
it.

If an object is found, the view will return a ``302`` redirect to detail
page of that object. If not, the view will return a ``404`` response.

For the pages endpoint, I've added a ``html_path`` parameter to this
view, this allows finding a page by its path on the site.

For example a GET request to ``/api/v2/pages/find/?html_path=/`` will
always generate a 302 response to the detail view of the homepage. This
uses Wagtail's internal routing mechanism so routable pages are
supported as well.

Fixes #4154
pull/4469/merge
Karl Hobley 2018-01-12 13:16:46 +00:00 zatwierdzone przez Matt Westcott
rodzic edbfba5af3
commit 17f7f70170
9 zmienionych plików z 212 dodań i 11 usunięć

Wyświetl plik

@ -5,6 +5,7 @@ Changelog
~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~
* Add `HelpPanel` to add HTML within an edit form (Keving Chung) * Add `HelpPanel` to add HTML within an edit form (Keving Chung)
* Added API endpoint for finding pages by HTML path (Karl Hobley)
* Persist tab hash in URL to allow direct navigation to tabs in the admin interface (Ben Weatherman) * Persist tab hash in URL to allow direct navigation to tabs in the admin interface (Ben Weatherman)
* Animate the chevron icon when opening sub-menus in the admin (Carlo Ascani) * Animate the chevron icon when opening sub-menus in the admin (Carlo Ascani)
* Look through the target link and target page slug (in addition to the old slug) when searching for redirects in the admin (Michael Harrison) * Look through the target link and target page slug (in addition to the old slug) when searching for redirects in the admin (Michael Harrison)

Wyświetl plik

@ -422,6 +422,18 @@ All exported fields will be returned in the response by default. You can use the
For example: ``/api/v2/pages/1/?fields=_,title,body`` will return just the For example: ``/api/v2/pages/1/?fields=_,title,body`` will return just the
``title`` and ``body`` of the page with the id of 1. ``title`` and ``body`` of the page with the id of 1.
.. _apiv2_finding_pages_by_path:
Finding pages by HTML path
--------------------------
You can find an individual page by its HTML path using the ``/api/v2/pages/find/?html_path=<path>`` view.
This will return either a ``302`` redirect response to that page's detail view, or a ``404`` not found response.
For example: ``/api/v2/pages/find/?html_path=/`` always redirects to the homepage of the site
Default endpoint fields Default endpoint fields
======================= =======================

Wyświetl plik

@ -14,7 +14,13 @@ New ``HelpPanel``
~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~
A new panel type ``HelpPanel`` allows to easily add HTML within an edit form. A new panel type ``HelpPanel`` allows to easily add HTML within an edit form.
This new feature was developed by Keving Chung. This new feature was developed by Kevin Chung.
API lookup by page path
~~~~~~~~~~~~~~~~~~~~~~~
The API now includes an endpoint for finding pages by path; see :ref:`apiv2_finding_pages_by_path`. This feature was developed by Karl Hobley.
Other features Other features
~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~

Wyświetl plik

@ -3,6 +3,7 @@ from collections import OrderedDict
from django.conf.urls import url from django.conf.urls import url
from django.core.exceptions import FieldDoesNotExist from django.core.exceptions import FieldDoesNotExist
from django.http import Http404 from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
from modelcluster.fields import ParentalKey from modelcluster.fields import ParentalKey
from rest_framework import status from rest_framework import status
@ -19,7 +20,7 @@ from .filters import (
from .pagination import WagtailPagination from .pagination import WagtailPagination
from .serializers import BaseSerializer, PageSerializer, get_serializer_class from .serializers import BaseSerializer, PageSerializer, get_serializer_class
from .utils import ( from .utils import (
BadRequestError, filter_page_type, page_models_from_string, parse_fields_parameter) BadRequestError, filter_page_type, page_models_from_string, parse_fields_parameter, get_object_detail_url)
class BaseAPIEndpoint(GenericViewSet): class BaseAPIEndpoint(GenericViewSet):
@ -76,6 +77,34 @@ class BaseAPIEndpoint(GenericViewSet):
serializer = self.get_serializer(instance) serializer = self.get_serializer(instance)
return Response(serializer.data) return Response(serializer.data)
def find_view(self, request):
queryset = self.get_queryset()
try:
obj = self.find_object(queryset, request)
if obj is None:
raise self.model.DoesNotExist
except self.model.DoesNotExist:
raise Http404("not found")
# Generate redirect
url = get_object_detail_url(self.request.wagtailapi_router, request, self.model, obj.pk)
if url is None:
# Shouldn't happen unless this endpoint isn't actually installed in the router
raise Exception("Cannot generate URL to detail view. Is '{}' installed in the API router?".format(self.__class__.__name__))
return redirect(url)
def find_object(self, queryset, request):
"""
Override this to implement more find methods.
"""
if 'id' in request.GET:
return queryset.get(id=request.GET['id'])
def handle_exception(self, exc): def handle_exception(self, exc):
if isinstance(exc, Http404): if isinstance(exc, Http404):
data = {'message': str(exc)} data = {'message': str(exc)}
@ -310,6 +339,7 @@ class BaseAPIEndpoint(GenericViewSet):
return [ return [
url(r'^$', cls.as_view({'get': 'listing_view'}), name='listing'), url(r'^$', cls.as_view({'get': 'listing_view'}), name='listing'),
url(r'^(?P<pk>\d+)/$', cls.as_view({'get': 'detail_view'}), name='detail'), url(r'^(?P<pk>\d+)/$', cls.as_view({'get': 'detail_view'}), name='detail'),
url(r'^find/$', cls.as_view({'get': 'find_view'}), name='find'),
] ]
@classmethod @classmethod
@ -405,3 +435,18 @@ class PagesAPIEndpoint(BaseAPIEndpoint):
def get_object(self): def get_object(self):
base = super().get_object() base = super().get_object()
return base.specific return base.specific
def find_object(self, queryset, request):
if 'html_path' in request.GET and request.site is not None:
path = request.GET['html_path']
path_components = [component for component in path.split('/') if component]
try:
page, _, _ = request.site.root_page.specific.route(request, path_components)
except Http404:
return
if queryset.filter(id=page.id).exists():
return page
return super().find_object(queryset, request)

Wyświetl plik

@ -8,14 +8,7 @@ from taggit.managers import _TaggableManager
from wagtail.core import fields as wagtailcore_fields from wagtail.core import fields as wagtailcore_fields
from .utils import get_full_url, pages_for_site from .utils import get_object_detail_url, pages_for_site
def get_object_detail_url(context, model, pk):
url_path = context['router'].get_object_detail_urlpath(model, pk)
if url_path:
return get_full_url(context['request'], url_path)
class TypeField(Field): class TypeField(Field):
@ -42,7 +35,7 @@ class DetailUrlField(Field):
"detail_url": "http://api.example.com/v1/images/1/" "detail_url": "http://api.example.com/v1/images/1/"
""" """
def get_attribute(self, instance): def get_attribute(self, instance):
url = get_object_detail_url(self.context, type(instance), instance.pk) url = get_object_detail_url(self.context['router'], self.context['request'], type(instance), instance.pk)
if url: if url:
return url return url

Wyświetl plik

@ -485,6 +485,44 @@ class TestDocumentDetail(TestCase):
self.assertEqual(content, {'message': "'title' does not support nested fields"}) self.assertEqual(content, {'message': "'title' does not support nested fields"})
class TestDocumentFind(TestCase):
fixtures = ['demosite.json']
def get_response(self, **params):
return self.client.get(reverse('wagtailapi_v2:documents:find'), params)
def test_without_parameters(self):
response = self.get_response()
self.assertEqual(response.status_code, 404)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(content, {
'message': 'not found'
})
def test_find_by_id(self):
response = self.get_response(id=5)
self.assertRedirects(response, 'http://localhost' + reverse('wagtailapi_v2:documents:detail', args=[5]), fetch_redirect_response=False)
def test_find_by_id_nonexistent(self):
response = self.get_response(id=1234)
self.assertEqual(response.status_code, 404)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(content, {
'message': 'not found'
})
@override_settings( @override_settings(
WAGTAILFRONTENDCACHE={ WAGTAILFRONTENDCACHE={
'varnish': { 'varnish': {

Wyświetl plik

@ -479,6 +479,44 @@ class TestImageDetail(TestCase):
self.assertEqual(content, {'message': "'title' does not support nested fields"}) self.assertEqual(content, {'message': "'title' does not support nested fields"})
class TestImageFind(TestCase):
fixtures = ['demosite.json']
def get_response(self, **params):
return self.client.get(reverse('wagtailapi_v2:images:find'), params)
def test_without_parameters(self):
response = self.get_response()
self.assertEqual(response.status_code, 404)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(content, {
'message': 'not found'
})
def test_find_by_id(self):
response = self.get_response(id=5)
self.assertRedirects(response, 'http://localhost' + reverse('wagtailapi_v2:images:detail', args=[5]), fetch_redirect_response=False)
def test_find_by_id_nonexistent(self):
response = self.get_response(id=1234)
self.assertEqual(response.status_code, 404)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(content, {
'message': 'not found'
})
@override_settings( @override_settings(
WAGTAILFRONTENDCACHE={ WAGTAILFRONTENDCACHE={
'varnish': { 'varnish': {

Wyświetl plik

@ -1038,6 +1038,67 @@ class TestPageDetail(TestCase):
self.assertEqual(content, {'message': "'title' does not support nested fields"}) self.assertEqual(content, {'message': "'title' does not support nested fields"})
class TestPageFind(TestCase):
fixtures = ['demosite.json']
def get_response(self, **params):
return self.client.get(reverse('wagtailapi_v2:pages:find'), params)
def test_without_parameters(self):
response = self.get_response()
self.assertEqual(response.status_code, 404)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(content, {
'message': 'not found'
})
def test_find_by_id(self):
response = self.get_response(id=5)
self.assertRedirects(response, 'http://localhost' + reverse('wagtailapi_v2:pages:detail', args=[5]), fetch_redirect_response=False)
def test_find_by_id_nonexistent(self):
response = self.get_response(id=1234)
self.assertEqual(response.status_code, 404)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(content, {
'message': 'not found'
})
def test_find_by_html_path(self):
response = self.get_response(html_path='/events-index/event-1/')
self.assertRedirects(response, 'http://localhost' + reverse('wagtailapi_v2:pages:detail', args=[8]), fetch_redirect_response=False)
def test_find_by_html_path_with_start_and_end_slashes_removed(self):
response = self.get_response(html_path='events-index/event-1')
self.assertRedirects(response, 'http://localhost' + reverse('wagtailapi_v2:pages:detail', args=[8]), fetch_redirect_response=False)
def test_find_by_html_path_nonexistent(self):
response = self.get_response(html_path='/foo')
self.assertEqual(response.status_code, 404)
self.assertEqual(response['Content-type'], 'application/json')
# Will crash if the JSON is invalid
content = json.loads(response.content.decode('UTF-8'))
self.assertEqual(content, {
'message': 'not found'
})
class TestPageDetailWithStreamField(TestCase): class TestPageDetailWithStreamField(TestCase):
fixtures = ['test.json'] fixtures = ['test.json']

Wyświetl plik

@ -25,6 +25,13 @@ def get_full_url(request, path):
return base_url + path return base_url + path
def get_object_detail_url(router, request, model, pk):
url_path = router.get_object_detail_urlpath(model, pk)
if url_path:
return get_full_url(request, url_path)
def pages_for_site(site): def pages_for_site(site):
pages = Page.objects.public().live() pages = Page.objects.public().live()
pages = pages.descendant_of(site.root_page, inclusive=True) pages = pages.descendant_of(site.root_page, inclusive=True)