Создание системы комментариев

⏱ 30 мин · 🧩 заданий: 6

План

  1. Модель Comment.
  2. Регистрация в админке (модерация).
  3. Форма CommentForm.
  4. View для обработки комментария.
  5. Вывод комментариев и формы в шаблоне поста.

Модель

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name="comments")
    name = models.CharField("Имя", max_length=80)
    email = models.EmailField()
    body = models.TextField("Комментарий")
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    active = models.BooleanField("Активен", default=True)

    class Meta:
        ordering = ["created"]
        indexes = [models.Index(fields=["created"])]
        verbose_name = "комментарий"
        verbose_name_plural = "комментарии"

    def __str__(self):
        return f"Комментарий {self.name} к «{self.post}»"
  • related_name="comments" → post.comments.all().
  • active позволит скрывать неуместные комментарии, не удаляя их.
python manage.py makemigrations blog
python manage.py migrate

Админка

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display = ["name", "email", "post", "created", "active"]
    list_filter = ["active", "created", "updated"]
    search_fields = ["name", "email", "body"]
    list_editable = ["active"]

Форма

from .models import Comment


class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ["name", "email", "body"]

Поля post, active, даты в форму не включаем — пользователь не должен их задавать.

View для отправки комментария

from django.views.decorators.http import require_POST

from .forms import CommentForm, EmailPostForm


@require_POST
def post_comment(request, post_id):
    post = get_object_or_404(Post, id=post_id, status=Post.Status.PUBLISHED)
    comment = None
    form = CommentForm(data=request.POST)
    if form.is_valid():
        comment = form.save(commit=False)   # объект создан, но не сохранён
        comment.post = post                 # дописываем то, чего нет в форме
        comment.save()
    return render(request, "blog/post/comment.html", {"post": post, "form": form, "comment": comment})

save(commit=False) — ключевой приём: получить объект модели из формы, дополнить его и только потом сохранить.

Маршрут:

path("<int:post_id>/comment/", views.post_comment, name="post_comment"),

Обновляем post_detail

def post_detail(request, year, month, day, post):
    post = get_object_or_404(Post, status=Post.Status.PUBLISHED, slug=post,
                             publish__year=year, publish__month=month, publish__day=day)
    comments = post.comments.filter(active=True)
    form = CommentForm()
    return render(request, "blog/post/detail.html", {"post": post, "comments": comments, "form": form})

Шаблоны

Фрагмент формы blog/post/includes/comment_form.html:

<h2>Добавить комментарий</h2>
<form action="{% url 'blog:post_comment' post.id %}" method="post">
  {% csrf_token %}
  {{ form.as_p }}
  <p><input type="submit" value="Отправить"></p>
</form>

В detail.html после текста поста:

{% with comments.count as total_comments %}
  <h2>{{ total_comments }} comment{{ total_comments|pluralize }}</h2>
{% endwith %}

{% for comment in comments %}
  <div class="comment">
    <p class="info">
      Комментарий {{ forloop.counter }} от {{ comment.name }}, {{ comment.created|date:"d.m.Y H:i" }}
    </p>
    {{ comment.body|linebreaks }}
  </div>
{% empty %}
  <p>Комментариев пока нет.</p>
{% endfor %}

{% include "blog/post/includes/comment_form.html" %}

Страница результата blog/post/comment.html:

{% extends "blog/base.html" %}

{% block title %}Комментарий{% endblock %}

{% block content %}
  {% if comment %}
    <h2>Ваш комментарий добавлен.</h2>
    <p><a href="{{ post.get_absolute_url }}">Вернуться к посту</a></p>
  {% else %}
    {% include "blog/post/includes/comment_form.html" %}
  {% endif %}
{% endblock %}

Если форма невалидна, на этой странице снова показывается форма с ошибками и введёнными данными.

Безопасность

Текст комментария выводится через {{ comment.body|linebreaks }} — Django экранирует HTML, поэтому вставленный <script> не выполнится. Никогда не применяйте |safe к пользовательскому вводу.

Улучшения

  • Перенаправлять на пост после комментария (redirect(post.get_absolute_url() + "#comments")) вместо отдельной страницы.
  • Хранить автора-пользователя вместо имени и email, когда появится авторизация.
  • Модерация: active=False по умолчанию и публикация из админки.
  • Древовидные ответы на комментарии и AJAX — в проекте «Сайт статей».

Итог

  • Comment связан с Post через ForeignKey с related_name="comments".
  • ModelForm + save(commit=False) для заполнения скрытых полей.
  • @require_POST для view, принимающей только отправку формы.
  • post.comments.filter(active=True) — только видимые комментарии.

Практика решено 0 из 6

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Написать код

Опишите модель Comment: связь post с Post (каскад, related_name="comments"), name (CharField 80), email (EmailField), body (TextField), created (дата создания, автоматически), active (BooleanField, по умолчанию True).

Задание 2 Написать код

Создайте CommentForm (ModelForm для Comment) с полями name, email, body.

Задание 3 Написать код

Допишите обработку формы в post_comment: если форма валидна, создайте комментарий без сохранения (commit=False), привяжите его к посту и сохраните.

Задание 4 Один вариант ответа

Как в post_detail получить только активные комментарии поста?

Задание 5 Написать код

Переменная total — число комментариев. Выведите подпись по-английски: 1 comment, но 3 comments. Окончание s добавьте фильтром pluralize.

Задание 6 Один вариант ответа

Зачем view post_comment обёрнута в @require_POST?

Войдите, чтобы отмечать пройденные уроки.