DEV Community

Dan Braghiș for Wagtail

Posted on

Wagtail API - how to customize the detail URL

A while ago, a member of the Wagtail community wanted to customize the PagesAPIEndpoint to access the specific page detail view via its slug (/api/v2/pages/the-page-slug, rather than id (/api/v2/pages/123)

The Wagtail API builds on the Django REST Framework (DRF), so the natural place to check was the DRF docs. The generic views documentation page points to changing lookup_field, however that does not work because BaseAPIEndpoint.get_object_detail_urlpath from which PagesAPIEndpoint is derived uses pk explicitly. The next logical place was to override the detail_url method for the model serializer (ref: BaseSerializer and DetailUrlField, with no success.

Digging further into the Wagtail API implementation internals reveals that the API router gets the URL information from each endpoint via get_urlpatterns and the BaseAPIEndpoint defines them as

# https://github.com/wagtail/wagtail/blob/v2.7/wagtail/api/v2/endpoints.py#L340
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'),
]
Enter fullscreen mode Exit fullscreen mode

With that in hand, we can then define our own endpoint that can handle both id and slug as parameters for the detail view.

# api.py
from wagtail.api.v2.endpoints import PagesAPIEndpoint
from wagtail.api.v2.router import WagtailAPIRouter


class MyPagesAPIEndpoint(PagesAPIEndpoint):
    """
    Our custom Pages API endpoint that allows finding pages by pk or slug
    """

    def detail_view(self, request, pk=None, slug=None):
        param = pk
        if slug is not None:
            self.lookup_field = 'slug'
            param = slug
        return super().detail_view(request, param)

    @classmethod
    def get_urlpatterns(cls):
        """
        This returns a list of URL patterns for the endpoint
        """
        return [
            path('', cls.as_view({'get': 'listing_view'}), name='listing'),
            path('<int:pk>/', cls.as_view({'get': 'detail_view'}), name='detail'),
            path('<slug:slug>/', cls.as_view({'get': 'detail_view'}), name='detail'),
            path('find/', cls.as_view({'get': 'find_view'}), name='find'),
        ]

# Create the router. “wagtailapi” is the URL namespace
api_router = WagtailAPIRouter('wagtailapi')

api_router.register_endpoint('pages', MyPagesAPIEndpoint)
Enter fullscreen mode Exit fullscreen mode

While the above works, slugs are only unique within a parent in Wagtail. It is, therefore, possible to have multiple pages with the same slug, but in different sections of the site (e.g.our-team in /about/our-team and /blog/our-team). This would lead to a MultipleObjectsReturned exception. To account for that, you need to do some defensive programming:

from django.core.exceptions import MultipleObjectsReturned
from django.shortcuts import redirect
from django.urls import reverse, path

from wagtail.api.v2.endpoints import PagesAPIEndpoint
from wagtail.api.v2.router import WagtailAPIRouter


class MyPagesAPIEndpoint(PagesAPIEndpoint):
    """
    Our custom Pages API endpoint that allows finding pages by pk or slug
    """

    def detail_view(self, request, pk=None, slug=None):
        param = pk
        if slug is not None:
            self.lookup_field = 'slug'
            param = slug
        try:
            return super().detail_view(request, param)
        except MultipleObjectsReturned:
            # Redirect to the listing view, filtered by the relevant slug
            # The router is registered with the `wagtailapi` namespace,
            # `pages` is our endpoint namespace and `listing` is the listing view url name.
            return redirect(
                reverse('wagtailapi:pages:listing') + f'?{self.lookup_field}={param}'
            )

    @classmethod
    def get_urlpatterns(cls):
        """
        This returns a list of URL patterns for the endpoint
        """
        return [
            path('', cls.as_view({'get': 'listing_view'}), name='listing'),
            path('<int:pk>/', cls.as_view({'get': 'detail_view'}), name='detail'),
            path('<slug:slug>/', cls.as_view({'get': 'detail_view'}), name='detail'),
            path('find/', cls.as_view({'get': 'find_view'}), name='find'),
        ]

# Create the router. “wagtailapi” is the URL namespace
api_router = WagtailAPIRouter('wagtailapi')

api_router.register_endpoint('pages', MyPagesAPIEndpoint)
Enter fullscreen mode Exit fullscreen mode

Using this technique we can provide additional endpoint URL patterns and make the Wagtail API cater for even more project specific requirements.

Happy coding!

Top comments (0)