46

If I have a view like:

class MyAPIView(APIView):
    def get(self, request, name=None):
        return {"hello": name or "world"}

How can I get that included in the generated documentation? Specifically, how can I get it included in the API Root, so it appears when I visit "http://example.com/api/"?

The documentation includes an example of an APIView with description, but doesn't describe the process of actually getting it included in the API browser.

Don Kirkby
  • 53,582
  • 27
  • 205
  • 286
David Wolever
  • 148,955
  • 89
  • 346
  • 502
  • Possible duplicate of [Routing API Views in Django Rest Framework?](http://stackoverflow.com/questions/18818179/routing-api-views-in-django-rest-framework) – David Avsajanishvili Nov 28 '15 at 12:42

7 Answers7

34

To mix with routers and APIView classes or method based in such a way that the API Root displays both with minimal code views in the APIRoot view I wrote a custom router that extends DefaultRouter and overrides get_urls and get_api_root_view; it looks like as follows :

from rest_framework import routers, views, reverse, response

class HybridRouter(routers.DefaultRouter):
    def __init__(self, *args, **kwargs):
        super(HybridRouter, self).__init__(*args, **kwargs)
        self._api_view_urls = {}

    def add_api_view(self, name, url):
        self._api_view_urls[name] = url

    def remove_api_view(self, name):
        del self._api_view_urls[name]

    @property
    def api_view_urls(self):
        ret = {}
        ret.update(self._api_view_urls)
        return ret

    def get_urls(self):
        urls = super(HybridRouter, self).get_urls()
        for api_view_key in self._api_view_urls.keys():
            urls.append(self._api_view_urls[api_view_key])
        return urls

    def get_api_root_view(self):
        # Copy the following block from Default Router
        api_root_dict = {}
        list_name = self.routes[0].name
        for prefix, viewset, basename in self.registry:
            api_root_dict[prefix] = list_name.format(basename=basename)
        api_view_urls = self._api_view_urls

        class APIRoot(views.APIView):
            _ignore_model_permissions = True

            def get(self, request, format=None):
                ret = {}
                for key, url_name in api_root_dict.items():
                    ret[key] = reverse.reverse(url_name, request=request, format=format)
                # In addition to what had been added, now add the APIView urls
                for api_view_key in api_view_urls.keys():
                    ret[api_view_key] = reverse.reverse(api_view_urls[api_view_key].name, request=request, format=format)
                return response.Response(ret)

        return APIRoot.as_view()

Then I use it as -

router = routers.HybridRouter()
router.register(r'abc', views.ABCViewSet)
router.add_api_view("api-view", url(r'^aview/$', views.AView.as_view(), name='aview-name'))
urlpatterns = patterns('',
    url(r'^api/', include(router.urls)),
Community
  • 1
  • 1
imyousuf
  • 1,245
  • 2
  • 11
  • 15
  • 3
    I wanted to notice, that source code of DefaultRouter has changed since this post, so the piece of code, you copied verbatim from DefualtRouter is now obsolete. I tried this code and it fails with exception: `NoReverseMatch at /api/`: `Reverse for 'cathegory-list' with arguments '()' and keyword arguments '{}' not found. 0 pattern(s) tried: []` – Boris Burkov Dec 24 '15 at 08:31
23

the generated documentation?

Hi David, first up I wouldn't quite describe the browsable API as 'generated documentation'.

If you need static documentation you're best off looking at a third party tool like django-rest-swagger.

The browsable API does mean that the APIs you build will be self-describing, but it's a little different from conventional static documentation tools. The browsable API ensures that all the endpoints you create in your API are able to respond both with machine readable (ie JSON) and human readable (ie HTML) representations. It also ensures you can fully interact directly through the browser - any URL that you would normally interact with using a programmatic client will also be capable of responding with a browser friendly view onto the API.

How can I get that included.

Just add a docstring to the view and it'll be included in the browsable API representation of whichever URLs you route to that view.

By default you can use markdown notation to include HTML markup in the description but you can also customise that behaviour, for example if you'd rather use rst.

Specifically, how can I get it included in the API Root.

You'll just want to explicitly add the URL to into the response returned by whatever view you have wired up to /api/. For example...

from rest_framework import renderers
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.reverse import reverse


class APIRoot(APIView):
    def get(self, request):
        # Assuming we have views named 'foo-view' and 'bar-view'
        # in our project's URLconf.
        return Response({
            'foo': reverse('foo-view', request=request),
            'bar': reverse('bar-view', request=request)
        })
Don Kirkby
  • 53,582
  • 27
  • 205
  • 286
Tom Christie
  • 33,394
  • 7
  • 101
  • 86
  • 26
    Is it possible to include class based views into the API root if you have also defined ViewSets that are registered via a router? – yogibear Jan 25 '14 at 03:05
  • Like @yogibear, I also wanted to include a class based view in the API root (and browsable API). My use case was an endpoint that should return one object, not a list of objects. I ended up just using a ViewSet and overriding the `list` method on the viewset as described [here](http://stackoverflow.com/a/30441337/1332513). – Kevin Oct 10 '15 at 17:12
  • 1
    > Just add a docstring to the view and it'll be included in the browsable API representation of whichever URLs you route to that view. -- In the question a simple hello world is given as an example to ask how to include it in the API Root, and no, adding a docstring to that example doesn't make the endpoint to appear in the list. The docstring does appear in the HTML page when you GET the endpoint from a browser, but in the API Root page that list all the available endpoints still the endpoint is not listed. – Mariano Ruiz Jul 13 '22 at 19:24
5

I have optimized HybridRouter for my use case and removed some code. Check it out:

class HybridRouter(routers.DefaultRouter):
    def __init__(self, *args, **kwargs):
        super(HybridRouter, self).__init__(*args, **kwargs)
        self.view_urls = []

    def add_url(self, view):
        self.view_urls.append(view)

    def get_urls(self):
        return super(HybridRouter, self).get_urls() + self.view_urls

    def get_api_root_view(self):
        original_view = super(HybridRouter, self).get_api_root_view()

        def view(request, *args, **kwargs):
            resp = original_view(request, *args, **kwargs)
            namespace = request.resolver_match.namespace
            for view_url in self.view_urls:
                name = view_url.name
                url_name = name
                if namespace:
                    url_name = namespace + ':' + url_name
                resp.data[name] = reverse(url_name,
                                          args=args,
                                          kwargs=kwargs,
                                          request=request,
                                          format=kwargs.get('format', None))
            return resp
        return view

And usage:

router = routers.HybridRouter(trailing_slash=False)
router.add_url(url(r'^me', v1.me.view, name='me'))
router.add_url(url(r'^status', v1.status.view, name='status'))

urlpatterns = router.urls

Or:

router = routers.HybridRouter(trailing_slash=False)
router.view_urls = [
    url(r'^me', v1.me.view, name='me'),
    url(r'^status', v1.status.view, name='status'),
]

urlpatterns = router.urls
nailgun
  • 121
  • 2
  • 3
  • This one works great for me as well. It's also much simpler than the others and very easy to understand, thanks! – PKKid Feb 23 '20 at 03:04
4

Updated version of @imyousuf code to work with DRF 3.4.1.

class HybridRouter(routers.DefaultRouter):
    def __init__(self, *args, **kwargs):
        super(HybridRouter, self).__init__(*args, **kwargs)
        self._api_view_urls = {}

    def add_api_view(self, name, url):
        self._api_view_urls[name] = url

    def remove_api_view(self, name):
        del self._api_view_urls[name]

    @property
    def api_view_urls(self):
        ret = {}
        ret.update(self._api_view_urls)
        return ret

    def get_urls(self):
        urls = super(HybridRouter, self).get_urls()
        for api_view_key in self._api_view_urls.keys():
            urls.append(self._api_view_urls[api_view_key])
        return urls

    def get_api_root_view(self, api_urls=None):
        # Copy the following block from Default Router
        api_root_dict = OrderedDict()
        list_name = self.routes[0].name
        for prefix, viewset, basename in self.registry:
            api_root_dict[prefix] = list_name.format(basename=basename)

        view_renderers = list(self.root_renderers)
        schema_media_types = []

        if api_urls and self.schema_title:
            view_renderers += list(self.schema_renderers)
            schema_generator = SchemaGenerator(
                title=self.schema_title,
                url=self.schema_url,
                patterns=api_urls
            )
            schema_media_types = [
                renderer.media_type
                for renderer in self.schema_renderers
                ]

        api_view_urls = self._api_view_urls

        class APIRoot(views.APIView):
            _ignore_model_permissions = True
            renderer_classes = view_renderers

            def get(self, request, *args, **kwargs):
                if request.accepted_renderer.media_type in schema_media_types:
                    # Return a schema response.
                    schema = schema_generator.get_schema(request)
                    if schema is None:
                        raise exceptions.PermissionDenied()
                    return Response(schema)

                # Return a plain {"name": "hyperlink"} response.
                ret = OrderedDict()
                namespace = request.resolver_match.namespace
                for key, url_name in api_root_dict.items():
                    if namespace:
                        url_name = namespace + ':' + url_name
                    try:
                        ret[key] = reverse.reverse(
                            url_name,
                            args=args,
                            kwargs=kwargs,
                            request=request,
                            format=kwargs.get('format', None)
                        )
                    except NoReverseMatch:
                        # Don't bail out if eg. no list routes exist, only detail routes.
                        continue

                # In addition to what had been added, now add the APIView urls
                for api_view_key in api_view_urls.keys():
                    url_name = api_view_urls[api_view_key].name
                    if namespace:
                        url_name = namespace + ':' + url_name
                    ret[api_view_key] = reverse.reverse(url_name, request=request, format=kwargs.get('format'))

                return response.Response(ret)

        return APIRoot.as_view()

How to use:

mobile_router = HybridRouter()
mobile_router.add_api_view("device", url(r'^device/register/$', DeviceViewSet.as_view({'post': 'register'}), name='device-register'))
Kurt Van den Branden
  • 11,995
  • 10
  • 76
  • 85
michal-michalak
  • 827
  • 10
  • 6
  • Please update it to work with DRF 3.7.3, self.schema_title is not defined anymore. I have also looked into it, but cannot get my head around how to change it precisely. – gabn88 Nov 21 '17 at 14:24
4

For the record, it is 2019 now and https://bitbucket.org/hub9/django-hybrid-router is still working, only modification is that line 64 has to be edited to become:

                regex = api_view_urls[api_view_key].pattern.regex
gabn88
  • 781
  • 8
  • 23
1

Solution by @imyousuf is nice, but it doesn't support url namespaces and is a bit outdated.

Here's an update of it:

class HybridRouter(routers.DefaultRouter):
    def __init__(self, *args, **kwargs):
        super(HybridRouter, self).__init__(*args, **kwargs)
        self._api_view_urls = {}

    def add_api_view(self, name, url):
        self._api_view_urls[name] = url

    def remove_api_view(self, name):
        del self._api_view_urls[name]

    @property
    def api_view_urls(self):
        ret = {}
        ret.update(self._api_view_urls)
        return ret

    def get_urls(self):
        urls = super(HybridRouter, self).get_urls()
        for api_view_key in self._api_view_urls.keys():
            urls.append(self._api_view_urls[api_view_key])
        return urls

    def get_api_root_view(self):

        # Copy the following block from Default Router
        api_root_dict = {}
        list_name = self.routes[0].name
        for prefix, viewset, basename in self.registry:
            api_root_dict[prefix] = list_name.format(basename=basename)

        # In addition to that:
        api_view_urls = self._api_view_urls

        class APIRoot(views.APIView):
            _ignore_model_permissions = True

            def get(self, request, *args, **kwargs):
                ret = OrderedDict()
                namespace = request.resolver_match.namespace
                for key, url_name in api_root_dict.items():
                    if namespace:
                        url_name = namespace + ':' + url_name
                    try:
                        ret[key] = reverse(
                            url_name,
                            args=args,
                            kwargs=kwargs,
                            request=request,
                            format=kwargs.get('format', None)
                        )
                    except NoReverseMatch:
                        # Don't bail out if eg. no list routes exist, only detail routes.
                        continue

                # In addition to what had been added, now add the APIView urls
                for api_view_key in api_view_urls.keys():
                    namespace = request.resolver_match.namespace
                    if namespace:
                        url_name = namespace + ":" + api_view_key
                    ret[api_view_key] = reverse(url_name,
                                        args=args,
                                        kwargs=kwargs,
                                        request=request,
                                        format=kwargs.get('format', None))

                return response.Response(ret)

        return APIRoot.as_view()
Boris Burkov
  • 13,420
  • 17
  • 74
  • 109
  • 1
    Doesn't work well with add_api_view. it simply takes the last working one and inserts that, instead of failing when you add something that should fail. – gabn88 Mar 08 '16 at 08:04
  • @gabn88 Thank you so much for code review and suggested edit. I'm not sure, what to do. May be, I should totally remove `name` argument from `add_api_view/remove_api_view` functions and strictly require that url patterns contain `name` argument? Because I don't want to duplicate the functionality of django's configuration as primary storage of url names. Or should I create my own optional name storage in the HybridRouter and stick with your edit? Do you have any considerations in favour of either approach? – Boris Burkov Mar 09 '16 at 15:24
  • To be honoust, I use the HybridRouter with my current edits and it seems to work fine, also with namespaces? Or did I miss a bug? – gabn88 Mar 09 '16 at 15:33
  • @gabn88 Well, when I introduced namespaced url "api:cathegory-list", this HybridRouter crashed with `NoReverseMatch at /api/: Reverse for 'cathegory-list' with arguments '()' and keyword arguments '{}' not found. 0 pattern(s) tried: []`. When I added namespace, it worked. (though, I updated DefaultRouter code to DRF3.3, but I don't think I introduced a bug there) – Boris Burkov Mar 09 '16 at 15:39
  • 1
    It seems to be broken in 3.7.3 of DRF again, no scheme_titles. Will try to fix it and post a new answer... – gabn88 Nov 21 '17 at 14:15
  • @gabn88 Did you get anywhere with that? – Oli Apr 07 '18 at 06:52
  • 1
    @Oli ended up using https://bitbucket.org/hub9/django-hybrid-router seems to work fine so far, but did no extensive testing as we are in the middle of a release and the API docs are not the highest priority right now. – gabn88 Jun 30 '18 at 12:37
1

Now in 2022 the hybrid router still works.

But you need to change the usage from :

mobile_router.add_api_view("device", url(r'^device/register/$', DeviceViewSet.as_view({'post': 'register'}), name='device-register'))

to:

mobile_router.add_api_view("device", re_path(r'^device/register/$', DeviceViewSet.as_view({'post': 'register'}), name='device-register'))

And you need to change the import as well

from Django.urls import re_path

The rest works fine from the bitbucket HybridRouter

Empusas
  • 372
  • 2
  • 17