【问题标题】:Using Django Rest Framework's browsable API with APIViews?将 Django Rest Framework 可浏览 API 与 API 视图一起使用?
【发布时间】:2013-09-20 00:15:47
【问题描述】:

如果我有这样的看法:

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

如何将其包含在生成的文档中?具体来说,如何才能将它包含在 API Root 中,以便在我访问“http://example.com/api/”时出现?

带有描述的 APIView 的documentation includes an example,但没有描述实际将其包含在 API 浏览器中的过程。

【问题讨论】:

标签: django-rest-framework


【解决方案1】:

为了与路由器和 APIView 类或方法混合,以使 API Root 在 APIRoot 视图中以最少的代码视图显示两者,我编写了一个自定义路由器,它扩展了 DefaultRouter 并覆盖了 get_urls 和 get_api_root_view;它看起来如下:

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

那我把它当作-

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

【讨论】:

  • 我想注意到,自这篇文章以来 DefaultRouter 的源代码发生了变化,所以你从 DefualtRouter 逐字复制的那段代码现在已经过时了。我尝试了这段代码,但异常失败:NoReverseMatch at /api/:Reverse for 'cathegory-list' with arguments '()' and keyword arguments '{}' not found. 0 pattern(s) tried: []
【解决方案2】:

生成的文档?

嗨,David,首先我不会完全将可浏览 API 描述为“生成的文档”。

如果您需要静态文档,最好查看第三方工具,例如 django-rest-swagger

可浏览的 API 确实意味着您构建的 API 将是自描述的,但它与传统的静态文档工具略有不同。可浏览的 API 确保您在 API 中创建的所有端点都能够以机器可读(即 JSON)和人类可读(即 HTML)表示形式进行响应。它还确保您可以直接通过浏览器进行完全交互 - 您通常使用编程客户端与之交互的任何 URL 也将能够以浏览器友好视图响应 API。

我怎样才能把它包括在内。

只需向视图添加一个文档字符串,它就会包含在您路由到该视图的任何 URL 的可浏览 API 表示中。

默认情况下,您可以使用 markdown 表示法在描述中包含 HTML 标记,但您也可以 customise that behaviour,例如,如果您更愿意使用 rst。

具体来说,我怎样才能将它包含在 API Root 中。

您只需将 URL 显式添加到连接到 /api/ 的任何视图返回的响应中。比如……

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

【讨论】:

  • 如果您还定义了通过路由器注册的 ViewSet,是否可以将基于类的视图包含到 API 根中?
  • 和@yogibear 一样,我也想在API 根(和可浏览的API)中包含一个基于类的视图。我的用例是一个应该返回一个对象而不是对象列表的端点。我最终只使用了 ViewSet 并覆盖了视图集上的 list 方法,如 here 所述。
【解决方案3】:

我已针对我的用例优化了 HybridRouter 并删除了一些代码。看看吧:

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

及用法:

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

或者:

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

【讨论】:

  • 这个对我也很有效。它也比其他的简单得多,也很容易理解,谢谢!
【解决方案4】:

@imyousuf 代码的更新版本可用于 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()

使用方法:

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

【讨论】:

  • 请更新它以使用 DRF 3.7.3,self.schema_title 不再定义。我也研究过它,但不知道如何精确地改变它。
【解决方案5】:

记录一下,现在是 2019 年,https://bitbucket.org/hub9/django-hybrid-router 还在工作,唯一的修改是第 64 行必须编辑为:

                regex = api_view_urls[api_view_key].pattern.regex

【讨论】:

    【解决方案6】:

    @imyousuf 的解决方案很好,但它不支持 url 命名空间,而且有点过时了。

    这是它的更新:

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

    【讨论】:

    • 不适用于 add_api_view。它只需要最后一个工作并插入它,而不是在您添加应该失败的内容时失败。
    • @gabn88 非常感谢您的代码审查和建议的编辑。我不确定,该怎么办。可能是,我应该从add_api_view/remove_api_view 函数中完全删除name 参数并严格要求url 模式包含name 参数?因为我不想复制 django 配置的功能作为 url 名称的主要存储。或者我应该在 HybridRouter 中创建自己的可选名称存储并坚持您的编辑?您对这两种方法有什么考虑吗?
    • 老实说,我将 HybridRouter 与我当前的编辑一起使用,它似乎工作正常,也可以使用命名空间?还是我错过了一个错误?
    • DRF 3.7.3 好像又坏了,没有scheme_titles。将尝试修复它并发布新答案...
    • @Oli 最终使用 bitbucket.org/hub9/django-hybrid-router 似乎到目前为止工作正常,但没有进行广泛的测试,因为我们正处于发布中间,API 文档现在不是最高优先级。
    猜你喜欢
    • 1970-01-01
    • 2015-04-28
    • 2021-01-07
    • 2014-05-28
    • 2019-09-14
    • 2016-03-24
    • 2015-01-16
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多