Evgenij LegotskojSept. 30, 2016, 11:42 a.m.

Django - Tutorial 011. Adding comments to the site based on Django

When I worked with the implementation of comments on the site for Django, I was surprised to find that Django does not provide any modules for implementation comments. Rather it gave him before, it was a unit django.contrib.comments, but in version 1.7 it was announced as deprecated and offered either to cut yourself, or you can use something like Disqus. Well, it sort of code also supports syntax highlighting, but ... in the articles one light, the other in the comments - it will be ugly.

Therefore I decided to write my own comments on the site.

To implement the necessary comments:

  • Add a new model, call it Comment ;
  • Add an idea that will handle the addition of a comment;
  • Add a form to add a comment;
  • Use Materialized Path to organize tree structure;

Comment model

Model comments will contain the following fields:

  • path - It will contain an array of integers, which will contain the full path to the root. As mentioned in the article on Materialized Path, is the ID of the parent element;
  • article_id - a foreign key to an article in which there is a comment;
  • author_id - a foreign key to the author's comment;
  • content - message;
  • pub_date - date and time of the publication of the comment;

In addition, given get_offset() methods, which will define the comment shift the level along the length of the path, and get_col() , which will determine the number of columns in the grid, which will take a comment and overridden method __str__ , which will be responsible for the display of the contents of a comment in the admin area.

The shift and the number of columns will organize a tree display of comments on the page, but the shift is not more than 6 columns, because at the moment the grid is divided into 12 columns.

class Comment(models.Model):
    class Meta:
        db\_table = "comments"

    path = ArrayField(models.IntegerField())
    article\_id = models.ForeignKey(Article)
    author\_id = models.ForeignKey(User)
    content = models.TextField('Comment')
    pub\_date = models.DateTimeField('Date of comment', default=timezone.now)

    def \_\_str\_\_(self):
        return self.content[0:200]

    def get\_offset(self):
        level = len(self.path) - 1
        if level > 5:
            level = 5
        return level

    def get\_col(self):
        level = len(self.path) - 1
        if level > 5:
            level = 5
        return 12 - level


Comments sent to the site by using the POST request on a particular address, which must be described in the file urls.py .

from django.conf.urls import url

from . import views

app\_name = 'post'
urlpatterns = [
    url(r'^(?P<article\_id>[0-9]+)/$', views.EArticleView.as\_view(), name='article'),
    url(r'^comment/(?P<article\_id>[0-9]+)/$', views.add\_comment, name='add\_comment'),

As I have said in many articles, I work in the module knowledge with articles and sections, but this time I brought the article into a separate module to unify the URL of the article, so they are not dependent on the sections and is not lost indexation, if Article It will be moved to another section. Therefore, the work is now talking with the post module. There also will be sent to and comment. Post comments in the end will be the path of post/comment/12 , if the user says the article with ID = 12.

Comment form

The comment form will be placed in a separate file forms.py .

Обработка формы и сохранение комментария будет происходить в представлении, поэтому метода save здесь нет. Данная форма служит лишь для ввода комментария и отсылки его на сервер.

from django import forms

from .models import Comment

class CommentForm(forms.Form):

    parent\_comment = forms.IntegerField(

    comment\_area = forms.CharField(

parent_comment field is very important, as it will contain the ID of the parent comment, which will be automatically supplied when responding to one of the comments under the article. The user will not fill it and it will be hidden. In addition, it is not mandatory to fill, as a comment may refer directly to the article.

But the attitude to a particular comment made by JavaScript scripts.


To add a comment, I was limited only by, without any representations, especially since decorator @login_required may implemented easly to the method than the presentation.

It is also responsible for the representation of articles mapping has been modified since it was necessary to include a comment in the form of given article page context, unless the user is authorized.

from django.views import View
from django.shortcuts import render\_to\_response, get\_object\_or\_404, redirect
from django.contrib import auth
from django.http import Http404
from django.contrib.auth.decorators import login\_required
from django.views.decorators.http import require\_http\_methods
from django.core.exceptions import ObjectDoesNotExist
from django.template.context\_processors import csrf

from knowledge.models import Article, Comment
from knowledge.forms import CommentForm

class EArticleView(View):
    template\_name = 'post/article.html'
    comment\_form = CommentForm

    def get(self, request, *args, **kwargs):
        article = get\_object\_or\_404(Article, id=self.kwargs['article\_id'])
        context = {}
        user = auth.get\_user(request)
        # Put in the context of all the comments that are relevant to the article 
        # simultaneously sorting them along the way, the auto-increment ID, 
        # so the problems with the hierarchy should not have any comments yet
        context['comments'] = article.comment\_set.all().order\_by('path')
        context['next'] = article.get\_absolute\_url()
        # We add form only if the user is authenticated
        if user.is\_authenticated:
            context['form'] = self.comment\_form

        return render\_to\_response(template\_name=self.template\_name, context=context)

# Decorators in which only authorized user 
# able to send comment via POST request
def add\_comment(request, article\_id):

    form = CommentForm(request.POST)
    article = get\_object\_or\_404(Article, id=article\_id)

    if form.is\_valid():
        comment = Comment()
        comment.path = []
        comment.article\_id = article
        comment.author\_id = auth.get\_user(request)
        comment.content = form.cleaned\_data['comment\_area']

        # Django does not allow to see the comments on the ID, we do not save it,
        # Although PostgreSQL has the tools in its arsenal, but it is not going to
        # work with raw SQL queries, so form the path after the first save
        # And resave a comment
        except ObjectDoesNotExist:


    return redirect(article.get\_absolute\_url())

Article Template comment

Did I mention that I use django-bootstrap3 module for page layout? So do not be surprised as this template are laid out.

Comments are ordinary lines in the Grid Bootstrap system, and the tree is achieved due to the shift of columns.

Very important here is the fact that each row id = {{comment.id}} - This is exactly the same value that will be entered in a hidden form field if the user does not comment on the article, and some of the comments.

On the same ID with JavaScript will move the comment form on a page. A form will be placed via show_comments_form() function. This function is placed in a handler reference "Reply" at every comment, as well as links to the handler, just to write a comment. This feature uses the jQuery library. So do not forget to connect it to your base template. I connected the version that is used with Bootstrap .

{% extends 'home/base.html' %}
{% load bootstrap3 %}
{% block page %}
        <h1>{{ article.article\_title }}</h1>
        {{ article.article\_content|safe }}
    {% for comment in comments %}
        <a name="comment-{{ comment.id }}"></a>
        <div class="row" id="{{ comment.id }}">
            <div class="col-md-{{ comment.get\_col }} col-md-offset-{{ comment.get\_offset }}">
                <div class="panel panel-default">
                    <div class="panel-heading">
                        <strong>{{ comment.author\_id.get\_full\_name|default:comment.author\_id.username }}</strong>&nbsp;&nbsp;
                        {{ comment.pub\_date }}
                        <a href="#comment-{{ comment.id }}">#</a>
                    <div class="panel-body">
                        <div>{{ comment.content|safe }}</div>
                        {% if form %}<a class="btn btn-default btn-xs pull-right"
                                        onclick="return show\_comments\_form({{ comment.id }})">
                            {% bootstrap\_icon "share-alt" %}&nbsp;&nbsp;Ответить</a>
                        {% endif %}
    {% endfor %}
    {% if form %}
        <h3 id="write\_comment"><a onclick="return show\_comments\_form('write\_comment')">Write a comment.</a></h3>
        <form id="comment\_form" action="{% url 'post:add\_comment' article.id %}" method="post">
        {% csrf\_token %}
        {% bootstrap\_form form %}
        {% buttons %}
            <button type="submit" class="btn btn-primary">{% bootstrap\_icon "comment" %}&nbsp;&nbsp;Комментировать</button>
        {% endbuttons %}
    {% else %}
        <div class="panel panel-warning">
            <div class="panel-heading">
                <h3 class="panel-title">Комментарии</h3>
            <div class="panel-body">
                Только авторизованные пользователи могут оставлять комментарии.<br />
    {% endif %}
{% endblock %}

JavaScript to move the comments page

Well, everything is simple to outrageous. If id = write_comment , it means that a comment is the first level and a hidden field will be empty, and the form is moved by the words "Write a comment." Otherwise fill in a hidden field and put it under the commentary, under which we give the answer.

function show\_comments\_form(parent\_comment\_id)
    if (parent\_comment\_id == 'write\_comment')
    $("#comment\_form").insertAfter("#" + parent\_comment\_id);

For Django I recommend VDS-server of Timeweb hoster .

We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.
Support the author Donate

не совсем понятно как это реализовать куда импортировать view и urls подскажите !

views и urls должны присутсвовать в вашем приложении, например сайт в Django состоит из нескольких приложений, которые создаются через команду startapp, по умолчанию там всегда есть директории urls и views .

В моём случае это приложение post.
app_name = 'post'

Если говорить про то, куда в итоге подключить urls , то они должны быть подключены в главный файл urls, он должен располагаться там же, где и файл settings.py

сделал немного по другому

class EArticleView(View):
    template_name = 'knowledge/article.html'
    comment_form = CommentForm

    def get(self, request,  *args, **kwargs):
        article = get_object_or_404(Article, id=self.kwargs['article_id'])
        context = {}
        user = auth.get_user(request)
        context['article'] = article
        # Помещаем в контекст все комментарии, которые относятся к статье
        # попутно сортируя их по пути, ID автоинкрементируемые, поэтому
        # проблем с иерархией комментариев не должно возникать
        context['comments'] = article.comment_set.all().order_by('path')
        context['next'] = article.get_absolute_url()
        # Будем добавлять форму только в том случае, если пользователь авторизован
        if user.is_authenticated:
            context['form'] = self.comment_form

        return render(request, template_name=self.template_name, context=context)

    # Декораторы по которым, только авторизованный пользователь
    # может отправить комментарий и только с помощью POST запроса
    def post(self, request, *args, **kwargs):
        if request.method == 'POST':

            form = CommentForm(request.POST)
            article = get_object_or_404(Article, id=self.kwargs['article_id'])
            if form.is_valid():
                comment = Comment(

                # Django не позволяет увидеть ID комментария по мы не сохраним его,
                # хотя PostgreSQL имеет такие средства в своём арсенале, но пока не будем
                # работать с сырыми SQL запросами, поэтому сформируем path после первого сохранения
                # и пересохраним комментарий
                except ObjectDoesNotExist:
                    print('не получилось')
            return redirect(article.get_absolute_url())
как думаете так тоже хорошо ?

Да, так будет даже лучше, я на сайте уже обновил до такого вида код

Вот это уже не нужно
if request.method == 'POST':
Поскольку Вы и так используете метод post, то есть эта проверка избыточна.

Что касается древовидных комментариев, то я от них как видите отказался. Я просто добавляю ID комментария, на который был дан ответ. Древовидные комментарии в итоге оказались не очень удобны для сайта с вставками программного кода.

есть визуальный пример ?


Визуальный пример чего? комментариев?
При ответе на конкретный комментарий рядом с ником отвечающего будет стрелочка и указание ник другого пользователя. Который будет ссылкой на комментарий, на который был дан ответ.


Доброго времени суток Евгений. У меня ка то так получилось:

Не подскажете с чем может быть связано?

Добрый день.

Скорее всего поля неправильно были получены. Что-то не так с запросом. То есть в полученном объекте значения values не были корректно забраны. Либо просто неправильно написали названия полей в шаблоне

Доброго времени суток Евгений. Не подскажете что я делаю не так? Получаю ошибку такого характера:

Reverse for 'add_comment' with arguments '('',)' not found. 1 pattern(s) tried: ['comment\\/(?P<article_id>[0-9]+)\\/$']

Говорит что ошибка в views в этой строке:

return render(request, template_name=self.template_name, context=context) 

Добрый день!

шаблон не находит, или шаблон неправильно прописали, или тег шаблона неправильно написан, иных выводов сделать не могу, из того, что вы написали. трейсбек нужно смотреть. Создайте тему на форуме и выложить трейсбек с ошибкой.

А какие меры можно принять, чтобы обеспечить защиту от спама?
И еще как можно реализовать, чтобы по комменты выводились в админке и их можно было отклонять/одобрять?

Самая первая мера - это комментарии только для зарегистрированных пользователей.
Пока ресурс маленький, то и спамеров почти нет, которые готовы зарегистрироваться на сайте ради спам комментария.
Ну а для регистрации такие ограничения как подтверждение email и recaptcha.
У меня так спамеры активно начали появляться только когда посещаемость перевалила за 1500 в день.

Что касается одобрения и отклонения, то у меня просто есть поле moderation в каждой модели и action для админки, которые помечают контент как спам или модерированный контент. Но если учесть, что обычно спаммер регистрируется и только спамит. То я просто удаляю весь акканут со всем контентом спамера, а ег email заношу в чёрный список, чтобы с этого email регистрация больше не проходила. Для меня это вопрос двух действий и 10 секунд, а для спамера, который вручную этим занимается - это очень накладно.

Ну и чтобы голову не греть лишний раз, то у меня просто есть прокси модели, которые сразу фильтруют в админке только немодерированный контент.

поле moderation в каждой модели и action для админки, которые помечают контент как спам или модерированный контент.

А action - это функция одобрения/отклонения?

У меня эта часть кода выведена в open source, смотрите здесь

path = ArrayField(models.IntegerField())

ArrayField подходит только для postresql, а для mysql - выдает ошибку. Как можно переписать эту строку, чтобы на mysql работало?

Я бы вам посоветовал выкинуть mysql на помойку, но вы меня наверное не послушаете.
Скорее или конвертировать последовательность Integer значений в строку и сохранять в обычный CharField или извращаться с ManyToManyField.
Даже не знаю, что из этих двух вариантов будет хуже. Один порожадает лишний оверхед с преобразованиями, а второй оверхед с запросами.
Ни то ни другое не адекватное, но при шлифовке наверное будет работать.

а в чем явное преимущество postgresql над mysql?)

Он более функциональный и его функционал объективно лучше поддерживается Django.

Из первого, что приходит на ум:

  • Это наличие полей типа Array
  • Поддержка полей для JSON
  • Хорошая поддержка GIS функционала, также есть и батарейки соответствующие

Честно, я так сразу не вспомню, но когда сам задавался вопросом, то столкнулся с тем, что рекомендуют при разработке на Джанго использовать Postgres.

Вообще самая рекомендуемая связка стека - это Django/PostgreSQL/Nginx.

Как бы можно и MySQL, но вы скорее увидите на StackOverflow вопрос о том как сделать в MySQL так, как это делается в PostgreSQL, а не наоборот.
А для меня это уже повод задуматься.


Извините, проверка функциональности, визуально здесь не увидел древовидной иерархии в комментах (вроде я тоже не дерево:))
зы спасибо за интересные статьи!

В рамках развития сайта выпилил полностью. Ибо неудобно было поддерживать. Да и всё-таки не нравятся они мне из-за избыточной траты полезного пространства. Не говоря уже о том, что на мобильных платформах это выглядит отвратно.

Меня интересует как экранировать или преобразовывать теги которые нежелательны при добавлении в бд. Ибо текст комментария выводится

{{ comment.text|safe }}

то есть как html. А без safe это просто набор текста и всё

Плохо я однако пользовался поиском по сайту. Спасибо

Поиск не совсем хорошо работает, так что норм


Only authorized users can post comments.
Please, Log in or Sign up
Card image cap
Pulsum Via

Project for travelers from EVILEG.


Let me recommend you a great European Fornex hosting.

Fornex has proven itself to be a stable host over the years.

For Django projects I recommend VPS hosting

Following the link you will receive a 5% discount on shared hosting services, dedicated servers, VPS and VPN

View Hosting
Share on social networks

The EVILEG project has switched to a non-commercial basis and will develop solely on the enthusiasm of the site creator, the enthusiasm of users, donations and the hosting referral system

Thank you for your support

Available ways to support the project



C ++ - Test 004. Pointers, Arrays and Loops

  • Result:10points,
  • Rating points-10

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:0points,
  • Rating points-10

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:50points,
  • Rating points-4
Last comments

Android and QML - Adding Splash Screen

Интересен формат иконки, если это png, то как решается проблема scalability? не растягивается ли лого на китайфонах с 1280х2500? У меня просто сплеш скрин с градиентом и логотипом, и вот несколь…

Qt/C++ - Lesson 023. Moving QGraphicsItem on QGraphicsScene with mouse help

FIGURE Abdominopelvic regions. Zjuaqd https://newfasttadalafil.com/ - Cialis Cialis Recommendations for preparing children and adolescents for invasive cardiac procedures a statement…

How to use nested forms in Django

Спасибо за полезную статью. Подскажите пожалуйста, что делать если нужно реализовать большее количество вложенных форм? Например если на модель Address ссылается fk другой модели, на котору…

Qt/C++ - Lesson 051. QMediaPlayer – simple audio player

Не думаю, QMediaPlayer в один поток проигрывает. Если вам нужно одновременное воспроизведение нескольких аудиоисточников, то вам нужна Bass audio library , насколько знаю, её обычно и…

Qt/C++ - Lesson 051. QMediaPlayer – simple audio player

есть такая вообще возможность ?
Now discuss on the forum

Sorting the added QML elements in the ListModel

I am writing an alarm clock in QML, I am required to sort the alarms in ascending order (depending on the date or time (if there are several alarms on the same day). I've done the sorting …

Изменение поведения QGroupBox при клике на его чекбокс

Я вынес виджеты вынес за пределы QGroupBox в итоге.

QSqlRelatipnalTabelModel Qt 4.8.1 как получить id внешней связи?

Есть еще принципиально другой вариант решить раз и навсегда вопрос с полей id внешней связи. Это форкнуть Qt 4.8.1 QSqlTableModel, то есть создать свою ветку развития. Например создадим кл…

Добавление AndroidManifest.xml в cmake

Добрый день. Как добавить AndroidManifest.xml в cmake? Это не работвет set(ANDROID_PACKAGE_SOURCE_DIR ${PROJECT_SOURCE_DIR}/android CACHE INTERNAL "")set(ANDROID_BUID_DIR ${CMAKE_C…

Событие wheelEvent для виджета QLineEdit

вот что получилось: gui.py from PyQt5 import QtCore, QtGui, QtWidgets class LineEdit(QtWidgets.QLineEdit): def wheelEvent(self, event): #print("_") delta = 1 if e…
© EVILEG 2015-2022
Recommend hosting TIMEWEB