loading...
Cover image for How to create a Comment Section for your Django Blog!

How to create a Comment Section for your Django Blog!

radualexandrub profile image Radu-Alexandru B Originally published at codingtranquillity.herokuapp.com ใƒป5 min read

Hi!

This mini-tutorial will focus on creating a Comment Section for adding (without update/delete) comments to each blog post. We will implement this feature while using a class-based view, namely our BlogPost DetailView, mine's looks like this:

# MainApp/models.py
class BlogPost(models.Model):
    title = models.CharField(max_length=100)
    subtitle = models.CharField(max_length=200, blank=True, null=True)
    content = models.TextField()
    date_posted = models.DateTimeField(default=timezone.now)
    author = models.ForeignKey(User, on_delete=models.SET_NULL, null=True)
    image = models.ImageField(upload_to='blog_images',
                              storage=gd_storage, null=True, blank=True)

    def __str__(self):
        return self.author.username + ', ' + self.title[:40]

    def get_absolute_url(self):
        return reverse('blogpost-detail', kwargs={'pk': self.pk})

# MainApp/views.py
class BlogPostDetailView(DetailView):
    model = BlogPost
    # template_name = MainApp/BlogPost_detail.html
    # context_object_name = 'object'
Enter fullscreen mode Exit fullscreen mode

First things first: we need to create our BlogComment model in our models.py. We can personalize our model in any way we want, I'll stick to the basics of a comment and will add the following fields: author, content/body, and a date (when the comment was posted):

# models.py
from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User

class BlogComment(models.Model):
    blogpost_connected = models.ForeignKey(
        BlogPost, related_name='comments', on_delete=models.CASCADE)
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    content = TextField()
    date_posted = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return str(self.author) + ', ' + self.blogpost_connected.title[:40]
Enter fullscreen mode Exit fullscreen mode
  • Every BlogComment will have an id (foreign key) of its BlogPost (a BlogPost can have multiple BlogComments), and if a BlogPost is deleted, then all the BlogComments that were linked to that BlogPost will be deleted (on_delete == CASCADE).
  • The author will also be a foreign key to the whole User object (that has its User id, username, email, etc), therefore a User can have multiple comments.
  • We will also add the magic method str() to view the comments in a more readable way (instead of viewing the object type) when we are making queries from our CLI/Admin panel).

Database Diagram for our comment

In models.py, in our BlogPost model, we can also write a function that will return the number of comments of a blog post:

# models.py
class BlogPost(models.Model):
    title = models.CharField(max_length=100)
    ...

    @property
    def number_of_comments(self):
        return BlogComment.objects.filter(blogpost_connected=self).count()
Enter fullscreen mode Exit fullscreen mode

After every change in the models.py file, we need to open our terminal and make the migrations to our database:

# CLI/Terminal
>> cd C:\Projects\...\YourDjangoAppMainFolder
>> python manage.py makemigrations
>> python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

Let's create a new form class: In the same folder as our models.py, create a new file named forms.py, where we'll write the following:

# forms.py
from django import forms
from .models import BlogComment

class NewCommentForm(forms.ModelForm):
    class Meta:
        model = BlogComment
        fields = ['content']
Enter fullscreen mode Exit fullscreen mode

Trรจs bien, now we can add to our get_context_data function within our class-based view BlogPost DetailView, in views.py, the following:

# views.py
from .models import BlogPost, BlogComment
from .forms import NewCommentForm

class BlogPostDetailView(DetailView):
    model = BlogPost
    # template_name = MainApp/BlogPost_detail.html
    # context_object_name = 'object'

    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)

        comments_connected = BlogComment.objects.filter(
            blogpost_connected=self.get_object()).order_by('-date_posted')
        data['comments'] = comments_connected
        if self.request.user.is_authenticated:
            data['comment_form'] = NewCommentForm(instance=self.request.user)

        return data
Enter fullscreen mode Exit fullscreen mode

Here we will retrieve all the comments from our current BlogPost object, store them (the query) in a local variable comments_connected, then send it further as a context to our HTML-based blogpost_detail.

However, in order to post comments directly from our class-based BlogPost DetailView, we also need to define a post method to receive the context from our form (situated in this view/html). Therefore, in the same class, we need to add:

# views.py
class BlogPostDetailView(DetailView):
    ... 

    def get_context_data(self, **kwargs):
        ...

    def post(self, request, *args, **kwargs):
        new_comment = BlogComment(content=request.POST.get('content'),
                                  author=self.request.user,
                                  blogpost_connected=self.get_object())
        new_comment.save()
        return self.get(self, request, *args, **kwargs)
Enter fullscreen mode Exit fullscreen mode

Finally, in our blogpost_detail.html let's write in the DjangoTemplateLanguage the following:

<!-- COMMENTS  -->
<h2>Leave your comment!</h2>
<div id="comments_section">

  {% if user.is_authenticated %}
  <form method="POST">
    {% csrf_token %}
    <div class="form-group">
      {{ comment_form }}
      <button class="btn btn-info" type="submit">Add comment <i class="fas fa-comments"></i></button>
    </div>
  </form>
  {% else %}
  <a class="btn btn-outline-info" href="{% url 'login' %}?next={{request.path}}">Log in to add a comment!</a><br>
  {% endif %}

  {% if comments %}
  <strong class="text-secondary">{{ object.number_of_comments }} Comment{{ object.number_of_comments|pluralize }}</strong>
  <hr>
  <ul>
    {% for comment in comments %}
    <li>           
     <div>
        <span>
          <strong class="text-info">{{ comment.author }} </strong>
          <small class="text-muted">{{ comment.date_posted }}</small>
        </span>
        <p>
          {{ comment.content|safe }}
        </p>
      </div>
    </li>
    {% endfor %}
  </ul>
  {% else %}
    <strong class="text-secondary">No comments yet...</strong>
  {% endif %}

</div>
Enter fullscreen mode Exit fullscreen mode

Sooo, there's a lot of code there, let's go through some parts of it step by step:

  • The first thing that we do is to check if the user is authenticated: if True, then show the user a form where he can write the content of his new comment. if False, then show the user a button that redirects him to the Login page. Also, it's important that after a user logs into his account to redirect him to the earlier blog post that he wanted to post a comment, so we'll add to our redirect link "?next={{request.path}}" where request.path is the current page path (e.g. localhost/blogpost/7)
  • Then we check if our current blogpost has any comments, if not, we'll put in our HTML "No comments yet..", but if we have any comments, we will then write the number of comments and then we will loop through each of them and show its author, date, and content.

Perfect, so we are almost done! However, this comment section will look rather dull... Therefore we need to create some styling, that we can find on Google or... here (thanks to bootdey.com)! To integrate it, we just need to add the CSS code in our blogpost_detail.html and the corresponding tags in our <div> and <ul> (list) sections, following their example.

Nice, now we are done! Hope you will find this useful. ๐Ÿ˜
You can see a live example of this comment section on my blog: codingtranquillity.herokuapp.com... where you can also find more articles like this!

Have a nice day and... Happy coding!
R.B.

Discussion

pic
Editor guide
Collapse
raybesiga profile image
Ray Besiga

Hi @Radu,

Great article but I just want to point out a few issues. The first is in your BlogPostDetailView

    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)

        comments_connected = BlogComment.objects.filter(
            blogpost_connected=self.get_object()).order_by('-date_posted')
Enter fullscreen mode Exit fullscreen mode

This is bound to throw a Type Error. In this case, it will be an object not callable error.

The other issue I see arising is with the post:

 def post(self, request, *args, **kwargs):
        new_comment = BlogComment(content=request.POST.get('content'),
                                  author=self.request.user,
                                  blogpost_connected=self.get_object())
Enter fullscreen mode Exit fullscreen mode

The blogpost_connected should be an instance of the BlogPost and not the BlogComment. I hope this is helpful. Best regards.

Collapse
jiraiyasennin profile image
Dostow**->

What if i don't need the user to be logged in? I'm working in a comment system for my personal site without a registration system :-)
Thanks.

Collapse
radualexandrub profile image
Radu-Alexandru B Author

Hmm... I didn't think about this option. But the process should be even simpler than the instructions I've written here.

Firstly, in our models.py we don't need an author based on User anymore, we will just use TextField():

# models.py
from django.db import models
from django.utils import timezone

class BlogComment(models.Model):
    blogpost_connected = models.ForeignKey(
        BlogPost, related_name='comments', on_delete=models.CASCADE)
    author = TextField()
    content = TextField()
    date_posted = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return self.blogpost_connected.title[:40]
Enter fullscreen mode Exit fullscreen mode

[Optional] We can still return the number of comments if we want in our main BlogPost class model in models.py:

# models.py
class BlogPost(models.Model):
    title = models.CharField(max_length=100)
    ...

    @property
    def number_of_comments(self):
        return BlogComment.objects.filter(blogpost_connected=self).count()
Enter fullscreen mode Exit fullscreen mode

Make the changes to our database:

# CLI/Terminal
>> cd C:\Projects\...\YourDjangoAppMainFolder
>> python manage.py makemigrations
>> python manage.py migrate
Enter fullscreen mode Exit fullscreen mode

Create the same forms.py with an author:

# forms.py
from django import forms
from .models import BlogComment

class NewCommentForm(forms.ModelForm):
    class Meta:
        model = BlogComment
        fields = ['author', 'content']
Enter fullscreen mode Exit fullscreen mode

And now in views.py we don't need to check if self.request.user.is_authenticated anymore, we will just send the form as context to our HTML-based blogpost_detail.html.

# views.py
from .models import BlogPost, BlogComment
from .forms import NewCommentForm

class BlogPostDetailView(DetailView):
    model = BlogPost
    # template_name = MainApp/BlogPost_detail.html
    # context_object_name = 'object'

    def get_context_data(self, **kwargs):
        data = super().get_context_data(**kwargs)

        comments_connected = BlogComment.objects.filter(
            blogpost_connected=self.get_object()).order_by('date_posted')
        data['comments'] = comments_connected
        data['comment_form'] = NewCommentForm( )

        return data

    def post(self, request, *args, **kwargs):
        new_comment = BlogComment(content=request.POST.get('content'),
                                  author=request.POST.get('author'),
                                  blogpost_connected=self.get_object())
        new_comment.save()
        return self.get(self, request, *args, **kwargs)
Enter fullscreen mode Exit fullscreen mode

And finally in our blogpost_detail.html we'll show directly the forms without checking if user.is_authenticated.

<!-- COMMENTS  -->
<h2>Leave your comment!</h2>
<div id="comments_section">

  <form method="POST">
    {% csrf_token %}
    <div class="form-group">
      {{ comment_form }}
      <button class="btn btn-info" type="submit">Add comment <i class="fas fa-comments"></i></button>
    </div>
  </form>

  {% if comments %}
  <strong class="text-secondary">{{ object.number_of_comments }} Comment{{ object.number_of_comments|pluralize }}</strong>
  <hr>
  <ul>
    {% for comment in comments %}
    <li>           
     <div>
        <span>
          <strong class="text-info">{{ comment.author }} </strong>
          <small class="text-muted">{{ comment.date_posted }}</small>
        </span>
        <p>
          {{ comment.content|safe }}
        </p>
      </div>
    </li>
    {% endfor %}
  </ul>
  {% else %}
    <strong class="text-secondary">No comments yet...</strong>
  {% endif %}

</div>
Enter fullscreen mode Exit fullscreen mode

Hope all of these will work. Good Luck!

Collapse
jiraiyasennin profile image
Dostow**->

Amazing!! Thank you a lot!! It's more or less how i thought it should be, now i can continue with the project :-D

Collapse
ninanolets profile image
Nina Noleto

Hey, thanks for this mini tutorial! I appreciate that it's detailed but simple. Made an account here just to say thanks, it kinda saved me today :)

Collapse
radualexandrub profile image
Radu-Alexandru B Author

Thank you!

I like to break the whole code into smaller steps, without puzzling the viewer with extra complexity or missing code parts. However, it's hard to find the extra time to write those kinds of posts, but appreciations like these motivate me.

Welcome to DEV Community!

Collapse
imbiru143 profile image
Birendra Bohara

what should i do if i have to use same comment model in different place