Django+DRF基于MPTT的嵌套评论API出现详情找不到问题求助
Django MPTT + DRF 嵌套评论系统:深层嵌套后早期评论无法编辑/删除
使用MPTT结合Django Rest Framework(DRF)实现嵌套评论系统,目前嵌套显示正常,所有增删改操作均通过DRF API完成,未直接调用Django ORM。但遇到一个Bug:
当评论嵌套到第7或第8层时,早期的评论(如最早的aaaa、bbbb)在编辑/删除时会返回*「detail: Not found.」*,但列表查询能正常显示这些评论。例如添加评论ggggg后,aaaa和bbbb无法操作;删除ggggg后(其子评论hhhh随CASCADE删除),aaaa和bbbb又恢复正常。
我的代码
models.py
from django.db import models from django.template.defaultfilters import truncatechars from mptt.managers import TreeManager from post.models import Post from account.models import Account from mptt.models import MPTTModel, TreeForeignKey # Create your models here. # With MPTT class CommentManager(TreeManager): def viewable(self): queryset = self.get_queryset().filter(level=0) return queryset class Comment(MPTTModel): parent = TreeForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='comment_children') post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comment_post') user = models.ForeignKey(Account, on_delete=models.CASCADE, related_name='comment_account') content = models.TextField(max_length=9000) created_date = models.DateTimeField(auto_now_add=True) updated_date = models.DateTimeField(auto_now=True) status = models.BooleanField(default=True) objects = CommentManager() def __str__(self): return f'Comment by {str(self.pk)}-{self.user.full_name.__self__}' @property def short_content(self): return truncatechars(self.content, 99) class MPTTMeta: # If changing the order - MPTT needs the programmer to go into console and do Comment.objects.rebuild() order_insertion_by = ['-created_date']
serializers.py(仅展示评论序列化部分)
class RecursiveField(serializers.Serializer): def to_representation(self, value): serializer = self.parent.parent.__class__(value, context=self.context) return serializer.data class CommentSerializer(serializers.ModelSerializer): post_slug = serializers.SerializerMethodField() user = serializers.StringRelatedField(read_only=True) user_name = serializers.SerializerMethodField() user_id = serializers.PrimaryKeyRelatedField(read_only=True) comment_children = RecursiveField(many=True) class Meta: model = Comment fields = '__all__' # noinspection PyMethodMayBeStatic # noinspection PyBroadException def get_post_slug(self, instance): try: slug = instance.post.slug return slug except Exception: pass # noinspection PyMethodMayBeStatic # noinspection PyBroadException def get_user_name(self, instance): try: full_name = f'{instance.user.first_name} {instance.user.last_name}' return full_name except Exception: pass # noinspection PyMethodMayBeStatic def validate_content(self, value): if len(value) < COM_MIN_LEN: raise serializers.ValidationError('The comment is too short.') elif len(value) > COM_MAX_LEN: raise serializers.ValidationError('The comment is too long.') else: return value def get_fields(self): fields = super(CommentSerializer, self).get_fields() fields['comment_children'] = CommentSerializer(many=True, required=False) return fields
API视图
class CommentAV(mixins.CreateModelMixin, generics.GenericAPIView): # This class only allows users to create comments but not list all comments. List all comments would # be too taxing for the server if the website got tons of comments. queryset = Comment.objects.viewable().filter(status=True) serializer_class = CommentSerializer def post(self, request, *args, **kwargs): return self.create(request, *args, **kwargs) def perform_create(self, serializer): # Overriding perform_create. Can create comment using the authenticated account. # Cannot pretend to be someone else to create comment on his or her behalf. commenter = self.request.user now = timezone.now() before_now = now - timezone.timedelta(seconds=COM_WAIT_TIME) # Make sure user can only create comment again after waiting for wait_time. this_user_comments = Comment.objects.filter(user=commenter, created_date__lt=now, created_date__gte=before_now) if this_user_comments: raise ValidationError(f'You have to wait for {COM_WAIT_TIME} seconds before you can post another comment.') elif Comment.objects.filter(user=commenter, level__gt=COMMENT_LEVEL_DEPTH): raise ValidationError(f'You cannot make another level-deep reply.') else: serializer.save(user=commenter) # By combining perform_create method to filter out only the owner of the comment can edit his or her own # comment -- and the permission_classes of IsAuthenticated -- allowing only authenticated user to create # comments. When doing custome permission - such as redefinte BasePermission's has_object_permission, # it doesn't work with ListCreateAPIView - because has_object_permission is meant to be used on single instance # such as object detail. permission_classes = [IsAuthenticated] class CommentAVAdmin(generics.ListCreateAPIView): queryset = Comment.objects.viewable() serializer_class = CommentSerializer permission_classes = [IsAdminUser] class CommentDetailAV(generics.RetrieveUpdateDestroyAPIView): queryset = Comment.objects.viewable().filter(status=True) serializer_class = CommentSerializer permission_classes = [CustomAuthenticatedOrReadOnly] def destroy(self, request, *args, **kwargs): instance = self.get_object() if not instance.user.id == self.request.user.id: return Response({ 'Error': 'Comment isn\'t deleted! Please log into the owner account of this comment to delete this comment.'}, status=status.HTTP_400_BAD_REQUEST) self.perform_destroy(instance) return Response({'Success': 'Comment deleted!'}, status=status.HTTP_204_NO_CONTENT) class CommentDetailAVAdmin(generics.RetrieveUpdateDestroyAPIView): queryset = Comment.objects.viewable() serializer_class = CommentSerializer permission_classes = [IsAdminUser] class CommentDetailChildrenAV(generics.RetrieveUpdateDestroyAPIView): queryset = Comment.objects.viewable().get_descendants().filter(status=True) serializer_class = CommentSerializer permission_classes = [CustomAuthenticatedOrReadOnly] def destroy(self, request, *args, **kwargs): instance = self.get_object() if not instance.user.id == self.request.user.id: return Response({ 'Error': 'Reply isn\'t deleted! Please log into the owner account of this reply to delete this reply.'}, status=status.HTTP_400_BAD_REQUEST) self.perform_destroy(instance) return Response({'Success': 'Comment deleted!'}, status=status.HTTP_204_NO_CONTENT)
博客应用中的API调用
add_comment = requests.post(BLOG_BASE_URL + f'api/post-list/comments/create-comments/', headers=headers, data=user_comment) add_reply = requests.post(BLOG_BASE_URL + f'api/post-list/comments/create-comments/', headers=headers, data=user_reply) requests.request('PUT', BLOG_BASE_URL + f'api/post-list/comments/{pk}/', headers=headers, data=user_comment) response = requests.request('PUT', BLOG_BASE_URL + f'api/post-list/comments/children/{pk}/', headers=headers, data=user_comment) response = requests.request('DELETE', BLOG_BASE_URL + f'api/post-list/comments/{pk}/', headers=headers)
已尝试的解决措施
- 执行
Comment.objects.rebuild(),未解决问题 - 在Python shell中可正常查询到出现问题的评论,能调用
get_ancestors方法,但API端点仍返回Not Found - 重启开发服务器后,所有评论可恢复正常的删改操作
- 了解到类似问题案例,但自身使用Django默认开发服务器,而非案例中的Apache
内容的提问来源于stack exchange,提问作者Vinh Nguyen
相关产品推荐
相关产品推荐

