Создание сайта на Django: Урок 31, система древовидных комментариев: часть 1 (модель, форма, представление)
avatar
7 | (offline)
❤️‍🔥Notehunter Developer
Добавлено:
Категория: Руководства «Django»
Комментариев: 0

Создание модели древовидных комментариев 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 комментариев... 

Комментарии к статье 0
Комментариев нет
Форма добавления комментария (необходима регистрация)