Evgenii Legotckoi
Evgenii LegotckoiSept. 30, 2016, 12:42 p.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

urls.py

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(
        widget=forms.HiddenInput,
        required=False
    )

    comment_area = forms.CharField(
        label="",
        widget=forms.Textarea
    )

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.

Views

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 = {}
        context.update(csrf(request))
        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
@login_required
@require_http_methods(["POST"])
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']
        comment.save()

        # 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
        try:
            comment.path.extend(Comment.objects.get(id=form.cleaned_data['parent_comment']).path)
            comment.path.append(comment.id)
        except ObjectDoesNotExist:
            comment.path.append(comment.id)

        comment.save()

    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 %}
    <article>
        <h1>{{ article.article_title }}</h1>
        {{ article.article_content|safe }}
    </article>
    <h2>Комментарии</h2>
    {% 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>
                    <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 %}
                    </div>
                </div>
            </div>
        </div>
    {% 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 %}
        </form>
    {% else %}
        <div class="panel panel-warning">
            <div class="panel-heading">
                <h3 class="panel-title">Комментарии</h3>
            </div>
            <div class="panel-body">
                Только авторизованные пользователи могут оставлять комментарии.<br />
            </div>
        </div>
    {% 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')
    {
        $("#id_parent_comment").val('')
    }
    else
    {
        $("#id_parent_comment").val(parent_comment_id);
    }
    $("#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.

Do you like it? Share on social networks!

bernar92
  • Dec. 6, 2017, 12:39 a.m.

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

Evgenii Legotckoi
  • Dec. 6, 2017, 3:18 a.m.

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

В моём случае это приложение post.
app_name = 'post'
Evgenii Legotckoi
  • Dec. 6, 2017, 3:22 a.m.

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

bernar92
  • Dec. 6, 2017, 6:19 a.m.

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

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 = {}
        context.update(csrf(request))
        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 запроса
    @method_decorator(login_required)
    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(
                    path=[],
                    article_id=article,
                    author_id=request.user,
                    content=form.cleaned_data['comment_area']
                )
                comment.save()

                # Django не позволяет увидеть ID комментария по мы не сохраним его,
                # хотя PostgreSQL имеет такие средства в своём арсенале, но пока не будем
                # работать с сырыми SQL запросами, поэтому сформируем path после первого сохранения
                # и пересохраним комментарий
                try:
                    comment.path.extend(Comment.objects.get(id=form.cleaned_data['parent_comment']).path)
                    comment.path.append(comment.id)
                    print('получилось')
                except ObjectDoesNotExist:
                    comment.path.append(comment.id)
                    print('не получилось')
                comment.save()
            return redirect(article.get_absolute_url())
как думаете так тоже хорошо ?

Evgenii Legotckoi
  • Dec. 6, 2017, 6:30 a.m.
Да, так будет даже лучше, я на сайте уже обновил до такого вида код

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

Что касается древовидных комментариев, то я от них как видите отказался. Я просто добавляю ID комментария, на который был дан ответ. Древовидные комментарии в итоге оказались не очень удобны для сайта с вставками программного кода.
bernar92
  • Dec. 7, 2017, 4:24 a.m.

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


bernar92
  • Dec. 7, 2017, 4:30 a.m.

так

Evgenii Legotckoi
  • Dec. 7, 2017, 4:47 a.m.

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

ИМ
  • Feb. 1, 2018, 1:58 p.m.

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

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

Добрый день.

Скорее всего поля неправильно были получены. Что-то не так с запросом. То есть в полученном объекте значения values не были корректно забраны. Либо просто неправильно написали названия полей в шаблоне
ИМ
  • Nov. 15, 2018, 4:35 p.m.

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


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) 







Добрый день!

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


progammist
  • May 20, 2020, 7:15 a.m.

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

Evgenii Legotckoi
  • May 20, 2020, 4:07 p.m.

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

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

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

progammist
  • May 21, 2020, 4:39 a.m.
  • (edited)

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

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

Evgenii Legotckoi
  • May 22, 2020, 2:43 a.m.

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

progammist
  • May 28, 2020, 1:56 p.m.

path = ArrayField(models.IntegerField())

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

Evgenii Legotckoi
  • May 28, 2020, 3:31 p.m.

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

progammist
  • May 28, 2020, 3:42 p.m.
  • (edited)

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

Evgenii Legotckoi
  • May 28, 2020, 3:49 p.m.

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

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

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

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

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

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

b
  • March 21, 2022, 5:29 a.m.

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

Evgenii Legotckoi
  • April 17, 2022, 5:36 p.m.

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

NSProject
  • June 15, 2022, 6:03 a.m.

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

{{ comment.text|safe }}

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

Evgenii Legotckoi
  • June 15, 2022, 6:50 a.m.
NSProject
  • June 15, 2022, 7:27 a.m.

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

Evgenii Legotckoi
  • June 15, 2022, 7:28 a.m.

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

Comments

Only authorized users can post comments.
Please, Log in or Sign up
AD

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

  • Result:50points,
  • Rating points-4
m

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

  • Result:80points,
  • Rating points4
m

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

  • Result:20points,
  • Rating points-10
Last comments
Evgenii Legotckoi
Evgenii LegotckoiNov. 1, 2024, 12:37 a.m.
Django - Lesson 064. How to write a Python Markdown extension Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEOct. 19, 2024, 6:19 p.m.
Fb3 file reader on Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовOct. 5, 2024, 5:51 p.m.
Django - Lesson 064. How to write a Python Markdown extension Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5July 5, 2024, 9:02 p.m.
QML - Lesson 016. SQLite database and the working with it in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
k
kmssrFeb. 9, 2024, 5:43 a.m.
Qt Linux - Lesson 001. Autorun Qt application under Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Now discuss on the forum
Evgenii Legotckoi
Evgenii LegotckoiJune 25, 2024, 1:11 a.m.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Nov. 15, 2024, 5:04 p.m.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectJune 4, 2022, 1:49 p.m.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9AnonimOct. 25, 2024, 7:10 p.m.
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Follow us in social networks