Evgenii Legotckoi
Evgenii Legotckoi30 вересня 2016 р. 12:42

Django - Підручник 011. Додавання коментарів до сайту на основі Django

Взявшись за реалізацію коментарів на сайті під Django, я з подивом виявив, що Django не надає жодних модулів для реалізації коментарів. Вірніше він надавав його раніше, це був модуль django.contrib.comments , але у версії 1.7 його оголосили як deprecated і запропонували або пиляти самостійно, або скористатися чимось подібним до Disqus. Добре , він ніби теж підтримує підсвічування синтаксису коду, але... у статтях одне підсвічування, у коментарях інше - це буде некрасиво.

Тому впроваджуватимемо власний велосипед і ловитимемо свої баги.

Для реалізації коментарів необхідно:

  • Додати нову модель, назвемо її Comment;
  • Додати подання, яке оброблятиме додавання коментаря;
  • Додати форму для введення коментаря;
  • Скористатися для організації деревоподібної структури підходом Materialized Path ;

Модель Comment

Модель коментарів міститиме такі поля:

  • path - міститиме масив цілісних значень, який міститиме повний шлях до кореня. Як було зазначено у статті по Materialized Path, це ID всіх батьківських елементів;
  • article_id - зовнішній ключ на статтю, в якій знаходиться коментар;
  • author_id - зовнішній ключ до автора коментар;
  • content - сам коментар;
  • pub_date - дата та час публікації коментаря;

Крім цього дані методи get_offset() , який визначатиме рівень зсуву коментаря по довжині шляху, і get_col() , який визначатиме кількість колонок у сітці, які займатимуть коментар, а також перевизначено метод str , який відповідатиме за відображення частини вмісту коментаря в адмінці.

Зсув і кількість колонок організовуватимуть деревоподібне відображення коментарів на сторінці, але зсув буде не більше 6 колонок, оскільки на даний момент сітка розбивається на 12 колонок.

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('Комментарий')
    pub_date = models.DateTimeField('Дата комментария', 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

Коментар надсилає на сайт за допомогою POST запиту за певною адресою, яку необхідно описати у файлі 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'),
]

Як я вже говорив у багатьох статтях, я працюю в модулі knowledge зі статтями та розділами, але цього разу я виніс статті в окремий модуль, щоб уніфікувати URL-статей, щоб вони не залежали від розділів і не втрачалася індексація в тому у випадку, якщо стаття буде переміщена до іншого розділу. Тому тепер робота йде з модулем post . Туди ж буде надіслано і коментар. Відправлятися коментар в результаті буде на шляху **post/comment/12, якщо користувач коментує статті з ID = 12.

Форма коментаря

Форму коментаря буде розміщено в окремому файлі 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 дуже важливе, оскільки воно міститиме ID батьківського коментаря, яке автоматично підставлятиметься при відповіді на один із коментарів під статтею. Користувач його заповнювати не буде і буде прихованим. До того ж, його заповнення не є обов'язковим, оскільки коментар може відноситися безпосередньо до статті.

Ну а ставлення до певного коментаря робиться за допомогою скрипта JavaScript.

Уявлення

Для додавання коментаря я обмежився лише методом, без жодного уявлення, тим більше, що декоратор @login_required простіше прикручується до методу, ніж до уявлення.

Також було модифіковано подання, що відповідає за відображення статті, оскільки необхідно було включити форму коментаря в контекст сторінки статті, що віддається, якщо, звичайно, користувач авторизований.

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)
        # Помещаем в контекст все комментарии, которые относятся к статье
        # попутно сортируя их по пути, 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_to_response(template_name=self.template_name, context=context)

# Декораторы по которым, только авторизованный пользователь 
# может отправить комментарий и только с помощью POST запроса
@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 не позволяет увидеть ID комментария по мы не сохраним его, 
        # хотя PostgreSQL имеет такие средства в своём арсенале, но пока не будем
        # работать с сырыми SQL запросами, поэтому сформируем path после первого сохранения
        # и пересохраним комментарий 
        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())

Шаблон статті з коментарем

Я вже говорив, що використовую модуль django-bootstrap3 для верстки сторінок? Тому не дивуйтеся тому, як зверстаний цей шаблон.

Коментарі являють собою звичайні рядки в Grid системі Bootstrap, а деревовидність досягається рахунок зсуву колонок.

Дуже важливою тут є наявність у кожного рядка id={{ comment.id }} - Це саме те значення, яке буде підставлятися у приховане поле форми, якщо користувач коментує не статтю, а якийсь із коментарів.

Тому ID за допомогою JavaScript буде переміщатися форма коментаря по сторінці. А поміщатиметься форма за допомогою функції show_comments_form(). Ця функція міститься в обробник посилання "Відповісти", у кожного коментаря, а також в обробник посилання, просто для написання коментаря. Ця функція використовує бібліотеку jQuery. Тому не забуваймо її підключити у вашому базовому шаблоні. У мене підключається та версія, яка використовується з 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')">Написать комментарий</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 для переміщення коментарів по сторінці


Ну, тут все просто до неподобства. Якщо id = write_comment , то означає, що коментар має перший рівень і приховане поле буде порожнім, а форма переміщується під напис "Написати коментар". Інакше заповнюємо приховане поле та поміщаємо його під коментарем , під яким даємо відповідь.

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);
}

Для Django рекомендую VDS-сервера хостера Timeweb .

Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Стабільний хостинг, на якому розміщується соціальна мережа EVILEG. Для проектів на Django радимо VDS хостинг.

Вам це подобається? Поділіться в соціальних мережах!

bernar92
  • 06 грудня 2017 р. 00:39

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

Evgenii Legotckoi
  • 06 грудня 2017 р. 03:18

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

В моём случае это приложение post.
app_name = 'post'
Evgenii Legotckoi
  • 06 грудня 2017 р. 03:22

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

bernar92
  • 06 грудня 2017 р. 06:19

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

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
  • 06 грудня 2017 р. 06:30
Да, так будет даже лучше, я на сайте уже обновил до такого вида код

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

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

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


bernar92
  • 07 грудня 2017 р. 04:30

так

Evgenii Legotckoi
  • 07 грудня 2017 р. 04:47

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

ИМ
  • 01 лютого 2018 р. 13:58

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

Не подскажете с чем может быть связано?
Evgenii Legotckoi
  • 01 лютого 2018 р. 16:20

Добрый день.

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

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


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) 







Evgenii Legotckoi
  • 16 листопада 2018 р. 01:50

Добрый день!

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


progammist
  • 20 травня 2020 р. 07:15

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

Evgenii Legotckoi
  • 20 травня 2020 р. 16:07

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

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

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

progammist
  • 21 травня 2020 р. 04:39
  • (відредаговано)

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

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

Evgenii Legotckoi
  • 22 травня 2020 р. 02:43

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

progammist
  • 28 травня 2020 р. 13:56

path = ArrayField(models.IntegerField())

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

Evgenii Legotckoi
  • 28 травня 2020 р. 15:31

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

progammist
  • 28 травня 2020 р. 15:42
  • (відредаговано)

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

Evgenii Legotckoi
  • 28 травня 2020 р. 15:49

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

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

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

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

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

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

b
  • 21 березня 2022 р. 05:29

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

Evgenii Legotckoi
  • 17 квітня 2022 р. 17:36

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

NSProject
  • 15 червня 2022 р. 06:03

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

{{ comment.text|safe }}

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

Evgenii Legotckoi
  • 15 червня 2022 р. 06:50
NSProject
  • 15 червня 2022 р. 07:27

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

Evgenii Legotckoi
  • 15 червня 2022 р. 07:28

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

Коментарі

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

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:50бали,
  • Рейтинг балів-4
m
  • molni99
  • 26 жовтня 2024 р. 01:37

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80бали,
  • Рейтинг балів4
m
  • molni99
  • 26 жовтня 2024 р. 01:29

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:20бали,
  • Рейтинг балів-10
Останні коментарі
ИМ
Игорь Максимов22 листопада 2024 р. 11:51
Django - Підручник 017. Налаштуйте сторінку входу до Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 жовтня 2024 р. 14:37
Django - Урок 064. Як написати розширення для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 жовтня 2024 р. 08:19
Читалка файлів fb3 на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов05 жовтня 2024 р. 07:51
Django - Урок 064. Як написати розширення для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas505 липня 2024 р. 11:02
QML - Урок 016. База даних SQLite та робота з нею в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Тепер обговоріть на форумі
Evgenii Legotckoi
Evgenii Legotckoi24 червня 2024 р. 15:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 листопада 2024 р. 06:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject04 червня 2022 р. 03:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9Anonim25 жовтня 2024 р. 09:10
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Слідкуйте за нами в соціальних мережах