7

This is my views.py:

class ChoicesViewSet(viewsets.ModelViewSet):
    queryset = SingleChoice.objects.all()
    serializer_class = SingleChoiceSerializer
    ...

class AssessmentTakersViewSet(viewsets.ModelViewSet):
    queryset = AssessmentTaker.objects.all()
    serializer_class = AssessmentTakersSerializer
    ...

@api_view(['POST'])
@parser_classes((JSONParser,))
def studio_create_view(request, format=None):
    """"
    A view that accept POST request with JSON content and in turn build out the 
    questions and choices. Post with application/json type.
    """
    ... 

This is my urls.py:


urlpatterns = [
    # http://localhost:8000/survey/api/studio-create
    path('api/studio-create', views.studio_create_view, name='studio-create-api'),
]

# drf config
router = routers.DefaultRouter()

router.register('api/choices', views.ChoicesViewSet)
router.register('api/assessment-takers', views.AssessmentTakersViewSet)

urlpatterns += router.urls

This works functionally, and is considered feature-complete, but but because studio-create_view is not registered with the router, the path doesn't show up under the API Root which isn't great documentation wise. In other words, the API root from Django Rest Framework isn't aware of the path, and a GET request to the root would list only:

http://localhost:8000/survey/

HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept

{

    "api/choices": "http://localhost:8000/survey/api/choices/",
    "api/assessment-takers": "http://localhost:8000/survey/api/assessment-takers/",
    ...
}

illustration

My question is, outside of viewsets that inherit from ModelViewSet, how do we get the custom views like studio_create_view to be registered with the router that Django Rest Framework provide so it's listed under the API root?

One would be inclined to try something like:

router.register('api/studio-create', views.studio_create_view, basename='studio-create-api')

But this will not work, throwing the following exception:

AttributeError: 'function' object has no attribute 'get_extra_actions'

Omitting the name or basename also doesn't work.

router.register('api/studio-create', views.studio_create_view)

This would prompt Django Rest Framework to throw the following exception:

AssertionError: basename argument not specified, and could not automatically determine the name from the viewset, as it does not have a .queryset attribute.

Note that the custom view function studio_create_view is a function, not a class, and hence doesn't inherit from any other classes, so similar questions where the view inherits from viewsets.ViewSet or viewsets.ModelViewSet or any of the likes are unlikely to be helpful. Appreciate any pointers as I've scoured the documentation extensively to no avail.

onlyphantom
  • 8,606
  • 4
  • 44
  • 58
  • 1
    [This](https://stackoverflow.com/questions/17496249/in-django-restframework-how-to-change-the-api-root-documentation) might help – Brian Destura Aug 07 '21 at 13:44
  • DefaultRouter is not a gateway to documentation. I'd recommend trying `drf-spectacular` (which is awesome) or one of the other tools listed. Is there a compelling reason to use DefaultRouter? I've not found one, _but_ if this is specific to **this** viewset then have you tried using an `@action(..)` on the viewset? – Andrew Aug 09 '21 at 21:27
  • 1
    I'd be open to trying `drf-spectacular`. I'm just really puzzled that this isn't something natively supported by the incredible DRF already given how my use-case isn't exactly edgy or novel. I've tried swapping out `DefaultRouter` for `SimpleRouter` and then trying all kind of workarounds. I feel like when I finally found the solution, it would be something so simple / trivial -- "oh just add two lines of code here and call the internal API to wire your function into the api root" but I'm not having any success with the docs :( – onlyphantom Aug 10 '21 at 05:12
  • 1
    It might be that you need to override the default SchemaGenerator class to tell it about your views. https://www.django-rest-framework.org/api-guide/schemas/#schemagenerator – Cory Aug 13 '21 at 06:41

1 Answers1

0

Here you can find an extension of the DefaultRouter that supports function based views and APIViews:

Using Django Rest Framework's browsable API with APIViews?

In short, here is the code: hybridrouter.py (from https://bitbucket.org/hub9/django-hybrid-router/src/master/ patched with https://stackoverflow.com/a/54520039/8589004)

from collections import OrderedDict

from django.urls import NoReverseMatch
from rest_framework import routers, views, reverse, response


class HybridRouter(routers.DefaultRouter):
    # From http://stackoverflow.com/a/23321478/1459749.
    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)
        api_view_urls = self._api_view_urls

        class APIRootView(views.APIView):
            _ignore_model_permissions = True
            exclude_from_schema = 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.reverse(
                            url_name,
                            args=args,
                            kwargs=kwargs,
                            request=request,
                            format=kwargs.get('format', None)
                        )
                    except NoReverseMatch:
                        continue
                # In addition to what had been added, now add the APIView urls
                for api_view_key in api_view_urls.keys():
                    regex = api_view_urls[api_view_key].pattern.regex
                    if regex.groups == 0:
                        url_name = api_view_urls[api_view_key].name
                        if namespace:
                            url_name = namespace + ':' + url_name
                        ret[api_view_key] = reverse.reverse(
                            url_name,
                            args=args,
                            kwargs=kwargs,
                            request=request,
                            format=kwargs.get('format', None)
                        )
                    else:
                        ret[api_view_key] = "WITH PARAMS: " + regex.pattern
                return response.Response(ret)

        return APIRootView.as_view()

    def register_router(self, another_router):
        self.registry.extend(another_router.registry)
        if hasattr(another_router, "_api_view_urls"):
            self._api_view_urls.update(another_router._api_view_urls)

And here you got the usage example:

router = HybridRouter()
router.register(r'devices', views.DevicesViewSet, basename='devices')
router.add_api_view(r'find-my-device', re_path(r'^find-my-device/$', views.find_my_device, name='find-my-device'))
Deloo
  • 96
  • 6