Commenting system for a website, using the Django framework

Introduction

    Hi. If you, like me, are asking the same questions, like:
  • How do I implement a commenting system on my own website?
  • What are the options and possible solutions?
  • Is there any ready-to-use user solutions( Django app)? With easy integration into my own website.
If so, you are in the right URL address.
In this article, I will describe my way of solving this kind of problem, I mean commenting and replying on the website. And this system will allow commenting, for guests, and for registered users.
Also, I want to make a note that my goal is not to make such a commenting system, that no one has ever seen in their lives. The goal was to make commenting right fit      for my website, that's it. And if this works for you too, great. If not, then I am sure that you always can      make some notes and learn something new for yourself.

Types and variations

How comments can be implemented? And what ways and variants to choose when implementing your own commenting system ?This is the question I asked myself when for the first time I needed to add commenting to the site.
    I chose 2 main types of system commenting, based on who can leave comments, the rest are just variations and tips.
  • Anonymous commenting (Any rogue will be able to leave a comment on your site)
  • Authenticated commenting (Commenting for registered users only)

Anonymous commenting

This is the easiest to implement, but at the same time the most dangerous. Why dangerous?
Here, perhaps, I didn’t answer quite correctly, but my dangerous point was that absolutely any person or bot should leave comments. In absolutely any quantity and absolutely any quality.
You yourself understand that anonymity gives people the freedom to do and write whatever they want. And from my own experience I know If a person has the opportunity to give a shit and even with impunity, those who wish to do so must be polite.

Authenticated commenting

Only registered users comment.
If you are creating an economic network, forum or online store, this is the comment form for you. Important note: Registration must provide some benefits to those who register.
I repeat again, if you have a blog, news portal or information portal, I do not recommend it.

What did I choose? And how it works for me.

I have a mixed one. Both anonymous and registered users can leave comments under posts.
Let's imagine you visited my website. You have read the article you are interested in and decided to leave a comment.
Scroll to the end, write what you think, and press the “Leave a comment” button.
A new form pops up when you click a button and asks you to enter your name. You entered it.
The message form has changed and your name has appeared.
You press, “Leave a comment” again. It appears on your page.
Now let's move on to the part where I explain step by step how to implement this commenting system.

Create a Comment model

Let's start with the Comment model. She will store all our comments.
    What fields do we need?
  • type ➜ field that indicates which article it relates to
  • user ➜ what user left the comment (registered one)
  • anonymous_user_name ➜ The name you chose for yourself
  • anonymous_user_id ➜ Required to be able to create an anonymous comment from a non-anonymous one
  • content ➜ actual content of comment
  • timeCreated ➜ when it was written

Prepare comment and comments templates

    In general, we need 3 templates, at least
  • one for rendering one comment (will be used when you need to add a new comment)
  • one for rendering a group of comments (will be used when they need to be loaded)
  • and another one where these comments will be
Here is a template for one 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">
				Remove
			</div>
		</div>
	</div>
	{% endif %}
</div>
			
			
				
Let me clarify a couple of unclear points in this template.
Checking if a comment was written by an anonymous user

			
			
{% if not com.anonymous_user_id|length > 0%}
			
			
		
Checking whether the user who posted this comment has an avatar

			
			
{% if com.user.avatar|length > 0 %}
			
			
		
Now this piece of the template

			
			
{% 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">
			Remove
		</div>
	</div>
</div>
{% endif %}
			
			
		
This is a pretty important piece, so to speak. In short, it says that only registered user will be able to delete their comments.
In the comment template everything is absolutely the same as for one comment. Except that it is needed to render multiple comments. That is, it uses a for loop

Prepare a template where these comments will appear

Now let's consider the place where these comments will be displayed.
This is the code I use in base_post.html. That is, this is the template from which I inherit all other posts (articles, cases, news). In short, provides basic functionality for my articles. Below in the chapter "Completed application and other resources"
In truth, I use comments for more than just posts. Each registered The user has his own account where he can view all the comments he left. And delete if he wants.
Let's look at the part of base_post.html that is responsible for comments.
First comes the comment form itself.

			
			
<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>
			
			
		
Next there is a button to leave a comment. For authenticated users, she will simply leave a comment. But for anonymous users, another button will be displayed (the button is still the same, it just has different functionality) It will only ask for the anonymous name. After which, it will be replaced with a regular button for sending a comment.
For anonymous

			
			
{% 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">
			Leave a comment
		</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="">
			Leave a comment
			</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="Your name" 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>

			
			
		
For registered

			
			
{% 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">
			Leave a comment
		</div>
	</div>
</div>
{% endif %}
			
			
		

Write an ajax request to leave a comment

Templates are of course good, but now we need to connect the server part with the front end. Let's write an ajax script
Don't forget to include it in 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 %}
			
			
		
A little clarification. First we include the jQuery library.

			
			
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
			
			
		
Then we save the csrf token and what post the user is viewing now

			
			
{% csrf_token %}
<script>
	const csrftoken = document.querySelector('[name=csrfmiddlewaretoken]').value;
	const post_slug = "{{post.slug}}"
</script>
			
			
		
Next, we connect our script to make comments work (our ajax requests will be there)

			
			
    <script src="{% static 'Comment/js/comments.js' %}"></script>
			
			
		
And since templates for articles, news and cases are inherited from this template, it can be separate scripts (or styles) are required. That's why it's here.

			
			
    {% block scripts_post %}
    {% endblock %}
			
			
		
When the Leave a comment button is clicked (whether from an authorized or anonymous user) This function is running.

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)
			})	
		},
	})
}
		
Where we are sending the name of the current post, message, and username to the server

var post = post_slug
var about = $("#about").val()	
var username = $("#comments_el__user_name_commentAdd").text()
		
If successful, we render comment.html and paste the resulting result above all comments. And don’t forget to add an event to be able to delete this comment.

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

Write an ajax request to load comments

Uploading comments to the server is great, but other users should also see comments. Here is a function to display comments for a single post.

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
		},
	})
}
		
It is important to note that this function is triggered when the scroll bar has reached the end. For this purpose, a special observer is used, who monitors this.

$(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);
	...

		

Write an ajax request to delete a comment

And a small bonus. How to delete comments. From the previous sections, you saw how and where events are assigned to delete a comment.
Now let's see how it is removed.

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()
		},
	})
}
		
As you noticed, to delete a comment you need its id. This is a custom attribute that we we fill in when we render the comment or comments.
Here he is

			
			
{% 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}}" >
			
			
		

Setting up views and paths

Almost everything is ready. All that remains is to figure out the routes and the performances themselves. Comment/urls.py looks like this:

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),
]
		
	
Let's look at each presentation separately. Except, of course, load_comments_by_user. You can watch this function yourself if you want. We are reviewing comments using the example of posts.

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)
		
	
    We save the comment as anonymous пользователь. Where:
  • type ➜ post to which the comment belongs
  • content ➜ content of comment
  • username ➜ anonymous user name
  • user_id ➜ current session id
Next, a record is created in the database and the page with the comment is rendered.

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)
		
	
Save the comment as authorized. Almost everything is the same, only here the user object who added the comment is added.

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)
		
	
You can consider this function a service function, because the only task of this function is insert the username into the form to create a comment.

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)
		
	
Delete a comment by 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)

		
	
Loading comments.
First, we check whether an authorized user is asking for a download or not.

try:
	user = User.objects.filter(name=request.session.get('username')).get()
except:
	user = None
		
	
Next, we determine how many, from which comment and from which article to load comments.

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))]
		
	
Then we render the found comments and return them to the user. It returns here:

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

		
	
Where we insert the received comments into the page, increase the counter and assign an event for deletion comment.

Completed application and other resources

Post Scriptum. Conclusion

So, now that the article is over, I would like to apologize for the fact that the article did not turn out the way it should have. go out. I'll explain now.
Initially, I planned to talk about 3 possible options for writing comments.
    Such as:
  • Anonymous
  • Via social networks
  • Only for registered users
But alas, as soon as I realized how much time it would take me, I decided to cool my ardor a little. And write an article specifically about my journey of writing a commenting system. That is, a combined version of anonymous/registered commenting.
I also wanted to insert comment forms for each type. That is, start with the simplest, then move on to commenting through social networks, and end it all with the fact that only registered users will be able to comment. Which, by the way, I don’t have yet.
Maybe someday I will write this article in the form I wanted, but for now we have what we have.

heart
5
3 connected dots
0

Used termins

Related questions