Skip to content

Latest commit

 

History

History
272 lines (186 loc) · 6.93 KB

150-评论.md

File metadata and controls

272 lines (186 loc) · 6.93 KB

评论是博客作者和读者进行沟通的重要方式,也是博客作者检视自身文章质量的手段。

虽然有很多方式可以将评论功能托管给第三方(我也推荐这么做),不过本着学习的目的,接下来就试着自己实现简单的评论接口。

准备工作

评论功能比较独立,因此另起一个 comment 的 App:

(venv) > python manage.py startapp comment

注册到配置文件:

# drf_vue_blog/settings.py

...

INSTALLED_APPS = [
    ...
    'comment',
]

接下来就是模型:

# comment/models.py

from django.db import models
from django.utils import timezone

from article.models import Article
from django.contrib.auth.models import User


class Comment(models.Model):
    author = models.ForeignKey(
        User,
        on_delete=models.CASCADE,
        related_name='comments'
    )

    article = models.ForeignKey(
        Article,
        on_delete=models.CASCADE,
        related_name='comments'
    )

    content = models.TextField()
    created = models.DateTimeField(default=timezone.now)

    class Meta:
        ordering = ['-created']

    def __str__(self):
        return self.content[:20]

模型包含一对多的作者外键、一对多的文章外键、评论实际内容、评论时间这4个字段。

执行 makemigrationsmigrate ,准备工作就完成了。

视图和序列化

视图集和之前章节的差不多:

# comment/views.py

from rest_framework import viewsets

from comment.models import Comment
from comment.serializers import CommentSerializer
from comment.permissions import IsOwnerOrReadOnly


class CommentViewSet(viewsets.ModelViewSet):
    queryset = Comment.objects.all()
    serializer_class = CommentSerializer
    permission_classes = [IsOwnerOrReadOnly]

    def perform_create(self, serializer):
        serializer.save(author=self.request.user)

接下来写评论的权限。

评论对用户身份的要求比文章的更松弛,非安全请求只需要是本人操作就可以了。

因此自定义一个所有人都可查看、仅本人可更改的权限:

# comment/permissions.py

from rest_framework.permissions import BasePermission, SAFE_METHODS

class IsOwnerOrReadOnly(BasePermission):
    message = 'You must be the owner to update.'

    def has_permission(self, request, view):
        if request.method in SAFE_METHODS:
            return True

        return request.user.is_authenticated

    def has_object_permission(self, request, view, obj):
        if request.method in SAFE_METHODS:
            return True

        return obj.author == request.user

进行非安全请求时,由于需要验证当前评论的作者和当前登录的用户是否为同一个人,这里用到了 def has_object_permission(...) 这个钩子方法,方法参数中的 obj 即为评论模型的实例。

看起来只需要实现这个 def has_object_permission(...) 就可以了,但还有一点点小问题:此方法是晚于视图集中的 def perform_create(author=self.request.user) 执行的。如果用户未登录时新建评论,由于用户不存在,接口会抛出 500 错误。

本着即使出错也要做出正确错误提示的原则,增加了 def has_permission(...) 方法。此方法早于 def perform_create(...) 执行,因此能够对用户登录状态做一个预先检查。

功能这样就实现了,但是重复的代码又出现了,让我们来消灭它。

删掉旧代码,把这个权限类修改为下面这样:

# comment/permissions.py
# ...
class IsOwnerOrReadOnly(BasePermission):
    message = 'You must be the owner to update.'

    def safe_methods_or_owner(self, request, func):
        if request.method in SAFE_METHODS:
            return True

        return func()

    def has_permission(self, request, view):
        return self.safe_methods_or_owner(
            request,
            lambda: request.user.is_authenticated
        )

    def has_object_permission(self, request, view, obj):
        return self.safe_methods_or_owner(
            request,
            lambda: obj.author == request.user
        )

用匿名函数将有函数体(闭包)作为参数,传递到 def safe_methods_or_owner(...) 方法里执行,效果和之前是完全一样的。

接下来的东西就都轻车熟路了。

将视图集注册到路由:

# drf_vue_blog/urls.py

...

# 这里直接导入 views 会冲突
from comment.views import CommentViewSet
router.register(r'comment', CommentViewSet)

将评论的序列化器写了:

# comment/serializers.py

from rest_framework import serializers

from comment.models import Comment
from user_info.serializers import UserDescSerializer


class CommentSerializer(serializers.ModelSerializer):
    url = serializers.HyperlinkedIdentityField(view_name='comment-detail')
    author = UserDescSerializer(read_only=True)

    class Meta:
        model = Comment
        fields = '__all__'
        extra_kwargs = {'created': {'read_only': True}}

跟之前一样, url 超链接字段让接口的跳转更方便,author 嵌套序列化器让显示的内容更丰富。

最后让评论通过文章接口显示出来:

# article/serializers.py

...

from comment.serializers import CommentSerializer

class ArticleDetailSerializer(...):
    id = serializers.IntegerField(read_only=True)
    comments = CommentSerializer(many=True, read_only=True)

    ...

这就完成了,代码量很少就完成了新功能。

测试

发几个请求测试接口逻辑是否正确。

未登录用户新建评论:

> http POST http://127.0.0.1:8000/api/comment/ article=1 content='New comment by Obama'                                 
HTTP/1.1 403 Forbidden
...
{
    "detail": "Authentication credentials were not provided."
}

用之前注册好的用户 Obama 新建评论:

PS C:\Users\Dusai> http -a Obama:admin123456  POST http://127.0.0.1:8000/api/comment/ article=1 content='New comment by Obama'

HTTP/1.1 201 Created
...
{
    "article": 1,
    "author": {
        ...
    },
    "content": "New comment by Obama",
    "created": "2020-12-28T05:57:55.092150Z",
    "id": 7,
    "url": "http://127.0.0.1:8000/api/comment/7/"
}

用非本人用户 dusai 更新评论:

...> http -a dusai:admin123456 POST http://127.0.0.1:8000/api/comment/7/ article=1 content='Updated by API' 

HTTP/1.1 405 Method Not Allowed
...
{
    "detail": "Method \"POST\" not allowed."
}

Obama 删除评论:

...> http -a Obama:admin123456 DELETE http://127.0.0.1:8000/api/comment/7/
            
HTTP/1.1 204 No Content
Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS
Content-Length: 0
Date: Mon, 28 Dec 2020 05:59:46 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.8.2
Vary: Accept, Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

非本人无法对资源进行更改,很好的符合了预期逻辑。