kopia lustrzana https://github.com/wagtail/wagtail
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 #4154pull/4469/merge
rodzic
edbfba5af3
commit
17f7f70170
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
=======================
|
||||
|
||||
|
|
|
@ -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
|
||||
~~~~~~~~~~~~~~
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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': {
|
||||
|
|
|
@ -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']
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
Ładowanie…
Reference in New Issue