Building a Social Media Site With Python and Django: Part 11 Adding Comment Replies

Video Tutorial

Code on Github

There will be a couple more tutorials for this social network, in this one we will add the ability to like/dislike comments and reply to comments. We won’t keep adding replies to replies, etc but we will allow the comments directly on the post to have replies. Let’s get started by adding likes and dislikes to the comment model.

Adding Likes and Dislikes to Comments

We need to add the same thing that we had in our Post model for the likes and dislikes, this will look almost exactly the same. We just need to change the related_name to be something unique, in this case I’ll just call it comment_likes and comment_dislikes. We also need to add a view and url, these will also look very similar to the post view and url except we are doing it for the comment model instead.

social/models.py

class Comment(models.Model):
comment = models.TextField()
created_on = models.DateTimeField(default=timezone.now)
post = models.ForeignKey('Post', on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
likes = models.ManyToManyField(User, blank=True, related_name='comment_likes')
dislikes = models.ManyToManyField(User, blank=True, related_name='comment_dislikes')

social/views.py

class AddCommentLike(LoginRequiredMixin, View):
def post(self, request, post_pk, pk, *args, **kwargs):
comment = Comment.objects.get(pk=pk)
is_dislike = False for dislike in comment.dislikes.all():
if dislike == request.user:
is_dislike = True
break
if is_dislike:
comment.dislikes.remove(request.user)
is_like = False for like in comment.likes.all():
if like == request.user:
is_like = True
break
if not is_like:
comment.likes.add(request.user)

if is_like:
comment.likes.remove(request.user)
next = request.POST.get('next', '/')
return HttpResponseRedirect(next)
class AddCommentDislike(LoginRequiredMixin, View):
def post(self, request, post_pk, pk, *args, **kwargs):
comment = Comment.objects.get(pk=pk)
is_like = False for like in comment.likes.all():
if like == request.user:
is_like = True
break
if is_like:
comment.likes.remove(request.user)
is_dislike = False for dislike in comment.dislikes.all():
if dislike == request.user:
is_dislike = True
break
if not is_dislike:
comment.dislikes.add(request.user)

if is_dislike:
comment.dislikes.remove(request.user)
next = request.POST.get('next', '/')
return HttpResponseRedirect(next)

social/urls.py

path('post/<int:post_pk>/comment/delete/<int:pk>/', CommentDeleteView.as_view(), name='comment-delete'),
path('post/<int:post_pk>/comment/<int:pk>/like', AddCommentLike.as_view(), name='comment-like'),

social/templates/social/post_detail.html

{% for comment in comments %}
{% if comment.is_parent %}
<div class="row justify-content-center mt-3 mb-5">
<div class="col-md-5 col-sm-12 border-bottom">
<div>
<a href="{% url 'profile' comment.author.profile.pk %}"><img class="rounded-circle post-img" height="30" width="30" src="{{ comment.author.profile.picture.url }}" /></a>
<p class="post-text"><a class="post-link text-primary" href="{% url 'profile' comment.author.profile.pk %}">@{{ comment.author }}</a> {{ comment.created_on }}</p>
<div class="mb-3">
{% if request.user == comment.author %}
<a href="{% url 'comment-delete' post.pk comment.pk %}" class="edit-color"><i class="fas fa-trash"></i></a>
{% endif %}
</div>
</div>
<p>{{ comment.comment }}</p>
<div class="d-flex flex-row">
<form method="POST" action="{% url 'comment-like' post.pk comment.pk%}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<button class="remove-default-btn" type="submit"><i class="far fa-thumbs-up"></i> <span>{{ comment.likes.all.count }}</span></button>
</form>
<form method="POST" action="{% url 'comment-dislike' post.pk comment.pk %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<button class="remove-default-btn" type="submit"><i class="far fa-thumbs-down"></i> <span>{{ comment.dislikes.all.count }}</span></button>
</form>
</div>
<div class="row justify-content-center mt-3 mb-5 d-none" id="{{ comment.pk }}">
<div class="col">
<form method="POST" action="{% url 'comment-reply' post.pk comment.pk %}">
{% csrf_token %}
{{ form | crispy }}
<div class="d-grid gap-2">
<button class="btn btn-success mt-3">Submit!</button>
</div>
</form>
</div>
</div>
</div>
</div>

Adding Replies to Comments

Now that we have likes and dislikes added, let’s add the ability to reply to these comments as well. For this, we will use a little Javascript to hide or show the comment box below each comment. To make this work, we need to add another foreign key to our Comment model to hold the parent comment id. We will also add a couple property methods to our comment model. Other than that, this shouldn’t be too much different than what we have done before.

First we need to add the additional foreign key to hold the parent comment so we can make sure this comment shows up in the correct spot in our template. After that we will add two property methods. Adding the property decorator will allow us to use these methods in our template using the regular template syntax. We will use these two methods to return all of the children and to return whether or not the comment is a parent comment, meaning it has no parent set.

social/models.py

class Comment(models.Model):
comment = models.TextField()
created_on = models.DateTimeField(default=timezone.now)
post = models.ForeignKey('Post', on_delete=models.CASCADE)
author = models.ForeignKey(User, on_delete=models.CASCADE)
likes = models.ManyToManyField(User, blank=True, related_name='comment_likes')
dislikes = models.ManyToManyField(User, blank=True, related_name='comment_dislikes')
parent = models.ForeignKey('self', on_delete=models.CASCADE, blank=True, null=True, related_name='+')
@property
def children(self):
return Comment.objects.filter(parent=self).order_by('-created_on').all()
@property
def is_parent(self):
if self.parent is None:
return True
return False

Now let’s add the reply form and the icon we will use to either hide this form or make it visible. We also need to add an onclick and set that to a function that we will create in a second. We also need to pass in the comment.pk from the template into this function. We will need this to determine which comment form to show/hide. We will also add an id to the form with the comment id. This is what we will match with our Javascript function to hide/show the correct form.

social/templates/social/post_detail.html

<div>
<button class="remove-default-btn"><i class="far fa-comment-dots" onclick="commentReplyToggle('{{ comment.pk }}')"></i></button>
</div>
<div class="row justify-content-center mt-3 mb-5 d-none" id="{{ comment.pk }}">
<div class="col">
<form method="POST" action="{% url 'comment-reply' post.pk comment.pk %}">
{% csrf_token %}
{{ form | crispy }}
<div class="d-grid gap-2">
<button class="btn btn-success mt-3">Submit!</button>
</div>
</form>
</div>
</div>

Now let’s add the javascript function to actually hide or show the comment reply form. We will create a new folder in the static directory and add a file there called social.js. All we need to do here is get the parent comment by finding the comment form that matches the id that we passed in. Then we will check if the d-none class is either included or not currently included in the form’s class list. Then we can either add or remove it to reverse the form’s current state. d-none is the same as a display: none css style so it will hide whatever element it is applied to.

static/js/social.js

function commentReplyToggle(parent_id) {
const row = document.getElementById(parent_id);
if (row.classList.contains('d-none')) {
row.classList.remove('d-none');
} else {
row.classList.add('d-none');
}
}

Next will be to add the view to add a comment and set the parent id based on what form it was submitted through. This should be pretty similar to other views, we will redirect back to the post detail that we are currently on when we finish adding the post.

social/views.py

class CommentReplyView(LoginRequiredMixin, View):
def post(self, request, post_pk, pk, *args, **kwargs):
post = Post.objects.get(pk=post_pk)
parent_comment = Comment.objects.get(pk=pk)
form = CommentForm(request.POST)
if form.is_valid():
new_comment = form.save(commit=False)
new_comment.author = request.user
new_comment.post = post
new_comment.parent = parent_comment
new_comment.save()

comments = Comment.objects.filter(post=post).order_by('-created_on')
context = {
'post': post,
'form': form,
'comments': comments,
}
return redirect('post-detail', pk=post_pk)

Now let’s add a url pattern for this view. In our template we already called it ‘comment-reply’ and we passed in the post pk and the comment pk so we need to make sure our url pattern matches that.

social/urls.py

path('post/<int:post_pk>/comment/reply/<int:pk>', CommentReplyView.as_view(), name='comment-reply'),

Finally the last step will be to update our post detail to list out every child comment below the parent comment. To do this we need to make a couple changes, we will first at the beginning of the for loop for the parent comments check to see if the current comment is a parent. We will use our property method to do this. If it is not we will not add it to the list. So this way we only have the parent comments being listed out first. Now we will add an extra for loop inside of the parent comment to list out all of the children. We can get the children using the other property method that we added earlier. We can add another for loop to list out all of those as well. Here is the template with all of this added.

social/templates/social/post_detail.html

{% for comment in comments %}
{% if comment.is_parent %}
<div class="row justify-content-center mt-3 mb-5">
<div class="col-md-5 col-sm-12 border-bottom">
<div>
<a href="{% url 'profile' comment.author.profile.pk %}"><img class="rounded-circle post-img" height="30" width="30" src="{{ comment.author.profile.picture.url }}" /></a>
<p class="post-text"><a class="post-link text-primary" href="{% url 'profile' comment.author.profile.pk %}">@{{ comment.author }}</a> {{ comment.created_on }}</p>
<div class="mb-3">
{% if request.user == comment.author %}
<a href="{% url 'comment-delete' post.pk comment.pk %}" class="edit-color"><i class="fas fa-trash"></i></a>
{% endif %}
</div>
</div>
<p>{{ comment.comment }}</p>
<div class="d-flex flex-row">
<form method="POST" action="{% url 'comment-like' post.pk comment.pk%}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<button class="remove-default-btn" type="submit"><i class="far fa-thumbs-up"></i> <span>{{ comment.likes.all.count }}</span></button>
</form>
<form method="POST" action="{% url 'comment-dislike' post.pk comment.pk %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<button class="remove-default-btn" type="submit"><i class="far fa-thumbs-down"></i> <span>{{ comment.dislikes.all.count }}</span></button>
</form>
<div>
<button class="remove-default-btn"><i class="far fa-comment-dots" onclick="commentReplyToggle('{{ comment.pk }}')"></i></button>
</div>
</div>
<div class="row justify-content-center mt-3 mb-5 d-none" id="{{ comment.pk }}">
<div class="col">
<form method="POST" action="{% url 'comment-reply' post.pk comment.pk %}">
{% csrf_token %}
{{ form | crispy }}
<div class="d-grid gap-2">
<button class="btn btn-success mt-3">Submit!</button>
</div>
</form>
</div>
</div>
</div>
</div>

{% for child_comment in comment.children %}
<div class="row justify-content-center mt-3 mb-5 child-comment">
<div class="col-md-5 col-sm-12 border-bottom">
<div>
<a href="{% url 'profile' child_comment.author.profile.pk %}"><img class="rounded-circle post-img" height="30" width="30" src="{{ child_comment.author.profile.picture.url }}" /></a>
<p class="post-text"><a class="post-link text-primary" href="{% url 'profile' child_comment.author.profile.pk %}">@{{ child_comment.author }}</a> {{ child_comment.created_on }}</p>
<div class="mb-3">
{% if request.user == child_comment.author %}
<a href="{% url 'comment-delete' post.pk child_comment.pk %}" class="edit-color"><i class="fas fa-trash"></i></a>
{% endif %}
</div>
</div>
<p>{{ child_comment.comment }}</p>
<div class="d-flex flex-row">
<form method="POST" action="{% url 'comment-like' post.pk child_comment.pk %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<button class="remove-default-btn" type="submit"><i class="far fa-thumbs-up"></i> <span>{{ child_comment.likes.all.count }}</span></button>
</form>
<form method="POST" action="{% url 'comment-dislike' post.pk child_comment.pk %}">
{% csrf_token %}
<input type="hidden" name="next" value="{{ request.path }}">
<button class="remove-default-btn" type="submit"><i class="far fa-thumbs-down"></i> <span>{{ child_comment.dislikes.all.count }}</span></button>
</form>
</div>
</div>
</div>
{% endfor %}
{% endif %}
{% endfor %}

That will conclude this addition to the social media app. Like I said earlier, we will add a couple more additions from what was suggested to me in soon, this is a good stopping point for now. Thank you for following along, we will be back later to add more to this.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store