Реклама

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

Реклама

Комментарии

  • #
  • 6 декабря 2017 г. 5:39

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

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

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

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

  • #
  • 6 декабря 2017 г. 11: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())
как думаете так тоже хорошо ?

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

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

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

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


так

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

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь
  • JaJay
  • 17 декабря 2017 г. 5:16

C++ - Тест 002. Константы

  • Результат 58 баллов
  • Очки рейтинга -2
  • JaJay
  • 17 декабря 2017 г. 4:55

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

  • Результат 93 баллов
  • Очки рейтинга 8
  • JaJay
  • 17 декабря 2017 г. 4:48

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

  • Результат 66 баллов
  • Очки рейтинга -1
Последние комментарии
  • EVILEG
  • 7 декабря 2017 г. 9:47

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

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

  • Bernar
  • 7 декабря 2017 г. 9:24

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

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

  • EVILEG
  • 6 декабря 2017 г. 11:30

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

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

  • Bernar
  • 6 декабря 2017 г. 11:19

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

сделал немного по другому class EArticleView(View): template_name = 'knowledge/article.html' comment_form = CommentForm def get(self, request, *args, **kwargs): ...

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

Пауза в многопоточности

QFuture, который возвращается QtConcurrent::map имеет методы pause() и resume() и теоретически должен поддерживать этот функционал. Но для Qt...

  • Миша
  • 15 декабря 2017 г. 11:26

Как найти в QVector макс и мин

Спасибо

  • Galant
  • 14 декабря 2017 г. 19:58

LPT

Понял! Спасибо!

  • EVILEG
  • 14 декабря 2017 г. 13:38

QCustomPlot можно ли построить прерывистую линию на одном графике?

Во-первых: В pro файле проект по идее достаточно указать следующий define для включения возможности рендеринга через OpenGL DEFINES += QCUSTOMPLOT_USE_OPENGL И во вторых:...

  • EVILEG
  • 13 декабря 2017 г. 8:05

В многопоточности выполнять действие только в одном из потоков

Статическиe методs QThread::currentThread(); и QThread::currentThreadId() могут возвращать указатель на поток и его handle id соответственно. Можете попробовать через как...