Let's build a recipe sharing web app using Django - Part 4

Looking back: In Part 3 we designed our base, list and detail templates and introduced Bootstrap for styling.  The application can now list and display posts nicely.  In this post we’ll go one step further by adding a commenting system and wiring it up to Django’s user authentication so people can leave feedback on recipes.

Sharing a recipe is fun, but reading people’s feedback is even better.  We’ll add a simple comments system and a like counter.  Django’s authentication framework gives us user accounts, so readers will need to log in to comment.

Creating the Comments app

In the terminal, run:

python manage.py startapp comments

Add 'comments' to the INSTALLED_APPS list in foodiegram/settings.py (underneath 'posts') so Django picks up the models.

Defining the Comment model

Open comments/models.py and define a Comment class.  Our model associates a comment with a post and a user, stores the comment text, timestamps, and supports threaded replies.  The Foodiegram tutorial defined the following fields:

from django.db import models
from django.contrib.auth.models import User
from posts.models import Post

class Comment(models.Model):
    post = models.ForeignKey(Post, on_delete=models.CASCADE, related_name='comments')
    author = models.ForeignKey(User, on_delete=models.CASCADE)
    content = models.TextField(max_length=1000)
    created_date = models.DateTimeField(auto_now_add=True)
    updated_date = models.DateTimeField(auto_now=True)
    parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, related_name='replies')

    class Meta:
        ordering = ['-created_date']

    def __str__(self):
        return f'Comment by {self.author.username} on {self.post.title}'

    @property
    def is_reply(self):
        return self.parent is not None

Run migrations to create the comments table:

python manage.py makemigrations comments
python manage.py migrate

Comment form

Create comments/forms.py and add a simple form to capture the comment text.  In Foodiegram the CommentForm uses a single content field with a textarea widget:

from django import forms
from .models import Comment

class CommentForm(forms.ModelForm):
    class Meta:
        model = Comment
        fields = ['content']
        widgets = {
            'content': forms.Textarea(attrs={
                'class': 'form-control',
                'rows': 3,
                'placeholder': 'Share your thoughts about this recipe…'
            })
        }

Comment views and URLs

In comments/views.py write two functions: one to add a comment and one to delete it.  You’ll need to decorate them with @login_required so only authenticated users can post or remove comments.  The Foodiegram tutorial outlines the logic: get the target Post, instantiate CommentForm with POST data, link the comment to the current user and the post, handle replies via parent_id, and save.  Here’s a simplified version:

from django.shortcuts import get_object_or_404, redirect
from django.contrib.auth.decorators import login_required
from django.contrib import messages
from posts.models import Post
from .models import Comment
from .forms import CommentForm

@login_required
def add_comment(request, post_slug):
    post = get_object_or_404(Post, slug=post_slug)
    if request.method == 'POST':
        form = CommentForm(request.POST)
        if form.is_valid():
            comment = form.save(commit=False)
            comment.post = post
            comment.author = request.user
            parent_id = request.POST.get('parent_id')
            if parent_id:
                comment.parent = Comment.objects.get(id=parent_id)
            comment.save()
            messages.success(request, 'Your comment has been added!')
    return redirect('post_detail', slug=post_slug)

@login_required
def delete_comment(request, comment_id):
    comment = get_object_or_404(Comment, id=comment_id)
    if comment.author == request.user or request.user.is_staff:
        post_slug = comment.post.slug
        comment.delete()
        messages.success(request, 'Comment deleted successfully.')
        return redirect('post_detail', slug=post_slug)
    messages.error(request, 'You can only delete your own comments.')
    return redirect('post_detail', slug=comment.post.slug)

Create a comments/urls.py and map these views:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^add/(?P<post_slug>[-\w]+)/$', views.add_comment, name='add_comment'),
    url(r'^delete/(?P<comment_id>\d+)/$', views.delete_comment, name='delete_comment'),
]

Finally, include the comments URLs in the project’s urls.py so they’re routed correctly:

url(r'^comments/', include('comments.urls')),

Displaying comments in the post detail template

At the bottom of post_detail.html, below the recipe content, add a form to post comments and list existing ones.  Loop over post.comments and nest replies.  Wrap the form in {% if user.is_authenticated %} so only logged‑in users see it.  For brevity this code isn’t shown here, but it follows the same pattern as the view.

Enabling user registration and login

Django’s authentication system provides helpers for creating users.  The UserCreationForm contains username, password1 and password2 fields.  In a new app called accounts you can implement a registration view like this:

from django.contrib.auth.forms import UserCreationForm
from django.shortcuts import render, redirect
from django.contrib import messages

def register(request):
    if request.method == 'POST':
        form = UserCreationForm(request.POST)
        if form.is_valid():
            form.save()
            messages.success(request, 'Account created successfully')
            return redirect('login')
    else:
        form = UserCreationForm()
    return render(request, 'accounts/register.html', {'form': form})

For login and logout you can either use Django’s built‑in class‑based views or roll your own.  The authenticate() and login() functions accept a username and password and return a User object if the credentials are valid; if the user is not authenticated you can display an error message.  Similarly, logout() clears the session.

Add URL patterns for registration, login and logout in accounts/urls.py and include them in the project’s urls.py.