How I Created a Blog Application Using Django – Part 3

5/5 - (2 votes)

In the previous two tutorials, you have set up our blog application, and it is already running on the local server.

In this tutorial, we want to take things further by adding a comment system that allows readers to add comments after reading an article.

  1. The first part explains our blog application, a model to store some posts, and an admin interface to perform CRUD operations.
  2. The second part saw us add views, URLs to map the views, and templates to display the blog posts.

You should check out these projects first to get everything from this tutorial. After this third part, you can check out this part:

🀠 Recommended: How I Created a Blog Application Using Django – Part 4

Creating the Comment Model

To set up a comment system, we need a separate database table to save and retrieve comments. Open the models.py file and add the following to create the Comment model.

class Comment(models.Model):
    name = models.CharField(max_length=80)
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    email = models.EmailField()
    body = models.TextField()
    created_on = models.DateTimeField(auto_now_add=True)
    active = models.BooleanField(default=False)

    class Meta:
        ordering = ['-created_on']

    def __str__(self):
        return f'Comment {self.content} by {self.name}'

This is similar to the previous models we created. We have a name field for users to add their names. The post field has a ForeignKey relation. This establishes a many-to-one relationship so that many comments can be made on one post.

The post field has three arguments including the post model in the relationship. The on_delete argument is set to models.CASCADE so that once a post is deleted, the comments will also be deleted. The last one, the related_name argument, is used to retrieve the post of a given comment.

We define an active Boolean field set to False. The author of the post will want to decide when to turn it to True once a comment is approved to prevent spam.

Let’s now run migrations to make these changes in the database.

python3 manage.py makemigrations
python3 manage.py migrate

Adding comments to the admin

The code below should go to the admin.py file. Be sure to import the Comment model.

class CommentAdmin(admin.ModelAdmin):
    list_display = ('name', 'body', 'post', 'created_on', 'active')
    list_filter = ('active', 'created_on')
    search_fields = ('name', 'email', 'body')
    actions = ['approve_comments']

    def approve_comments(self, request, queryset):
        queryset.update(active=True)

admin.site.register(Comment, CommentAdmin)

The first three lines inside the CommentAdmin class should be familiar as it’s similar to what we did earlier to customize the admin interface in the first part series of this project tutorial. However, since we want to prevent spam by setting the action field in the Comment class to False, we also want to define when to set it to True.

We did this in the approve_comments() method which updates the active field to True. As you can see, it is already added to the actions method. With this, we can easily approve many comments at once. If this is not clearly understood, you will understand it soon after we open the admin interface.

Create a superuser or log in to open the admin interface.

Did you see the β€˜Approve comments’ in the Action section? Once you select all comments and click Go, it will approve all the comments. This feature is exactly what we define in the admin.py file.

Hoping that it is understood, we proceed to the next step.

Creating a Form

In the previous section, we added comments to the admin. The main reason for doing so is to manually approve all comments. However, the author doesn’t need to edit or create comments.

In that case, we have to create a form for readers to create and edit comments. Without doing this, we cannot get feedback from visitors as they do not have access to the admin.

By convention, all forms are created in a forms.py file inside the app folder. Ours will not be an exception. So, hoping you have already done so, write the following:

from .models import Comment
from django import forms



class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ('name', 'email', 'body')

Creating a form is made easy with Django as everything is done in just a few lines of code. Django’s inbuilt form has already done the heavy lifting with its rich and secure API. All we have to do is to create a class that inherits the class. Then, we define the fields we want to be displayed. As simple as that.

Notice that we use the ModelForm class because we want the form input to be saved in the database model.

Creating Views

In the second part of this project tutorial, we created class-based views. We will make a big adjustment to the blog detail view to include form processing using function-based views.

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

def blog_detail(request, slug):
    post = get_object_or_404(Post, slug=slug)
    comments = post.comments.filter(active=True)
    new_comments = None
    if request.method == 'POST':
        comment_form = CommentForm(data=request.POST)
        if comment_form.is_valid():
            
            new_comment = comment_form.save(commit=False)

            new_comment.post = post
                 new_comment.save()
    else:
        comment_form = CommentForm()
    context = {
        'post': post,
        'comments': comments,
        'new_comments': new_comments,
        'comment_form': comment_form
    }
    return render(request, 'blog_detail.html', context)

We use the get_object_0r_404() function to get the Post model. The post variable contains the current post. if not found, it raises an Http404 exception. Once found, it retrieves only approved comments. The word β€˜comments’ in post.comments is the related name we assigned to the post field while creating a relationship with the Post model.

For new comments, we call in the CommentForm class. It passes in comment details for validation. Once validated, we assign it to the new_comment variable that was initially set to None, but without saving it (commit=False) until we link it to the post object which is the current post. Afterward, we save the form into the database.

If the request method is GET, an empty form will be passed to the template. In the end, whatever results from executing the view function, whether the request method is POST or GET, is included in the context dictionary and passed to the template.

If this seems difficult to understand, take your time to read the above explanations again.

Creating Templates

Adjust the blog_detail.html file to look like this:

{% extends 'base.html' %}
{% block content %}

<div class="container">
  <div class="row">
    <div class="col-md-8 card mb-4  mt-3 left  top">
      <div class="card-body">
        <h1>{% block title %} {{ post.title }} {% endblock title %}</h1>
        <p class=" text-muted">{{ post.author }} | {{ post.created_on }}</p>
        {% for category in post.categories.all %}
        <p class="text-muted">{{ category.name }}</p>
        {% endfor %}
        <p class="card-text ">{{ post.body | safe }}</p>
      </div>
    </div>
    {% block sidebar %} {% include 'sidebar.html' %} {% endblock sidebar %}
  </div>
</div>

    <div class="col-md-8 card mb-4  mt-3 ">
      <div class="card-body">
        <!-- comments -->
        <h2>{{ comments.count }} comments</h2>

        {% for comment in comments %}
        <div class="comments" style="padding: 10px;">
          <p class="font-weight-bold">
            {{ comment.name }}
            <span class=" text-muted font-weight-normal">
              {{ comment.created_on }}
            </span>
          </p>
          {{ comment.body | linebreaks }}
        </div>
        {% endfor %}
      </div>
    </div>
    <div class="col-md-8 card mb-4  mt-3 ">
      <div class="card-body">
      {% if new_comment %}
        <div class="alert alert-success" role="alert">
          Your comment will appear once approved
        </div>
        {% else %}
        <h3>Leave a comment</h3>
        <form method="post" style="margin-top: 1.3em;">
          {{ comment_form.as_p }}
          {% csrf_token %}
          <button type="submit" class="btn btn-primary  btn-lg">Submit</button>
        </form>
        {% endif %}
      </div>
    </div>
  </div>
</div>
{% endblock content %}

We use Django templating engine to dynamically loop through all the comments and display them. If new comments are added, we display a message, β€˜Your comment will appear once approved.’ Once the comments are approved by the admin, they will be displayed.

Otherwise, we display the form for users to comment. Remember, the comment_form holds the form object. By using comment_form.as_p, we want the form to be rendered within the paragraph tags. As usual, the csrf_token serves as a protection to the form from cross-site scripting attacks.

The URLs

Update the URL with the following:

path('<slug:slug>/', blog_detail, name='blog_detail')

Testing the Form

Start the local server to test the form.

Enter comments by filling out the form. Then go to the admin interface to approve the comments.

Styling the Form

We can make the form look better by using django-crispy-form. Install it using pip.

pip install django-crispy-forms

After installation, add it to the INSTALLED_APPS section of the settings.py file.

INSTALLED_APPS = [
    ...
    # third-party app
    'crispy_forms',
]

Notice in the base.html file that we are using Bootstrap. Hence, we have to indicate that in the settings.py file.

CRISPY_TEMPLATE_PACK = 'bootstrap4'

In that case, we also have to install crispy-bootstrap4 and add it to the INSTALLED_APPS.

INSTALLED_APPS = [
    ...
    # third-party apps
    'crispy_forms',
    'crispy_bootstrap4',
]

Let’s now adjust the blog_detail markup file to use the crispy form. On top of the file, load the crispy tag.

{% load crispy_forms_tags %}
...
<form method="post" style="margin-top: 1.3em;">
          {{ comment_form | crispy }}
          {% csrf_token %}
          <button type="submit" class="btn btn-primary  btn-lg">Submit</button>
 </form>

We make a one-line change removing comment_form.as_p and replacing it with Django crispy form tag.

Start the server to see the change.

Thanks to django-crispy-forms, the form now looks more mature.

Conclusion

In this tutorial, we created a comment system for readers to give feedback on a given post read.

This involves creating a database model, customizing the admin interface, creating a form for comments; adjusting the view function, the URLs, and the template.

No doubt, it was an interesting project tutorial. The source code on my GitHub page has been updated to reflect these changes.

Are we ready to deploy the app to a live server? Not yet. The blog content is so congested and not formatted. No pictures are displayed. I also want to include other things I mentioned in the previous series. We still have a lot to do.

However, we are making good progress. In the next series of this project, we will start with formatting our blog content. Stay tuned!

Here’s the next part of the series:

🀠 Recommended: How I Created a Blog Application Using Django – Part 4

Also, you may enjoy the following tutorial:

πŸ”— Recommended: How I Created a Sketch-and-Cartoon-Making App Using Flask