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> {{ 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" %} Ответить</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" %} Комментировать</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 .
не совсем понятно как это реализовать куда импортировать view и urls подскажите !
views и urls должны присутсвовать в вашем приложении, например сайт в Django состоит из нескольких приложений, которые создаются через команду startapp, по умолчанию там всегда есть директории urls и views .
Если говорить про то, куда в итоге подключить urls , то они должны быть подключены в главный файл urls, он должен располагаться там же, где и файл settings.py
сделал немного по другому
есть визуальный пример ?
так
Визуальный пример чего? комментариев?
При ответе на конкретный комментарий рядом с ником отвечающего будет стрелочка и указание ник другого пользователя. Который будет ссылкой на комментарий, на который был дан ответ.
Доброго времени суток Евгений. У меня ка то так получилось:
Добрый день.
Доброго времени суток Евгений. Не подскажете что я делаю не так? Получаю ошибку такого характера:
Говорит что ошибка в views в этой строке:
Добрый день!
шаблон не находит, или шаблон неправильно прописали, или тег шаблона неправильно написан, иных выводов сделать не могу, из того, что вы написали. трейсбек нужно смотреть. Создайте тему на форуме и выложить трейсбек с ошибкой.
А какие меры можно принять, чтобы обеспечить защиту от спама?
И еще как можно реализовать, чтобы по комменты выводились в админке и их можно было отклонять/одобрять?
Самая первая мера - это комментарии только для зарегистрированных пользователей.
Пока ресурс маленький, то и спамеров почти нет, которые готовы зарегистрироваться на сайте ради спам комментария.
Ну а для регистрации такие ограничения как подтверждение email и recaptcha.
У меня так спамеры активно начали появляться только когда посещаемость перевалила за 1500 в день.
Что касается одобрения и отклонения, то у меня просто есть поле moderation в каждой модели и action для админки, которые помечают контент как спам или модерированный контент. Но если учесть, что обычно спаммер регистрируется и только спамит. То я просто удаляю весь акканут со всем контентом спамера, а ег email заношу в чёрный список, чтобы с этого email регистрация больше не проходила. Для меня это вопрос двух действий и 10 секунд, а для спамера, который вручную этим занимается - это очень накладно.
Ну и чтобы голову не греть лишний раз, то у меня просто есть прокси модели, которые сразу фильтруют в админке только немодерированный контент.
А action - это функция одобрения/отклонения?
У меня эта часть кода выведена в open source, смотрите здесь
ArrayField подходит только для postresql, а для mysql - выдает ошибку. Как можно переписать эту строку, чтобы на mysql работало?
Я бы вам посоветовал выкинуть mysql на помойку, но вы меня наверное не послушаете.
Скорее или конвертировать последовательность Integer значений в строку и сохранять в обычный CharField или извращаться с ManyToManyField.
Даже не знаю, что из этих двух вариантов будет хуже. Один порожадает лишний оверхед с преобразованиями, а второй оверхед с запросами.
Ни то ни другое не адекватное, но при шлифовке наверное будет работать.
а в чем явное преимущество postgresql над mysql?)
Он более функциональный и его функционал объективно лучше поддерживается Django.
Из первого, что приходит на ум:
Честно, я так сразу не вспомню, но когда сам задавался вопросом, то столкнулся с тем, что рекомендуют при разработке на Джанго использовать Postgres.
Вообще самая рекомендуемая связка стека - это Django/PostgreSQL/Nginx.
Как бы можно и MySQL, но вы скорее увидите на StackOverflow вопрос о том как сделать в MySQL так, как это делается в PostgreSQL, а не наоборот.
А для меня это уже повод задуматься.
Извините, проверка функциональности, визуально здесь не увидел древовидной иерархии в комментах (вроде я тоже не дерево:))
зы спасибо за интересные статьи!
В рамках развития сайта выпилил полностью. Ибо неудобно было поддерживать. Да и всё-таки не нравятся они мне из-за избыточной траты полезного пространства. Не говоря уже о том, что на мобильных платформах это выглядит отвратно.
Меня интересует как экранировать или преобразовывать теги которые нежелательны при добавлении в бд. Ибо текст комментария выводится
то есть как html. А без safe это просто набор текста и всё
тыц Django - Урок 038. Использование BeatifulSoup 4 для очистки публикуемого контента от нежелательных html тегов
Плохо я однако пользовался поиском по сайту. Спасибо
Поиск не совсем хорошо работает, так что норм