自定义 DjangoRestFramework 可浏览 API

rob*_*nki 6 django django-templates django-rest-framework

我正在使用 Django Rest Framework 3.11.0,我想使用 BrowsableAPIRenderer 和自定义模板来呈现实例的详细信息。我只想覆盖 dict/json 的渲染,在下图中标记为红色,我想保留其余所有内容。

通过覆盖,restframework/api.html我只能更改标题、标题和一些字段,但我没有找到一种方法来呈现实例的详细信息,例如在表格中。有没有办法做到这一点?

澄清:我有带有大型字典的模型,我想将它们显示得比内联字符串更漂亮。我认为当我找到如何定制(已经很漂亮的)Django RestFramework BrowsableAPI 时,我也将能够解决我的问题。

(如果您想解决类似的问题,请查看我的更新 2。)

截屏


更新1

这是我从 Bedilbeks 得到的答案(直到第一次更新)。

我不想更改所有视图,因此我不会全局注册渲染器。

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'rest_framework.renderers.BrowsableAPIRenderer',
         # 'users.renderers.CustomBrowsableAPIRenderer',
    ]
}
Run Code Online (Sandbox Code Playgroud)

相反,我将设置renderer_classes为 myUserViewSet并在此处使用 my CustomBrowsableAPIRenderer

class UserViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin):
    queryset = UserModel.objects.all()
    serializer_class = UserSerializer
    renderer_classes = [renderers.JSONRenderer, CustomBrowsableAPIRenderer]
Run Code Online (Sandbox Code Playgroud)

我需要覆盖api.html模板,但我不希望此更改应用到所有地方,因此我在渲染器中动态选择模板。默认情况下,它BrowsableAPIRenderer有一个template = "rest_framework/api.html"属性,但我需要逻辑,所以我使用@property装饰器来执行以下操作:

  • 检查我们是否在detail视野范围内
  • 检查 GET 参数

如果我们在详细视图中并且"table"存在参数,则返回我的模板,否则返回默认值。

class CustomBrowsableAPIRenderer(BrowsableAPIRenderer):
    @property
    def template(self):
        view = self.renderer_context.get("view", {})
        table = "table" in view.request.query_params
        if view and hasattr(view, "detail") and view.detail and table:
            return "users/api.html"  # custom template
        else:
            return "rest_framework/api.html"  # default

    def get_default_renderer(self, view):
        table = "table" in view.request.query_params
        if hasattr(view, "detail") and view.detail and table:
            return TableHtmlRenderer()

        return super().get_default_renderer(view)
Run Code Online (Sandbox Code Playgroud)

的关键部分api.html如下所示(第 123 行左右)。

...
{% block style %}
    {{ block.super }}
    <link rel="stylesheet" type="text/css" href="{% static "css/api.css" %}"/>
{% endblock %}

<!-- HERE IS THE ACTUAL CONTENT -->
</span></pre><div class="prettyprint" style="overflow: auto;">{{ content|urlize_quoted_links }}</div>
            </div>
...
Run Code Online (Sandbox Code Playgroud)

实际上,我并不是为User模型和 ViewSet 这样做,但为了示例,我坚持这样做。在我的模型中,我想要渲染更大的 JSON 元素,因此我在我的模型中进行一些预处理,TableHTMLRenderer以缩进形式返回 JSON。

class TableHtmlRenderer(TemplateHTMLRenderer):
    media_type = "text/html"
    format = "api"
    template_name = "table_template.html"

    def get_template_context(self, data, renderer_context):
        for key in data.keys():
            try:
                data[key] = json.dumps(json.loads(data[key]), indent=4)
            except (JSONDecodeError, TypeError):
                pass

        context = {
            "data": data
        }

        response = renderer_context["response"]
        if response.exception:
            context["status_code"] = response.status_code

        return context
Run Code Online (Sandbox Code Playgroud)

通过 URL 的控制,我可以在默认渲染器和自定义/表格渲染器之间切换。

  • localhost.me:8000/api/users/1/?table

http://localhost.me:8000/api/users/1/?table

  • localhost.me:8000/api/users/1/

http://localhost.me:8000/api/users/1/

到目前为止一切顺利,我现在有了自己的 Renderer 类,并且可以修改 User 实例的 API 视图的外观。我仍然在与表格作斗争,因为长行上的换行符不起作用,并且它不会停留在 div 的边界内。

这是模板app.css中加载的内容api.html

pre.inline {
    padding: 0;
    border: none;
    word-break: break-all;
    word-wrap: break-word;
    display: contents;
}

table, th, td {
    vertical-align: top;
    padding: 2px;
    text-align: left;}

table {
    //table-layout: fixed;
    width: 100% !important;
    word-wrap:break-word;
}

th, td {
    border-bottom: 1px solid #ddd;
    overflow: auto;
    width: 100%;
}

tr:hover {
    background-color: #f2f2f2;
}

tr:nth-child(even) {
    background-color: #f5f5f5;
}
Run Code Online (Sandbox Code Playgroud)

更新2

由于我现在可以使用自定义的 BrowsableAPIRenderer 和模板显示一些视图,并进行了一些技巧,所以我回到了导致我提出这个问题的问题。我想了解 DRF 如何渲染我的模型,进行更改以显示大型嵌套字典。

我发现将BrowsableAPIRenderer模型内容作为单个字符串插入到api.html模板中,就像标签{{ content|urlize_quoted_links }}内部一样<pre>。插入发生在BrowsableAPIRenderer.get_content方法中。

# original code
renderer_context['indent'] = 4
content = renderer.render(data, accepted_media_type, renderer_context)
Run Code Online (Sandbox Code Playgroud)

我现在发现我所要做的就是子类化BrowsableAPIRenderer并重写该get_content方法。我就是这样做的。

class LogBrowsableAPIRenderer(BrowsableAPIRenderer):    
    def get_content(self, renderer, data, accepted_media_type, renderer_context):
        """
        Extends BrowsableAPIRenderer.get_content.
        """
        if not renderer:
            return '[No renderers were found]'

        renderer_context['indent'] = 4
        # content = renderer.render(data, accepted_media_type, renderer_context)

        # try to convert all string-values into dictionaries
        data_dict = dict(data.items())
        for k in data_dict.keys():
            try:
                data_dict[k] = json.loads(data_dict[k], strict=False)
            except JSONDecodeError:
                # ignore errors and move on for now
                pass

        # dump into indented string again
        content = json.dumps(data_dict, indent=4, sort_keys=True).encode(encoding="utf-8")

        render_style = getattr(renderer, 'render_style', 'text')
        assert render_style in ['text', 'binary'], 'Expected .render_style "text" or "binary", but got "%s"' % render_style
        if render_style == 'binary':
            return '[%d bytes of binary content]' % len(content)

        return content
Run Code Online (Sandbox Code Playgroud)

我还意识到我可以用不同的措辞来表达我的问题,以便更快地结束这个问题。

Bed*_*bek 1

这不是最合适和最好的答案,但我认为这或多或少是你想要的,而且有点黑客。

假设我们想要公开/users/端点,并且我们有以下内容views.py

from django.contrib.auth import get_user_model
from rest_framework.mixins import ListModelMixin, RetrieveModelMixin
from rest_framework.routers import DefaultRouter
from rest_framework.serializers import ModelSerializer
from rest_framework.viewsets import GenericViewSet

UserModel = get_user_model()


class UserSerializer(ModelSerializer):
    class Meta:
        model = UserModel
        fields = ('first_name', 'last_name')


class UserViewSet(GenericViewSet, ListModelMixin, RetrieveModelMixin):
    queryset = UserModel.objects.all()
    serializer_class = UserSerializer


router = DefaultRouter()
router.register('users', UserViewSet)
urlpatterns = router.urls

Run Code Online (Sandbox Code Playgroud)

因此,首先,让我们renderers.py在一个应用程序中创建(假设我们有users应用程序):

from rest_framework.renderers import BrowsableAPIRenderer, TemplateHTMLRenderer


class TableHtmlRenderer(TemplateHTMLRenderer):
    media_type = 'text/html'
    format = 'api'
    template_name = 'users/table_template.html'

    def get_template_context(self, data, renderer_context):
        context = {'data': data}
        response = renderer_context['response']
        if response.exception:
            context['status_code'] = response.status_code
        return context


class CustomBrowsableAPIRenderer(BrowsableAPIRenderer):

    def get_default_renderer(self, view):
        if view.detail:
            return TableHtmlRenderer()

        return super().get_default_renderer(view)

Run Code Online (Sandbox Code Playgroud)

通过这种方式,我们重写BrowsableAPIRenderer我们的CustomBrowsableAPIRenderer只是为了更改我们的默认渲染器content(在我们的例子中,它是dictjson 可序列化的 obj)。我们通过访问 viewset.detail 属性来检查我们的视图是否是detail=True

然后,我们需要TemplateHTMLRenderer用我们自己的覆盖现有的TableHTMLRenderer,以将我们的 dict 对象作为data上下文users/table_template.html

所以,现在我们必须创建我们的users/table_template.html

<table>
    <tr>
        <th>Key</th>
        <th>Value</th>
    </tr>
    {% for key, value in data.items %}
    <tr>
        <td>{{ key }}</td>
        <td>{{ value }}</td>
    </tr>
    {% endfor %}
</table>

Run Code Online (Sandbox Code Playgroud)

现在,我们准备在启用渲染器后进行检查settings.py

REST_FRAMEWORK = {
    'DEFAULT_RENDERER_CLASSES': [
        'rest_framework.renderers.JSONRenderer',
        'users.renderers.CustomBrowsableAPIRenderer',
    ]
}
Run Code Online (Sandbox Code Playgroud)

如果你看到了,现在我们rest_framework.renderers.BrowsableAPIRenderer用我们自己的users.renderers.CustomBrowsableAPIRenderer自定义渲染器替换了。

在此输入图像描述

我们看到现在我们有表而不是 json 结构。

我希望,这也对你有用。


更新

如果我们想要摆脱空格或者想要更多自定义修改,我们必须覆盖并更改api.html. 因此,我们创建自己的users/api.html

{% extends "rest_framework/base.html" %}
{% load i18n %}
{% load rest_framework %}

{% block content %}
...
</span></pre>
            <div class="prettyprint">{{ content|urlize_quoted_links }}
            </div>
        </div>
...
{% endblock content %}

Run Code Online (Sandbox Code Playgroud)

在放置 3 个点的地方,我们必须复制并粘贴从块开始的...其余部分,因为块太大,我们想要更改该块内的某些内容。如果不复制粘贴整个块,我们就无法做到这一点。如果我们将我们的模板与旧的(元素完成的地方)进行比较,我们会删除上下文并将其放入元素末尾之后的新 div 中,如代码示例中所示,这样现在我们不使用元素,也不会出现额外的空格放。base.htmlcontentcontentcontentbase.html</span>{{ content|urlize_quoted_links }}</pre><pre>

然后,如果我们看一下结果: 在此输入图像描述

但是,糟糕的是,现在我们已经更改并覆盖了整个模板,因此我们也破坏了其他视图: 在此输入图像描述

结果,你会看到我们的结果变得笨拙,并且我们做了太多的修改来达到我们想要的结果。这就是为什么我真的不建议api.html仅仅为了改变那content部分而重写。相反,我会通过覆盖渲染器类来使用我的第一个答案,该答案不那么老套。