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
.