Adding Django Threaded Comments in Blog - Django Blog #6

Hello Internet Programmer, today we implement threaded comment in blog without any external module. So, activate your virtual environment and let’s code.

Creating Comment model

Open models.py of blog application and add following Comment model after Post model and also make the following change in Post model

# post model
class Post(models.Model)
    ...
    ...
    
    # added after get_absolute_url function
    # to get comment with parent is none and active is true, we can use this in template
    def get_comments(self):
        return self.comments.filter(parent=None).filter(active=True)

# comment model    
class Comment(models.Model):
    post=models.ForeignKey(Post,on_delete=models.CASCADE, related_name="comments")
    name=models.CharField(max_length=50)
    email=models.EmailField()
    parent=models.ForeignKey("self", null=True, blank=True, on_delete=models.CASCADE)
    body = models.TextField()
    
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    active = models.BooleanField(default=True)

    class Meta:
        ordering = ('created',)
    
    def __str__(self):
        return self.body

    def get_comments(self):
        return Comment.objects.filter(parent=self).filter(active=True)

This is your Comment model. It contains a ForeignKey to associate a comment with a single post. This many-to-one relationship is defined in the Comment model because each comment will be made on one post, and each post may have multiple comments.

The related_name attribute allows you to name the attribute that you use for the relationship from the related object back to this one. After defining this, you can retrieve the post of a comment object using comment.post and retrieve all comments of a post using post.comments.all().

If you don’t define the related_name attribute, Django will use the name of the model in lowercase, followed by _set (that is, comment_set ) to name the relationship of the related object to the object of the model, where this relationship has been defined.

Here, parent the field allows defining which is parent comment and which is child comment. In short, allow us to make a threaded comment system.

We have included an active Boolean field that you will use to manually deactivate inappropriate comments. We use the created field to sort comments in chronological order by default.

We added get_comments() function in Post model to get a parent comment in HTML template with a parent is none and active status is true. This will very useful to make threaded comments easily.

We also get_comments() function in Comment model to get child comment in HTML template.

The new Comment model that you just created is not yet synchronized into the database. Run the following command to generate a new migration that reflects the creation of the new model,

python3 manage.py makemigrations

Now, you need to create the related database schema and apply the changes to the database. Run the following command to apply existing migrations,

python3 manage.py migrate

Next, you can add your new model to the administration site in order to manage comments through a simple interface.

Open the admin.py file of the blog application, import the Comment model, and add the following ModelAdmin class,

@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
    list_display=('name', 'email', 'post', 'created', 'active')
    list_filter = ('active', 'created', 'updated')
    search_fields = ('name', 'email', 'body')

Now admin.py looks like this,

admin.py, comment model registering, comment system in django

admin.py

Start the development server with the python manage.py runserver command and open http://127.0.0.1:8000/admin/ in your browser.

You should see the new Comments model included in the BLOG section, as shown in the following screenshot,

Comments model in django admin

Comments model in django admin

The model is now registered in the administration site, and you can manage Comment instances using a simple interface.

Creating forms from models

We need a comment form to let users comment on blog posts.

For form, Django has a built-in forms framework that allows you to create forms in an easy manner. The forms framework makes it simple to define the fields of your form, specify how they have to be displayed and indicate how they have to validate input data. The Django forms framework offers a flexible way to render forms and handle data.

Django comes with two base classes to build forms:

  • Form: Allows you to build standard forms
  • ModelForm: Allows you to build forms tied to model instances

We use ModelForm because you have to build a form dynamically from your Comment model.

First, create a forms.py file inside the directory of your blog application,

making form

forms.py

And add following code,

from django import forms
from .models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('name', 'email', 'body')
    
    # overriding default form setting and adding bootstrap class
    def __init__(self, *args, **kwargs):
        super(CommentForm, self).__init__(*args, **kwargs)
        self.fields['name'].widget.attrs = {'placeholder': 'Enter name','class':'form-control'}
        self.fields['email'].widget.attrs = {'placeholder': 'Enter email', 'class':'form-control'}
        self.fields['body'].widget.attrs = {'placeholder': 'Comment here...', 'class':'form-control', 'rows':'5'}

     

To create a form from a model, you just need to indicate which model to use to build the form in the Meta class of the form. Django introspects the model and builds the form dynamically for you.

Handling ModelForms in views

We will use the post detail view to instantiate the form and process it, in order to keep it simple.

Edit the views.py file, add imports for the Comment model and the CommentForm form, and modify the post_detail view to make it look like the following,

from django.shortcuts import render, get_object_or_404, redirect
from .models import Post, Comment
from .forms import CommentForm

def post_detail(request, post):
    post=get_object_or_404(Post,slug=post,status='published')

    # List of active comments for this post
    comments = post.comments.filter(active=True)
    new_comment = None

    if request.method == 'POST':
        # A comment was posted
        comment_form = CommentForm(data=request.POST)
        if comment_form.is_valid():
            # Create Comment object but don't save to database yet
            new_comment = comment_form.save(commit=False)
            # Assign the current post to the comment
            new_comment.post = post
            # Save the comment to the database
            new_comment.save()
            # redirect to same page and focus on that comment
            return redirect(post.get_absolute_url()+'#'+str(new_comment.id))
        else:
            comment_form = CommentForm()

    return render(request, 'post_detail.html',{'post':post,'comments': comments,'comment_form':comment_form})

We used the post_detail view to display the post and its comments. We added a QuerySet to retrieve all active comments for this post using comments = post.comments.filter(active=True)

We build this QuerySet, starting from the post object. Instead of building a QuerySet for the Comment model directly, we leverage the post object to retrieve the related Comment objects.

We use the manager for the related objects that you defined as comments using the related_name attribute of the relationship in the Comment model. We use the same view to let your users add a new comment. We initialize the new_comment variable by setting it to None. We will use this variable when a new comment is created.

We build a form instance with comment_form = CommentForm() if the view is called by a GET request.

If the request is done via POST, you instantiate the form using the submitted data and validate it using the is_valid() method.

If the form is invalid, you render the template with the validation errors. If the form is valid, we take the following actions,

You create a new Comment object by calling the form’s save() method and assign it to the new_comment variable, as follows:
new_comment = comment_form.save(commit=False)

The save() method creates an instance of the model that the form is linked to and saves it to the database. If you call it using commit=False, you create the model instance but don’t save it to the database yet. This comes in handy when you want to modify the object before finally saving it, which is what you will do next.

You assign the current post to the comment you just created: new_comment.post = post

Finally, you save the new comment to the database by calling its save() method: new_comment.save()

Finally we redirect to the same page and focus current comment.

Now we make view for reply of comment, add following view after post_detail view,

# handling reply, reply view
def reply_page(request):
    if request.method == "POST":

        form = CommentForm(request.POST)

        if form.is_valid():
            post_id = request.POST.get('post_id')  # from hidden input
            parent_id = request.POST.get('parent')  # from hidden input
            post_url = request.POST.get('post_url')  # from hidden input

            reply = form.save(commit=False)
    
            reply.post = Post(id=post_id)
            reply.parent = Comment(id=parent_id)
            reply.save()

            return redirect(post_url+'#'+str(reply.id))

    return redirect("/")

We render same for when user click on reply button.

We gate post id, parent id, and post url from HTML template.

Post id is used to get current post id where actually comment made.

Then this is child comment right so we need parent comment id. We get this using parent id.

Finally we save the form in database.

Now set URL for this reply view, open urls.py of blog application and add following url pattern,

from django.urls import path
from . import views

app_name = 'blog'

urlpatterns=[
    path('',views.post_list,name="post_list"),
    path('<slug:post>/',views.post_detail,name="post_detail"),
    path('comment/reply/', views.reply_page, name="reply"), #this
]

Now view and urls is ready to display and process new comments.

Adding comments to the post detail template

Create comment.html in template folder and add following code,

<div class="border-0 border-start border-2 ps-2" id="{{comment.id}}">

        <div class="mt-3">
            <strong>{{comment.name}}</strong> 
            {% if  comment.parent.name%} to <strong>{{comment.parent.name}}</strong>{% endif %}
            <small class="text-muted">On {{ comment.created.date }}</small>
        </div>
        <div class="border p-2 rounded">
            <p>{{comment.body}}</p>
            <button class="btn btn-primary btn-sm" onclick="handleReply({{comment.id}})">Reply</button>
        
            <div id="reply-form-container-{{comment.id}}" style="display:none">
            
                <form method="post" action="{% url 'blog:reply' %}" class="mt-3">
                    {% csrf_token %}
                    <input type="hidden" name="post_id" value="{{post.id}}">
                    <input type="hidden" name="parent" value="{{comment.id}}">
                    <input type="hidden" name="post_url" value="{{post.get_absolute_url}}">


                    {{comment_form.as_p}}

                    <div>
                        <button type="button" onclick="handleCancel({{comment.id}})" class="btn btn-light border btn-sm">Cancel</button>
                        <button type="submit" class="btn btn-primary btn-sm">Submit</button>
                    </div>
                </form>
            </div>
        </div>
        {% for comment in comment.get_comments %}
            {% include 'comment.html' with comment=comment %}
        {% endfor %}
</div>

Above code run recursively because we added {% include 'comment.html' with comment=comment %}

With this, we get child comments by comment.get_comments using for loop and we can see the threaded comment.

Now open post_detail.html, and add following code after article tag.

<hr/>
        <h3>Add Comment</h3>
        <form method="post" action="">
            {% csrf_token %}
            {{ comment_form.as_p }}
            <button type="submit" class="btn btn-primary">Comment</button>
        </form>

    
        {% with comments.count as total_comments %}
            <h3 class="mt-5">
                {{ total_comments }} comment{{ total_comments|pluralize }}
            </h3>
        {% endwith %}

        {% if not post.comments.all %}
            No comments yet
        
        {% else %}
            {% for comment in post.get_comments %}
                {% include 'comment.html' with comment=comment %}
            {% endfor %}
        {% endif %}
   

We rendered the form and we use csrf token because we are using post method here.

We are using the Django ORM in the template, executing the QuerySet comments. count().

Note that the Django template language doesn’t use parentheses for calling methods.

The {% with %} tag allows us to assign a value to a new variable that will be available to be used until the {% endwith %} tag.

We use the pluralize template filter to display a plural suffix for the word “comment,” depending on the total_comments value. Template filters take the value of the variable they are applied to as their input and return a computed value.

The pluralize template filter returns a string with the letter “s” if the value is different from 1. The preceding text will be rendered as 0 comments, 1 comment, or N comments. Django includes plenty of template tags and filters that can help you to display information in the way that you want.

With post.get_comments we get parent comment and using {% include 'comment.html' with comment=comment %} we get child comment after it.

Finally, now we add little bit JavaScript to toggle buttons, for that create js/main.js in static folder and add following code,

function handleReply(response_id) {
    const reply_form_container = document.querySelector(`#reply-form-container-${response_id}`)
    if (reply_form_container) {
        reply_form_container.style.display = 'block';
    }
}

function handleCancel(response_id) {
    const reply_form_container = document.querySelector(`#reply-form-container-${response_id}`)
    if (reply_form_container) {
        reply_form_container.style.display = 'none';
    }
}

now add this in base.html inside head tag like you added css,

<!-- javascript add -->
<script src="{% static 'js/main.js'%}"></script>

Now open any post and see the comment section,

This is how comment form looks like,

comment form

comment form

Following is the screenshot of thread comment,

threaded comment system in django

threaded comment system

You can reply to any comment,

reply to comment

reply to comment

I hope you enjoyed this tutorial.

That’s it for this tutorial. Please share this with your friend.

GitHub Link: https://github.com/SoniArpit/awwblog

Previous: Adding Featured Image in Blog Posts – Django Blog #5

Next: Adding the Tagging Functionality in the Blog – Django Blog #7