Реклама

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

TutorialDjangoКомментарии, Django, Python, Materialized Path1551

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

 

@EVILEG 30 сентября 2016 г. 22:42

Реклама

Реклама

Комментарии

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь

Реклама

Реклама