Комментарии на сайт используя Django

Вступление

Привет. Если ты, как и я, находишься на том этапе что:
  • Не знаешь как реализовать комментирование на своём сайте ?
  • Ищешь возможности и варианты реализации для этого самого комментирования ?
  • Просто хочешь готовое приложение/решение которое можно было бы легко интегрировать на свой собственный сайт ?
Тогда ты попал по-верному URL-адресу.
В этой статье я опишу то как можно реализовать простую систему комментирования. Которая бы совмещала возможность комментировать как для гостей, так и для зарегистрированных пользователей.
Хочу заметить, моей целью не было написание такой системы комментирования, которой свет не видывал. Эта система подходит для меня и моего сайта. Если и вам она подходит, замечательно. Ну а если нет, я уверен, что из этой статьи можно много чего подчеркнуть для себя.

Виды и варианты

Как комментирование вообще можно реализовать и какие есть подходы/варианты? Такой вот вопрос я себя задал, когда впервые потребовалось, добавить комментирование мне на сайт.

Анонимное комментирование

Это самое простое в реализации, но при этом самое опасное. Почему опасное ?
Тут, возможно, я не совсем корректно выразился, но под опасным я имел в виду то, что абсолютно любой человек или бот сможет оставить комментарии. В абсолютно любом количестве, и абсолютно любого качества.
Ты ведь сам понимаешь, анонимность даёт людям свободу делать и писать всё что угодно. И по своему опыту знаю, если у человека есть возможность, поднасрать да ещё и безнаказанно, обязательно найдутся желающие.

Аутентифицированное комментирование

Комментируют только зарегистрированные пользователи.
Если вы разрабатываете социальную сеть или форум или интернет магазин, то эта форма комментирования для вас. Важно помнить, регистрация должна предоставлять некие преимущества тем кто зарегистрировался.
Опять повторюсь, если у вас блог, новостной портал или информационный портал, то не советую. Человеческая лень, это очень тяжело преодолимое препятствие.

Что же выбрал я? И как оно работает у меня.

У меня смешанная. Как анонимусы, так и зарегистрированные пользователи могут оставлять комментарии под постами.
Представим, вы посетили мой сайт. Вы прочитали интересующую вас статью и решили оставить комментарий.
Скролите до конца, пишете то о чём думаете, жмякаете кнопку, Оставить комментарий.
Выскакивает новая форма, на кнопке, и просит вас ввести ваше имя. Вы вводите его.
Форма оставления комментария изменилась, появилось ваше имя.
Вы, жмякаете, Оставить комментарий ещё раз. Он появляется у вас на странице.
Теперь перейдём к той части, где я пошагово объясняю, как реализовать данную систему комментирования.

Создаём модель комментария

Начнём с модели Comment. Она будет хранить все наши комментарии.

Подготавливаем шаблон комментария и комментариев

Вот шаблон для одного комментария

			
			
<div class="comments_el">
	<div class="comments_el__meta">
		<div class="comments_el__meta_el comments_el__user">
			<div class="comments_el__user_avatar">
				{% if not com.anonymous_user_id|length > 0%}
					{% if com.user.avatar|length > 0 %}
					<img class="icon icon_avatar" src='{% get_media_prefix %}{{ com.user.avatar }}' alt="Аватарка пользователя">
					{% else %}
					<img class="icon icon_avatar" src="{% static 'User/img/default_avatar.png' %}" alt="Аватар по умолчанию">
					{% endif %}
				{% else%}
					<img class="icon icon_avatar" src="{% static 'User/img/default_avatar.png' %}" alt="Аватар по умолчанию">
				{% endif %}
			</div>
			<div class="comments_el__user_name">
				{% if not com.anonymous_user_id|length > 0%}
					{{com.user}}
				{% else %}
					{{com.anonymous_user_name}}
				{% endif %}
			</div>
		</div>
		<div class="comments_el__meta_el">
			{{com.timeCreated}}
		</div>
	</div>
	<div class="comments_el__content">
		{{com.content}}
	</div>
	{% if user.slug == com.user.slug and request.session.is_auth %}
	<div id="comment_remove__button" class="comment_remove__button comment_button" commentid="{{com.id}}" >
		<div id="page_add__case" class="page_add__case">
			<div id="plus_description" class="case_tosite  plus_description_addComment">
				Удалить
			</div>
		</div>
	</div>
	{% endif %}
</div>
			
			
				
Поясню пару непонятных моментов в этом шаблоне.
Проверка, если комментарий написан анонимным пользователем

			
			
{% if not com.anonymous_user_id|length > 0%}
			
			
		
Проверка, установленна ли аватарка у пользователя, который опубликовал данный комментарий

			
			
{% if com.user.avatar|length > 0 %}
			
			
		
Теперь этот кусочек шаблона

			
			
{% if user.slug == com.user.slug and request.session.is_auth %}
<div id="comment_remove__button" class="comment_remove__button comment_button" commentid="{{com.id}}" >
	<div id="page_add__case" class="page_add__case">
		<div id="plus_description" class="case_tosite  plus_description_addComment">
			Удалить
		</div>
	</div>
</div>
{% endif %}
			
			
		
Это довольно важный кусочек, так сказать. Вкратце тут говорится, что только зарегистрированный пользователь сможет удалять свои комментарии.
В шаблоне для комментариев всё абсолютно точно так же как и для одного комментария. За тем исключением, что он нужен для рендеринга нескольких комментариев. То есть, там используется цикл for

Подготавливаем шаблон, где эти комментарии будут появляться

Теперь рассмотрим место, где эти комментарии будут отображаться.
Вот этот код я использую в base_post.html. То есть, это шаблон от которого у меня наследуются все остальные посты( статьи, кейсы, новости ). Короче говоря, предоставляет базовый функционал для моих статей. Ниже в главе Готовое приложение и другие ресурсы я оставил ссылку на файл целиком
По правде говоря я использую комментарии не только в постах. У каждого зарегистрированного пользователя есть свой кабинет, где он может просмотреть все оставленные им комментарии. И удалить, если захочет.
Расмотрим ту часть base_post.html которая отвечает за комментарии.
Сначала идёт сама форма оставления комментария

			
			
<div id="comment_add" class="comments_el">
	<div class="comments_el__meta_el comments_el__commentAdd">
		<div class="comments_el__user_avatar">
			{% if user.avatar|length > 0 %}
			<img class="icon icon_avatar" src='{% get_media_prefix %}{{ user.avatar }}' alt="Аватар пользователя" id="comments_el__user_avatar_commentAdd">
			{% else %}
			<img class="icon icon_avatar" src="{% static 'User/img/default_avatar.png' %}" alt="Аватар по умолчанию" id="comments_el__user_avatar_commentAdd">
			{% endif %}
		</div>
		<div id="comments_el__user_name_commentAdd" class="comments_el__user_name">
			{{user}}
		</div>
	</div>
	{% csrf_token %}
	<div class="form">
		<form enctype="multipart/form-data" class="form_form" id="toSignup" action="send_new_user" method="get">
			<textarea class="form_el__about" placeholder="Ваше сообщение" name="about" rows=4  id="about"></textarea>
		</form> 
	</div>
	...
</div>
			
			
		
Дальше есть кнопка оставить комментарий. Для аутентифицированных пользователей, она просто оставит комментарий. Но для анонимов, будет отображена другая кнопка ( кнопка всё таже, просто функционал у неё другой) Она запросит только имя анонима. После чего, будет заменена на обычную кнопку отправления комментария.
Для анонимов

			
			
{% if not request.session.is_auth %}
<div id="comment_add__buttonSendGuesting" class="active">
	<div id="page_add__case" class="page_add__case">
		<div id="plus_description" class="case_tosite authorized_addComment full_height comment_button next_button">
			Оставить комментарий}
		</div>
	</div>
</div>
<div id="comment_add__buttonShowLoginOptions">
	<div id="page_add__case" class="page_add__case">
		<div id="plus_description" class="case_tosite authorized_addComment full_height comment_button next_button">
			<div id="loginableSocialNets_text" class="">
				Оставить комментарий
			</div>
			<div id="loginableSocialNets" class="row active in_middle">
				<div id="parsingForm" class="row in_middle">
					<div class="parsingForm_text"></div>
					<div class="parsingForm_textarea">
						{% csrf_token %}
						<div class="form">
							<form enctype="multipart/form-data" class="form_form row" id="toVerifyViaSocNet"  action="prepare_user" method="get">
								<textarea class="form_el__about" placeholder="Твоё имя" name="username" rows=1  id="userID"></textarea>
							</form> 
						</div>
					</div>
				</div>
				<div id="parsingFormSubmit" class=" next_button in_middle padder basic_button">
					✔
				</div>
			</div>
		</div>
	</div>
</div>

			
			
		
Для зарегистрированных

			
			
{% else %}
<div id="comment_add__buttonSendAuthorized">
	<div id="page_add__case" class="page_add__case">
		<div id="plus_description" class="case_tosite authorized_addComment full_height comment_button next_button">
			Оставить комментарий
		</div>
	</div>
</div>
{% endif %}
			
			
		

Пишем ajax запрос для оставления комментария

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

			
			
{% block scripts %}
	<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
	{% csrf_token %}
	<script>
		const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
		const post_slug = "{{post.slug}}"
	</script>
	<script src="{% static 'Comment/js/comments.js' %}"></script>
	{% block scripts_post %}
	{% endblock %}
{% endblock %}
			
			
		
Небольшое пояснение. Сначала мы подключаем jQuery библиотеку.

			
			
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
			
			
		
Потом сохраняем csrf токен и то какой пост пользователь просматривает сейчас

			
			
{% csrf_token %}
<script>
	const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
	const post_slug = "{{post.slug}}"
</script>
			
			
		
Дальше подключаем наш скрипт для работы комментариев( там будут находиться наши ajax запросы )

			
			
    <script src="{% static 'Comment/js/comments.js' %}"></script>
			
			
		
И так как от этого шаблона наследуются шаблоны статей, новостей и кейсов, им могут быть необходимы отдельные скрипты ( или стили ). Поэтому это здесь.

			
			
    {% block scripts_post %}
    {% endblock %}
			
			
		
Когда нажимается кнопка Отправить коментарий(будь то, от авторизованного или анонимного пользователя) Выполняется данная функция.

function sendComment(path){
	var post = post_slug
	var about = $("#about").val()	
	var username = $("#comments_el__user_name_commentAdd").text()

	$.ajax({
		type: "POST",
		url: "/" + language_code + "/" + path + "/",
		data: {
			'post': post,
			'about': about,
			'username': username
		},
		headers: {'X-CSRFToken': csrftoken},
		mode: 'same-origin', // Do not send CSRF token to another domain.
		success: function(result){
			$(result).insertAfter("#comment_add")
			$(".comment_remove__button").off()
			$(".comment_remove__button").one('click', function() {
				removeComment(this)
			})	
		},
	})
}
		
Где мы, отправляем на сервер имя текущего поста, сообщение, и имя пользователя

var post = post_slug
var about = $("#about").val()	
var username = $("#comments_el__user_name_commentAdd").text()
		
При успехе, мы рендерим comment.html и вставляем полученый результат над всеми комментариями. И ещё не забываем добавить событие для возможности удаления данного комментария.

$(result).insertAfter("#comment_add")
$(".comment_remove__button").off()
$(".comment_remove__button").one('click', function() {
	removeComment(this)
})	
		

Пишем ajax запрос для загрузки комментариев

Загружать комментарии на сервер это отлично, но и другие пользователи должны видеть комментарии. Вот функция для отображения комментариев для одного поста.

function loadComments(){
	var post = post_slug
	$.ajax({
		type: "POST",
		url: "/" + language_code + "/load_comments/",
		data: {
			'post': post,
			'number': number,
			'offset': offset,
		},
		headers: {'X-CSRFToken': csrftoken},
		mode: 'same-origin', // Do not send CSRF token to another domain.
		success: function(result){
			$(result).insertBefore("#scroll-sentinel")
			$(".comment_remove__button").off()
			$(".comment_remove__button").one('click', function() {
				removeComment(this)
			})	
			offset = offset + number
		},
	})
}
		
Важно отметить, эта функция срабатывает, когда полоса прокрутки дошла до конца. Для этого используется специальный наблюдатель, который и следит за этим.

$(document).ready(function(){
	default_avatar_path = $("#comments_el__user_avatar_commentAdd").attr('src')
	const observer = new IntersectionObserver((entries, observer) => {
	  // Loop through the entries
	  for (const entry of entries) {
		// Check if the entry is intersecting the viewport
		if (entry.isIntersecting) {
			// Load more content
			loadComments()
		}
	  }
	});
	const scrollSentinel = document.querySelector("#scroll-sentinel");
	observer.observe(scrollSentinel);
	...

		

Пишем ajax запрос для удаления комментария

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

function removeComment(toRemove){
	var post = post_slug
	var comment_id = $(toRemove).attr("commentid")
	$.ajax({
		type: "POST",
		url: "/" + language_code + "/remove_comment/",
		data: {
			'post': post,
			'comment_id': comment_id,
		},
		headers: {'X-CSRFToken': csrftoken},
		mode: 'same-origin', // Do not send CSRF token to another domain.
		success: function(result){
			// Removes only client side part
			$(toRemove).parent().remove()
		},
	})
}
		
Как ты заметил, чтобы удалить комментарий нужен его id. Это кастомный атрибут, который мы заполняем когда рендерим комментарий или комментарии.
Вот он

			
			
{% if user.slug == com.user.slug and request.session.is_auth %}
<div id="comment_remove__button" class="comment_remove__button comment_button" commentid="{{com.id}}" >
			
			
		

Настраиваем представления и адреса

Почти всё готово. Осталось только разобраться с маршрутами и самими представлениями. Comment/urls.py выглядит так:

from django.urls import path
from .views import * 


urlpatterns = [
	path('send_comment_guesting/', send_comment_guesting),
	path('send_comment_authorized/', send_comment_authorized),
	path('remove_comment/', remove_comment),
	path('prepare_user/', prepare_user),
	path('load_comments/', load_comments),
	path('load_comments_by_user/', load_comments_by_user),
]
		
	
Разберём каждое представление по отдельности. Кроме, конечно, load_comments_by_user. Эту функцию ты сможешь посмотреть и сам если захочешь. Мы разбираем комментирование на примере постов.

def send_comment_guesting(request):
	media_root = settings.MEDIA_URL
	if request.method == 'POST':
		type = Post.objects.filter(slug=request.POST['post']).get()
		content = request.POST['about']
		username = request.POST['username']
		user_id = request.session.session_key
		comment = Comment(
				anonymous_user_id=user_id,
				anonymous_user_name=username,
				type=type,
				content=content
		)
		comment.save()
	context = {
		'com': comment,
		'media_root': media_root,
	}
	return render(request, "Comment/comment.html", context=context)
		
	
    Сохраняем комментарий как анонимный. Где:
  • type ➜ пост к которому принадлежит комментарий
  • content ➜ содержимое комментария
  • username ➜ имя анонимного пользователя
  • user_id ➜ текущий id сессии
Дальше, создаётся запись в базе данных и рендерится страница с коментарием.

def send_comment_authorized(request):
	media_root = settings.MEDIA_URL
	if request.method == 'POST':
		user = User.objects.filter(name=request.session.get('username')).get()
		type = Post.objects.filter(slug=request.POST['post']).get()
		content = request.POST['about']
		comment = Comment(user=user, type=type, content=content)
		comment.save()
	context = {
		'com': comment,
		'user': user,
		'media_root': media_root,
	}
	return render(request, "Comment/comment.html", context=context)
		
	
Сохраняем комментарий как авторизованный. Почти всё тоже самое, только здесь добавляется объект пользователя, который добавил комментарий.

def prepare_user(request):
	data = {
			'isValid': False,
			'username': None,
			}
	if request.method == "GET":
		username = request.GET['username']
		data['isValid'] = True
		data['username'] = username

	return JsonResponse(data)
		
	
Можно считать данную функцию служебной ведь, единственная задача данной функции это вставить имя пользователя в форму для создания комментария.

def remove_comment(request):
	if request.method == 'POST':
		comment_id = request.POST['comment_id']
		comment = Comment.objects.filter(id=comment_id).get()
		comment.delete()
		status = 200

	return JsonResponse({}, status=status)
		
	
Удаляем комментарий по id.

def load_comments(request):
	try:
		user = User.objects.filter(name=request.session.get('username')).get()
	except:
		user = None

	media_root = settings.MEDIA_URL
	if request.method == 'POST':
		number = request.POST.get('number', 5)
		offset = request.POST.get('offset', 0)
		type = Post.objects.filter(slug=request.POST['post']).get()
		comments = Comment.objects.filter(type=type).order_by('-timeCreated')[(int(offset)):(int(number)) + (int(offset))]

	context = {
		'comments': comments,
		'user': user,
		'media_root': media_root,
	}
	return render(request, "Comment/comments.html", context=context)

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

try:
	user = User.objects.filter(name=request.session.get('username')).get()
except:
	user = None
		
	
Дальше, мы определяем сколько, с какого комментария и с какой статьи загрузить комментарии.

number = request.POST.get('number', 5)
offset = request.POST.get('offset', 0)
type = Post.objects.filter(slug=request.POST['post']).get()
comments = Comment.objects.filter(type=type).order_by('-timeCreated')[(int(offset)):(int(number)) + (int(offset))]
		
	
После рендерим найденные комментарии и возвращаем пользователю. Он возвращается здесь:

function loadComments():
	...
	success: function(result){
		$(result).insertBefore("#scroll-sentinel")
		$(".comment_remove__button").off()
		$(".comment_remove__button").one('click', function() {
			removeComment(this)
		})	
		offset = offset + number
	},

		
	
Где мы вставляем полученные комментарии на страницу, увеличиваем счётчик и назначаем событие для удаления комментария.

Готовое приложение и другие ресурсы

Пост Скриптум. Вывод

Итак, теперь когда статья закончилась я бы хотел извиниться за то что статья не вышла такой, какой она должна была выйти. Сейчас поясню.
Изначально я планировал рассказать про 3 возможных варианта написания комментариев.
Но увы, как только я понял, как много это времени у меня займёт, я решил немного поостудить свой пыл. И написать статью именно о моём пути написания системы комментирования. То есть, комбинированный вариант анонимного/зарегистрированного комментирования.
Ещё я хотел вставить формы оставления комментариев по каждому виду. То есть, начать с самого простого, потом перейти к комментированию через социальные сети, и закончить всё тем что комментировать смогут лишь зарегистрированые пользователи. Которых кстати у меня ещё нет.
Может, когда-нибудь, я напишу данную статью в том виде что хотел, ну а пока имеем, что имеем.

сердце 4
3 соединённые точки 0

Коментарии

(1)
Аватар по умолчанию
None
Оставить комментарий
Оставить комментарий