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)
* 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)
* 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)

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
``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
=======================

Wyświetl plik

@ -14,7 +14,13 @@ New ``HelpPanel``
~~~~~~~~~~~~~~~~~
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
~~~~~~~~~~~~~~

Wyświetl plik

@ -3,6 +3,7 @@ from collections import OrderedDict
from django.conf.urls import url
from django.core.exceptions import FieldDoesNotExist
from django.http import Http404
from django.shortcuts import redirect
from django.urls import reverse
from modelcluster.fields import ParentalKey
from rest_framework import status
@ -19,7 +20,7 @@ from .filters import (
from .pagination import WagtailPagination
from .serializers import BaseSerializer, PageSerializer, get_serializer_class
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):
@ -76,6 +77,34 @@ class BaseAPIEndpoint(GenericViewSet):
serializer = self.get_serializer(instance)
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):
if isinstance(exc, Http404):
data = {'message': str(exc)}
@ -310,6 +339,7 @@ class BaseAPIEndpoint(GenericViewSet):
return [
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'^find/$', cls.as_view({'get': 'find_view'}), name='find'),
]
@classmethod
@ -405,3 +435,18 @@ class PagesAPIEndpoint(BaseAPIEndpoint):
def get_object(self):
base = super().get_object()
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 .utils import get_full_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)
from .utils import get_object_detail_url, pages_for_site
class TypeField(Field):
@ -42,7 +35,7 @@ class DetailUrlField(Field):
"detail_url": "http://api.example.com/v1/images/1/"
"""
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:
return url

Wyświetl plik

@ -485,6 +485,44 @@ class TestDocumentDetail(TestCase):
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(
WAGTAILFRONTENDCACHE={
'varnish': {

Wyświetl plik

@ -479,6 +479,44 @@ class TestImageDetail(TestCase):
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(
WAGTAILFRONTENDCACHE={
'varnish': {

Wyświetl plik

@ -1038,6 +1038,67 @@ class TestPageDetail(TestCase):
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):
fixtures = ['test.json']

Wyświetl plik

@ -25,6 +25,13 @@ def get_full_url(request, 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):
pages = Page.objects.public().live()
pages = pages.descendant_of(site.root_page, inclusive=True)