Создание модели древовидных комментариев Django
Для создания древовидных комментариев нам понадобится модуль django-mptt. В уроке 4 мы его устанавливали. Поэтому можете зайти в урок 4 и освежить память.
Более оптимизированный обновленный вариант древовидных комментариев на моём втором сайте
Напоминаю, что я использую декомпозицию в своих проектах. Но вы можете создавать модели и другие django элементы так, как угодно Вам.
Я же создам файл comments.py в папочке models, нашего приложения blog.
modules/blog/models/comments.py
from django.contrib.auth.models import User
from django.db import models
from mptt.fields import TreeForeignKey
from mptt.models import MPTTModel
from modules.blog.models import Article
class Comment(MPTTModel):
"""
Модель комментариев для блога статей с возможностью вложенности.
Подключен плагин MPTT для вложенности.
"""
article = models.ForeignKey(Article, on_delete=models.CASCADE, verbose_name='Статья', related_name='comments', related_query_name='comment')
author = models.ForeignKey(User, verbose_name='Автор материала', on_delete=models.CASCADE, related_name='comments_author')
content = models.TextField(verbose_name='Текст комментария', max_length=1500)
created_at = models.DateTimeField(verbose_name='Дата добавления', auto_now_add=True, db_index=True)
updated_at = models.DateTimeField(verbose_name='Дата обновления', auto_now=True, db_index=True)
is_published = models.BooleanField(verbose_name='Опубликовать', default=True)
is_fixed = models.BooleanField(verbose_name='Зафиксировать', default=False)
parent = TreeForeignKey('self', verbose_name='Родительский комментарий', null=True, blank=True, db_index=True, related_name='children', on_delete=models.CASCADE)
class MTTMeta:
"""
Сортировка по вложенности
"""
order_insertion_by = ('-created_at',)
class Meta:
"""
Сортировка, название модели в админ панели, таблица в данными
"""
ordering = ('-is_fixed', '-created_at')
verbose_name = 'Комментарий'
verbose_name_plural = 'Комментарии'
db_table = 'app_comments'
def __str__(self):
"""
Возвращение заголовка статьи
"""
return f'{self.author} написал под статьей: {self.article.title}'
Пояснения:
- С помощью MPTTModel я создаю древовидную систему комментариев.
- Ссылаюсь на Article (статью), так как комментарий закрепляется за статьей.
- Ссылаюсь на User (автора комментария)
- order_insertion_by - сортировка по вложенности
Не забываем, если используете структуру папок как у меня, то надо добавить нашу модель в __init__.py
modules/blog/models/__init__.py
from modules.blog.models.articles import Article
from modules.blog.models.categories import Category
from modules.blog.models.comments import Comment
__all__ = ('Article', 'Category', 'Comment')
Вроде бы и ничего сложного. Приступим к созданию формы комментария.
Аналогично модели я создам comments.py в папке с формами.
modules/blog/forms/comments.py
from django import forms
from modules.blog.models import Comment
class CommentCreateForm(forms.ModelForm):
"""
Форма добавления комментариев к статьям
"""
parent = forms.IntegerField(widget=forms.HiddenInput, required=False)
content = forms.CharField(label='', widget=forms.Textarea(attrs={'cols': 30, 'rows': 5, 'placeholder': 'Комментарий', 'class': 'form-control'}))
class Meta:
model = Comment
fields = ('content',)
Пояснения:
- Поле parent скрыто от наших глаз. Оно будет работать через JavaScript при добавлении с сайта.
- Из всех полей нам нужно добавить в форму content и поле parent, где будем хранить id комментария, на который мы хотим ответить.
Добавим форму в __init__.py
modules/blog/forms/__init__.py
from modules.blog.forms.articles import ArticleCreateForm
from modules.blog.forms.comments import CommentCreateForm
__all__ = '__all__'
Отлично. Теперь нам нужно создать представление для создания комментариев.
В папке views я создам comments.py
modules/blog/views/comments.py
from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse
from django.views.generic import CreateView
from modules.blog.forms import CommentCreateForm
from modules.blog.models import Comment, Article
class CommentCreateView(LoginRequiredMixin, CreateView):
"""
Создание комментариев для статей
"""
model = Comment
form_class = CommentCreateForm
to_model = Article
def is_ajax(self):
return self.request.headers.get('X-Requested-With') == 'XMLHttpRequest'
def form_invalid(self, form):
if self.is_ajax():
return JsonResponse({'error': form.errors}, status=400)
def form_valid(self, form):
if self.is_ajax():
comment = form.save(commit=False)
comment.article_id = self.to_model.objects.get(pk=self.kwargs['pk']).pk
comment.author = self.request.user
try:
comment.parent_id = self.model.objects.get(pk=form.cleaned_data['parent']).pk
except ObjectDoesNotExist:
comment.parent_id = None
comment.save()
return JsonResponse({
'comment_is_child': comment.is_child_node(),
'comment_id': comment.id,
'comment_author': comment.author.username,
'comment_parent_id': comment.parent_id,
'comment_created_at': comment.created_at.strftime('%Y-%b-%d %H:%M:%S'),
'comment_avatar': comment.author.profile.get_avatar,
'comment_content': comment.content,
'comment_get_absolute_url': comment.author.profile.get_absolute_url()
}, status=200)
def handle_no_permission(self):
return JsonResponse({'error': 'Необходимо авторизоваться'}, status=400)
Пояснения:
- Это наш почти привычный CreateView, но с некоторыми модификациями для работы с JsonResponse и JavaScript.
- Так как в Django 4.1 нет метода is_ajax, я добавил его вручную.
- comment.parent_id я буду получать из формы, скрытого поля.
Не забываем про __init__.py при декомпозиции и структуры как у меня:
modules/blog/views/__init__.py
from modules.blog.views.articles import *
from modules.blog.views.comments import *
__all__ = '__all__'
Также добавим представление в urls.py
from django.urls import path, include
from modules.blog.views import ArticleListView, ArticleDetailView, ArticleByCategoryListView, ArticleCreateView, \
ArticleUpdateView, ArticleDeleteView, CommentCreateView
urlpatterns = [
path('', ArticleListView.as_view(), name='home'),
path('cat/<int:pk>/<str:slug>/', ArticleByCategoryListView.as_view(), name='article-by-cat'),
path('articles/', include([
path('', ArticleListView.as_view(), name='article-list'),
path('create/', ArticleCreateView.as_view(), name='article-create-view'),
path('<int:pk>/comments/create/', CommentCreateView.as_view(), name='comment-create-view'), #new
path('<str:slug>/', ArticleDetailView.as_view(), name='article-detail'),
path('<str:slug>/update/', ArticleUpdateView.as_view(), name='article-update-view'),
path('<str:slug>/delete/', ArticleDeleteView.as_view(), name='article-delete-view'),
])),
]
Давайте зарегистрируем нашу модель в админке и создадим миграции:
(venv) PS C:\Users\Razilator\Desktop\Courses\App\backend> python manage.py makemigrations
Migrations for 'blog':
modules\blog\migrations\0004_comment.py
- Create model Comment
(venv) PS C:\Users\Razilator\Desktop\Courses\App\backend> python manage.py migrate
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions, sites, system
Running migrations:
Applying blog.0004_comment... OK
(venv) PS C:\Users\Razilator\Desktop\Courses\App\backend>
modules/blog/admin.py
@admin.register(Comment)
class CommentAdminPage(DraggableMPTTAdmin):
"""
Админ-панель модели комментариев
"""
list_display = ('tree_actions', 'indented_title', 'article', 'author', 'created_at', 'is_published')
mptt_level_indent = 2
list_display_links = ('article',)
list_filter = ('created_at', 'is_fixed', 'author')
list_editable = ('is_published',)
def get_queryset(self, request):
return super().get_queryset(request).select_related('author', 'article')
Пояснения:
- Наследуюсь от класса DraggableMPTTAdmin, который добавляет открытие и закрытие тредов в админке.
- Добавляю метод get_queryset, с использованием select_related, что мы использовали в 30 уроке.

Давайте приступим к реализации JavaScript кода для добавления mptt комментариев...