实现分类、标签、归档日期接口

2020-06-164089 阅读5 评论

我们的博客有一个侧边栏功能,分别列出博客文章的分类列表、标签列表、归档时间列表,通过点击侧边栏对应的条目,还可以进入相应的页面。例如点击某个分类,博客将跳转到该分类下全部文章列表页面。这些数据的展示都需要开发对应的接口,以便前端调用获取数据。

分类列表、标签列表实现比较简单,我们这里给出接口的设计规范,大家可以使用前几篇教程中学到的知识点轻松实现(具体实现可参考 GtiHub 上的源代码)。

分类列表接口: /categories/

标签列表接口:/tags/

归档日期列表的接口实现稍微复杂一点,因为我们需要从已有文章中归纳文章发表日期。事实上,我们在上一部教程 HelloDjango - Django博客教程(第二版)页面侧边栏:使用自定义模板标签 已经讲解了如何获取归档日期列表,只是当时返回的归档日期列表直接用于模板的渲染,而这里我们需要将归档日期列表序列化后通过 API 接口返回。

具体来说,获取博客文章发表时间归档列表的方法是调用查询集(QuerySet)的 dates 方法,提取记录中的日期。核心代码就一句:

Post.objects.dates('created_time', 'month', order='DESC')

这里 Post.objects.dates 方法会返回一个列表,列表中的元素为每一篇文章(Post)的创建日期(已去重),日期都是 Python 的 date 对象,精确到月份,降序排列。

有了返回的归档日期列表,接下来就实现相应的 API 接口视图函数:

blog/views.py

from rest_framework import mixins, status, viewsets
from rest_framework.decorators import action
from rest_framework.serializers import DateField

class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    # ...

    @action(
        methods=["GET"], detail=False, url_path="archive/dates", url_name="archive-date"
    )
    def list_archive_dates(self, request, *args, **kwargs):
        dates = Post.objects.dates("created_time", "month", order="DESC")
        date_field = DateField()
        data = [date_field.to_representation(date) for date in dates]
        return Response(data=data, status=status.HTTP_200_OK)

注意这里我们涉及到了几个以前没有详细讲解过的用法。

一是 action 装饰器,它用来装饰一个视图集中的方法,被装饰的方法会被 django-rest-framework 的路由自动注册为一个 API 接口。

回顾一下我们之前在使用视图集 viewset 时提到过 action(动作)的概念,django-rest-framework 预定义了几个标准的动作,分别为 list 获取资源列表,retrieve 获取单个资源、update 和 partial_update 更新资源、destroy 删除资源,这些 action 具体的实现方法,分别由 mixins 模块中的混入类提供。例如 用类视图实现首页 API 中我们介绍过 mixins.ListModelMixin,这个混入类提供了 list 动作对应的标准实现,即 list 方法。视图集中所有以上提及的以标准动作命名的方法,都会被 django-rest-framework 的路由自动注册为标准的 API 接口。

django-rest-framework 默认只能识别标准命名的视图集方法并将其注册为 API,但我们可以添加更多非标准的 action,而为了让 django-rest-framework 能够识别这些方法,就需要使用 action 装饰器进行装饰。

其实我们可以简单地将 action 装饰的方法看作是一个视图函数的实现,因此可以看到方法传入的第一个参数为 request 请求对象,函数体就是这个视图函数需要执行的逻辑,显然,方法最终必须要返回一个 HTTP 响应对象。

action 装饰器通常用于在视图集中添加额外的接口实现。例如这里我们已有了 PostViewSet 视图集,标准的 list 实现了获取文章资源列表的逻辑。我们想添加一个获取文章归档日期列表的接口,因此添加了一个 list_archive_dates 方法,并使用 action 进行装饰。通常如果要在视图集中添加额外的接口实现,可以使用如下的模板代码:

@action(
    methods=["allowed http method name"], 
    detail=False or True, 
    url_path="url/path", 
    url_name="url name"
)
def method_name(self, request, *args, **kwargs):
    # 接口逻辑的具体实现,返回一个 Response

通常 action 装饰器以下 4 个参数都会设置:

methods:一个列表,指定访问这个接口时允许的 HTTP 方法(GET、POST、PUT、PATCH、DELETE)

detail:True 或者 False。设置为 True,自动注册的接口 URL 中会添加一个 pk 路径参数(请看下面的示例),否则不会。

url_path:自动注册的接口 URL。

url_name:接口名,主要用于通过接口名字反解对应的 URL。

当然,我们还可以在 action 中设置所有 ViewSet 类所支持的类属性,例如 serializer_classpagination_classpermission_classes 等,用于覆盖类视图中设置的属性值。

以上是 action 用法的一个基本介绍,现在来分析一下 list_archive_dates 这个 action 来加深理解。

methods 参数指定接口需要通过 GET 方法访问,detail 为 Falseurl_path 设置为 archive/dates,因此最终自动生成的接口路由就是 /posts/archive/dates/。如果我们设置 detail 为 True,那么生成的接口路由就是 /posts/<int:pk>/archive/dates/,生成的 URL 中就会多一个 pk 路径参数。

list_archive_dates 具体的实现逻辑中,以下几点需要注意:

一是独立使用序列化字段(Field)。之前序列化字段都是在序列化器(Serializer)里面使用的,因为通常来说接口需要序列化一个对象的多个字段。而这个接口中只需要序列化一个时间字段(类型为 Python 标准库中的 datetime.date),所以没必要单独定义一个序列化器了,直接拿 django-rest-framework 提供的用于序列化时间类型的 DateField 就可以了。用法也很简单,实例化序列化字段,调用其 to_representation 方法,将需要序列化的值传入即可(其实序列化器在序列对象的多个字段时,内部也是分别调用对应序列化字段的 to_representation 方法)。

我们通过列表推导式生成一个序列化后的归档日期列表,这个列表是可被序列化的。接着我们在接口返回一个 ResponseResponse 将序列化后的结果包装返回(保存在 data 属性中),django-rest-framework 会进一步帮我们把这个 Response 中包含的数据解析为合适的格式(例如 JSON)。

status=status.HTTP_200_OK 指定这个接口返回的状态码,HTTP_200_OK 是一个预定义的常数,即 200。django-rest-framework 将常用 HTTP 请求的状态码常数预定义 status 模块里,使用预定义的变量而不是直接使用数字的好处一是增强代码可读性,二是减少硬编码。

由于 PostViewSet 视图集已经通过 django-rest-framework 的路由进行了注册,因此 list_archive_dates 也会被连带着自动注册为一个接口。启动开发服务器,访问 /posts/archive/dates/,就可以看到返回的文章归档日期列表。

文章归档日期返回结果

注意到红框圈出部分,django-rest-framework API 交互后台会识别到额外定义的 action 并将它们展示出来,点击就可以进入到相应的 API 页面。

现在,侧边栏所需要的数据接口就开发完成了,接下来实现返回某一分类、标签或者归档日期下的文章列表接口。

使用视图集简化代码 我们开发了获取全部文章的接口。事实上,分类、标签或者归档日期文章列表的 API,本质上还是返回一个文章列表资源,只不过比首页 API 返回的文章列表资源多了个“过滤”,只过滤出了指定的部分文章而已。对于这样的场景,我们可以在请求 API 时加上查询参数,django-rest-framework 解析查询参数,然后从全部文章列表中过滤出查询所指定的文章列表再返回。

这在 RESTful API 的设计中肯定是会遇到的,因此第三方库 django-filter 帮我们实现了上述所说的查询过滤功能,而且和 django-rest-framework 有很好的集成,我们可以在 django-rest-framework 中非常方便地使用 django-filter。

既然要使用它,当然是先安装它(已安装跳过):pipenv install django-filter

接着我们来配置 PostViewSet,为其设置用于过滤返回结果集的一些属性,代码如下:

from django_filters.rest_framework import DjangoFilterBackend
from .filters import PostFilter

class PostViewSet(
    mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
):
    # ...
    filter_backends = [DjangoFilterBackend]
    filterset_class = PostFilter

非常的简单,仅仅设置了 filter_backendsfilterset_class 两个属性。其中 filter_backends 设置为 DjangoFilterBackend,这样 API 在返回结果时, django-rest-framework 会调用设置的 backend(这里是 DjangoFilterBackend) 的 filter 方法对 get_queryset 方法返回的结果进行进一步的过滤,而 DjangoFilterBackend 会依据 filterset_class(这里是 PostFilter)中定义的过滤规则来过滤查询结果集。

当然 PostFilter 还没有定义,我们来定义它。首先在 blog 应用下创建一个 filters.py 文件,用于存放自定义 filter 的代码,PostFilter 代码如下:

from django_filters import rest_framework as drf_filters

from .models import Post


class PostFilter(drf_filters.FilterSet):
    created_year = drf_filters.NumberFilter(
        field_name="created_time", lookup_expr="year"
    )
    created_month = drf_filters.NumberFilter(
        field_name="created_time", lookup_expr="month"
    )

    class Meta:
        model = Post
        fields = ["category", "tags", "created_year", "created_month"]

PostFilter 的定义和序列化器 Serializer 非常类似。

categorytags 两个过滤字段因为是 Post 模型中定义的字段,因此 django-filter 可以自动推断其过滤规则,只需要在 Meta.fields 中声明即可。

归档日期下的文章列表,我们设计的接口传递 2 个查询参数:年份和月份。由于这两个字段在 Post 中没有定义,Post 记录时间的字段为 created_time,因此我们需要显示地定义查询规则,定义的规则是:

查询参数名 = 查询参数值的类型(查询的模型字段,查询表达式)

例如示例中定义的 created_year 查询参数,查询参数值的类型为 number,即数字,查询的模型字段为 created_time,查询表达式是 year。当用户传递 created_year 查询参数时,django-filter 实际上会将以上定义的规则翻译为如下的 ORM 查询语句:

Post.objects.filter(created_time__year=created_year传递的值)

现在回到 API 交互后台,先进到 /post/ 接口下,默认返回了全部文章列表。可以看到右上角多了个过滤器(红框圈出部分)。

点击会弹出过滤参数输入的交互面板,在这里可以交互式地输入查询过滤参数的值。

例如选择如下的过滤参数,得到查询的 URL 为:

http://127.0.0.1:10000/api/posts/?category=1&tags=1&created_year=2020&created_month=1

这条查询返回创建于 2020 年 1 月,id 为 1 的分类下,id 为 1 的标签下的全部文章。

通过不同的查询参数组合,就可以得到不同的文章资源列表了。

-- EOF --

5 评论
登录后回复
大圣骑
2021-01-07 11:48:11

打卡完成

回复
runiccolon
2020-07-07 12:20:19

报错:django_filters/rest_framework/form.html

回复
runiccolon runiccolon
2020-07-07 12:20:52

TemplateDoesNotExist at /api/posts/

在添加了filter后

回复
追梦人物 runiccolon
2020-07-25 20:29:37

django_filter 加入 INSTALLED_APP 没?

回复
wyg 追梦人物
2021-10-22 21:05:44

应该是 django_filters (带s)

回复