Creating a comment system

Now we are going to build a comment system for the blog, wherein the users will be able to comment on posts. To build the comment system, you will need to:

  • Create a model to save comments
  • Create a form to submit comments and validate the input data
  • Add a view that processes the form and saves the new comment into the database
  • Edit the post detail template to display the list of comments and the form for adding a new comment

First, let's build a model to store comments. Open the models.py file of your blog application and add the following code:

class Comment(models.Model):
    post = models.ForeignKey(Post, related_name='comments')
    name = models.CharField(max_length=80)
    email = models.EmailField()
    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 'Comment by {} on {}'.format(self.name, self.post)

This is our Comment model. It contains a ForeignKey to associate the 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 might have multiple comments. The related_name attribute allows us to name the attribute that we use for the relation from the related object back to this one. After defining this, we 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 undercase name of the model followed by _set (that is, comment_set) to name the manager of the related object back to this one.

You can learn more about many-to-one relationships at https://docs.djangoproject.com/en/1.8/topics/db/examples/many_to_one/.

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

The new Comment model 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:

python manage.py makemigrations blog

You should see this output:

Migrations for 'blog':
  0002_comment.py:
    - Create model Comment

Django has generated a file 0002_comment.py inside the migrations/ directory of the blog application. Now, you need to create the related database schema and apply the changes to the database. Run the following command to apply existing migrations:

python manage.py migrate

You will get an output that includes the following line:

Applying blog.0002_comment... OK

The migration we just created has been applied and now a blog_comment table exists in the database.

Now, we can add our new model to the administration site in order to manage comments through a simple interface. Open the admin.py file of the blog application and add an import for the Comment model and the following ModelAdmin:

from .models import Post, Comment

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

Start the development server with the command python manage.py runserver and open http://127.0.0.1:8000/admin/ in your browser. You should see the new model included in the Blog section, as shown in the following screenshot:

Creating a comment system

Our model is now registered into the admin site and we can manage Comment instances using a simple interface.

Creating forms from models

We still need to build a form to let our users comment on blog posts. Remember that Django has two base classes to build forms: Form and ModelForm. You used the first one previously to let your users share posts by e-mail. In the present case, you will need to use ModelForm because you have to build a form dynamically from your Comment model. Edit the forms.py of your blog application and add the following lines:

from .models import Comment

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

To create a form from a model, we 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 us. Each model field type has a corresponding default form field type. The way we define our model fields is taken into account for form validation. By default, Django builds a form field for each field contained in the model. However, you can explicitly tell the framework which fields you want to include in your form using a fields list, or define which fields you want to exclude using an exclude list of fields. For our CommentForm, we will just use the name, email, and body fields for the form because those are the only fields our users will be able to fill in.

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 models.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 .models import Post, Comment
from .forms import EmailPostForm, CommentForm

def post_detail(request, year, month, day, post):
    post = get_object_or_404(Post, slug=post,
                                   status='published',
                                   publish__year=year,
                                   publish__month=month,
                                   publish__day=day)

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

    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()
    else:
        comment_form = CommentForm()                               
    return render(request,
                  'blog/post/detail.html',
                  {'post': post,
                   'comments': comments,
                   'comment_form': comment_form})

Let's review what we have added to our view. We are using the post_detail view to display the post and its comments. We add a QuerySet to retrieve all active comments for this post:

    comments = post.comments.filter(active=True)

We are building this QuerySet starting from the post object. We are using the manager for related objects we defined as comments using the related_name attribute of the relationship in the Comment model.

We also use the same view to let our users add a new comment. Therefore 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, we instantiate the form using the submitted data and validate it using the is_valid() method. If the form is invalid, we render the template with the validation errors. If the form is valid, we take the following actions:

  1. We create a new Comment object by calling the form's save() method like this:
    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 with commit=False, you create the model instance, but you don't save it to the database. This comes very handy when you want to modify the object before finally saving, which is what we do next. The save() method is available for ModelForm but not for Form instances, since they are not linked to any model.

  2. We assign the current post to the comment we just created:
    new_comment.post = post

    By doing this, we are specifying that the new comment belongs to the given post.

  3. Finally, we save the new comment to the database with the following code:
    new_comment.save()

Our view is now ready to display and process new comments.

Adding comments to the post detail template

We have created the functionality to manage comments for a post. Now we need to adapt our post_detail.html template to do the following:

  • Display the total number of comments for the post
  • Display the list of comments
  • Display a form for users to add a new comment

First, we will add the total comments. Open the blog_detail.html template and append the following code inside the content block:

{% with comments.count as total_comments %}
  <h2>
    {{ total_comments }} comment{{ total_comments|pluralize }}
  </h2>
{% endwith %}

We are using the Django ORM in the template executing the queryset comments.count(). Note that 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.

Note

The {% with %} template tag is useful to avoid hitting the database or accessing expensive methods multiple times.

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 input and return a computed value. We will discuss template filters in Chapter 3, Extending Your Blog Application.

The pluralize template filter displays an "s" if the value is different than 1. The preceding text will be rendered as 0 comments, 1 comment, or N comments. Django includes plenty of template tags and filters that help you display information in the way you want.

Now, let's include the list of comments. Append the following lines to the template after the preceding code:

{% for comment in comments %}
  <div class="comment">
    <p class="info">
      Comment {{ forloop.counter }} by {{ comment.name }}
      {{ comment.created }}
    </p>
    {{ comment.body|linebreaks }}
  </div>
{% empty %}
  <p>There are no comments yet.</p>
{% endfor %}

We use the {% for %} template tag to loop through comments. We display a default message if the comments list is empty, telling our users there are no comments for this post yet. We enumerate comments with the {{ forloop.counter }} variable, which contains the loop counter in each iteration. Then we display the name of the user who posted the comment, the date, and the body of the comment.

Finally, you need to render the form or display a successful message instead when it is successfully submitted. Add the following lines just below the previous code:

{% if new_comment %}
  <h2>Your comment has been added.</h2>
{% else %}
  <h2>Add a new comment</h2>
  <form action="." method="post">
    {{ comment_form.as_p }}
    {% csrf_token %}
    <p><input type="submit" value="Add comment"></p>
  </form>
{% endif %}

The code is pretty straightforward: If the new_comment object exists, we display a success message because the comment was successfully created. Otherwise, we render the form with a paragraph <p> element for each field and include the CSRF token required for POST requests. Open http://127.0.0.1:8000/blog/ in your browser and click on a post title to see its detail page. You will see something like the following:

Adding comments to the post detail template

Add a couple of comments using the form. They should appear under your post in chronological order, like this:

Adding comments to the post detail template

Open http://127.0.0.1:8000/admin/blog/comment/ in your browser. You will see the admin page with the list of comments you created. Click on one of them to edit it, uncheck the Active checkbox, and click the Save button. You will be redirected to the list of comments again and the Active column will display an inactive icon for the comment. It should look like the first comment in the following screenshot:

Adding comments to the post detail template

If you go back to the post detail view, you will notice that the deleted comment is not displayed anymore; neither is it being counted for the total number of comments. Thanks to the active field, you can deactivate inappropriate comments and avoid showing them in your posts.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset