Appearance
评论是博客作者和读者进行沟通的重要方式,也是博客作者检视自身文章质量的手段。
虽然有很多方式可以将评论功能托管给第三方(我也推荐这么做),不过本着学习的目的,接下来就试着自己实现简单的评论接口。
准备工作
评论功能比较独立,因此另起一个 comment
的 App:
(venv) > python manage.py startapp comment
注册到配置文件:
python
# drf_vue_blog/settings.py
...
INSTALLED_APPS = [
...
'comment',
]
接下来就是模型:
python
# 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个字段。
执行 makemigrations
和 migrate
,准备工作就完成了。
视图和序列化
视图集和之前章节的差不多:
python
# 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)
接下来写评论的权限。
评论对用户身份的要求比文章的更松弛,非安全请求只需要是本人操作就可以了。
因此自定义一个所有人都可查看、仅本人可更改的权限:
python
# 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(...)
执行,因此能够对用户登录状态做一个预先检查。
功能这样就实现了,但是重复的代码又出现了,让我们来消灭它。
删掉旧代码,把这个权限类修改为下面这样:
python
# 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(...)
方法里执行,效果和之前是完全一样的。
接下来的东西就都轻车熟路了。
将视图集注册到路由:
python
# drf_vue_blog/urls.py
...
# 这里直接导入 views 会冲突
from comment.views import CommentViewSet
router.register(r'comment', CommentViewSet)
将评论的序列化器写了:
python
# 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
嵌套序列化器让显示的内容更丰富。
最后让评论通过文章接口显示出来:
python
# article/serializers.py
...
from comment.serializers import CommentSerializer
class ArticleDetailSerializer(...):
id = serializers.IntegerField(read_only=True)
comments = CommentSerializer(many=True, read_only=True)
...
这就完成了,代码量很少就完成了新功能。
测试
发几个请求测试接口逻辑是否正确。
未登录用户新建评论:
python
> 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
新建评论:
python
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
更新评论:
python
...> 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
删除评论:
python
...> 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
非本人无法对资源进行更改,很好的符合了预期逻辑。