Реклама

Django - Урок 011. Добавление комментариев на сайт с Django

Django, Python, MaterializedPath

Взявшись за реализацию комментариев на сайте под 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);
}

Реклама

Комментарии

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь
  • BlinCT
  • 22 октября 2017 г. 12:46

C++ - Тест 003. Условия и циклы

  • Результат 64 баллов
  • Очки рейтинга -1
  • Kiops
  • 22 октября 2017 г. 3:56

C++ - Тест 001. Первая программа и типы данных

  • Результат 86 баллов
  • Очки рейтинга 6
  • Kiops
  • 22 октября 2017 г. 2:41

Qt - Тест 001. Сигналы и слоты

  • Результат 100 баллов
  • Очки рейтинга 10
Последние комментарии
  • EVILEG
  • 21 октября 2017 г. 3:06

Qt/C++ - Урок 031. QCustomPlot - строим график по времени

Добавил архив с проектом

  • EVILEG
  • 20 октября 2017 г. 20:06

Qt/C++ - Урок 031. QCustomPlot - строим график по времени

После работы поищу, должен где-то быть на винте.

  • Миша
  • 20 октября 2017 г. 20:04

Qt/C++ - Урок 031. QCustomPlot - строим график по времени

не могли бы вы выложить архив с рабочей версией скрипта?

  • EVILEG
  • 20 октября 2017 г. 20:03

Qt/C++ - Урок 030. QCustomPlot - быстрый старт в работе с графиками

Использование дизайнера в Qt Creator и использование ui файлов является распространённой практикой в Qt фреймворке. Написать отдельную статью про то, что это такое? - может быть. Опи...

  • Миша
  • 20 октября 2017 г. 19:43

Qt/C++ - Урок 030. QCustomPlot - быстрый старт в работе с графиками

Но почему вы это не описали? Не могли бы вы описать.

Сейчас обсуждают на форуме
  • EVILEG
  • 22 октября 2017 г. 12:05

Закрепление якорей в момент создания объекта через JS

Добрый день! Якоря - это не те свойства, которые можно устанавливать сразу по инициализации, лучше их править после создания объекта, поскольку при одновременной установке они могут в...

  • EVILEG
  • 21 октября 2017 г. 23:33

Создание истории редактирования постов на сайте

Ясно. Тогда я лучше не буду тратить время на его проверку. Тем более, что я использую гугловский prettyprint для подсветки кода. Спасибо за информацию.

QFile::copy() возвращает false

Получилось! Спасибо огромное! path1 = "C:/Users/555/Pictures/00GAF13AP001-002.jpg"true

  • cordsac
  • 19 октября 2017 г. 15:49

How can I select the QGraphicView Item and change the properties

Ok I'll check it sir,If you can please do article(tutorial) about this,Its really useful.Thank you if you can give me some sample code when you free.thanks again

  • cordsac
  • 17 октября 2017 г. 19:28

How can I open SVG file through QT

Okay,Thank you sir :)