Проблема с вложенными комментариями при использовании MPTT в Django и Django Rest Framework API - результат не найден
Я пытаюсь создать систему вложенных комментариев с помощью MPTT, но использую Django Rest Framework для сериализации дерева MPTT. Я добился того, что вложенные комментарии работают - и эти комментарии добавляются, редактируются и удаляются только путем вызова конечных точек API Django Rest Framework - без использования вызовов Django ORM DB вообще. К сожалению, есть ошибка, которую я не смог разгадать! Хотя комментарии добавляются, редактируются и удаляются нормально - но когда седьмой или восьмой комментарий вложен - внезапно первый комментарий или первый вложенный комментарий становится [detail: Not found.] - то есть он возвращает пустой результат или выдает неизвестную ошибку валидации, которую я не смог понять почему. Это приводит к тому, что при нажатии на редактирование или удаление ошибочных комментариев становится невозможным - но часть GET в порядке, поскольку эти ошибочные комментарии действительно отображаются в разделе комментариев (или я должен сказать, что часть списка возвращается нормально). Изображение, которое я прикреплю, покажет, что когда я ввел комментарий ggggg, комментарии aaaa и bbbb будут выдавать ошибки при попытке отредактировать или удалить их. Если я удалю комментарий gggg, комментарий 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 для комментариев будут выглядеть следующим образом:
Вызовы API будут выглядеть следующим образом в blog_post app views:
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)
Эти вызовы в представлении приложения для записи блога позволят мне разрешить авторизованным пользователям создавать, редактировать и удалять комментарии.
Кто-нибудь знает, почему мое приложение получило эту ошибку? Любая помощь будет оценена по достоинству! Я где-то читал про получение ноды refresh_from_db() - но как мне это сделать в сериализации? Также, Comment.objects.rebuild() не помогает!
Оки, я догадался!
Я думаю, что при вызове одного и того же объекта в дереве MPTT для GET и PUT как-то выплевывается странная ошибка, которая не позволяет мне редактировать затронутые ответы. Итак, моим решением сейчас является просто создание конечной точки с API вида ниже:
class CommentChildrenAV(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().get_descendants().filter(status=True)
serializer_class = CommentSerializer
def get(self, request, pk):
replies = Comment.objects.viewable().get_descendants().filter(status=True, pk=pk)
serializer = CommentSerializer(replies, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
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]
Это представление API позволило бы мне передать pk ответа - получить JSON ответ следующим образом:
response = requests.request("GET", BLOG_BASE_URL + f'api/post-list/children/get-child/{pk}/', headers=headers)
Получив ответ в JSON, я могу получить исходное содержание ответа и ввести это содержание ответа в исходные данные формы ответа следующим образом:
edit_form = CommentForm(initial=original_comment_data)
Затем я получаю новое содержимое POST, которым пользователь хочет заменить содержимое оригинального ответа - суть решения, которое я сейчас использую, заключается в следующем: если пользователь аутентифицирован и если user_id оригинального JSON содержимого (имеется в виду комментатор текста ответа) совпадает с request.user.id - тогда я просто делаю:
if request.method == 'POST':
# I can't use API endpoint here to edit reply because some weird bug won't allow me to do so.
# Instead of calling the endpoint api for edit reply - I just update the database with
# using ORM (Object Relational Manager) method.
if request.user.is_authenticated:
print(content[0]['user_id'], os.getcwd())
if request.user.id == content[0]['user_id']:
Comment.objects.filter(status=True, pk=pk).update(content=request.POST.get(strip_invalid_html('content')))
post_slug = content[0]['post_slug']
return redirect('single_post', post_slug)
Теперь это действительно решает мою проблему! Я просто надеялся, что мне не придется обманывать, идя по пути ORM для редактирования ответа. Я бы предпочел 100% вызовы API для всех действий в этом приложении. Вздох... но теперь мое приложение полностью функционирует в плане наличия системы комментариев, которая вложена с помощью пакета MPTT.