Add comment threads to Django in 9 mins 🧵
We'll build a simple app that shows user comments in 9 minutes. This includes:
allowing users to add comments and reply to comments.
showing each user's profile image (using the Gravatar API).
adding sample comments into the Django database from yaml (using the Django loaddata
management command).
To see a full demo of the app, check out the Circumeo link at the end of the article.
Optional video guide (featuring me 🏇🏿) here:
Let's get cooking 👨🍳 (i.e., coding)
Set up our Django app
Install packages and create our Django app
pip install --upgrade django pyyamldjango-admin startproject core .python3 manage.py startapp sim
Add our app sim
to the INSTALLED_APPS.
- Add the
humanize
app to INSTALLED_APPS. We'll use this to show how long ago a comment was published.
# settings.pyINSTALLED_APPS = ["django.contrib.humanize","sim",...]
Add templates
- Create a folder named
templates
in thesim
app. - Create a file
base.html
in thetemplates
directory.
<!doctype html><html lang="en"><head><meta charset="UTF-8" /><style>#container {display: flex;justify-content: center;padding: 10px;}#reply-container {padding: 10px;}#reply-container,.comments {background-color: #f6f6ef;}.comment-head {display: flex;gap: 8px;font-size: 10px;color: #828282;margin-bottom: 4px;}.comment {font-family: Verdana, Geneva, sans-serif;font-size: 9pt;}.comment-body {margin-bottom: 4px;}.reply-link {display: inline-block;margin-bottom: 8px;color: #000;}</style></head><body><div id="container">{% block content %}{% endblock %}</div></body></html>
- Create a folder called
partials
in thetemplates
folder. - Create a file
_comment.html
in thetemplates/partial
folder:
{% load humanize %} {% load custom_tags %}<table><tr><td><imgsrc="data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw=="width="{% if indent %}40{% else %}0{% endif %}"height="1"/></td><td><div class="comment"><div class="comment-head"><div class="author-gravatar"><imgsrc="https://www.gravatar.com/avatar/{{ comment.author.username|md5 }}?d=retro&s=12"alt="Gravatar"/></div><div class="comment-author">{{ comment.author.username }}</div><div class="comment-date">{{ comment.created_at|naturaltime }}</div></div><div class="comment-body">{{ comment.content }}</div><font size="1"><a href="{% url 'reply' comment_id=comment.id %}" class="reply-link">Reply</a></font>{% if children %}<div class="children">{% for child in comment.get_children %} {% include"partials/_comment.html" with comment=child children=True indent=True%} {% endfor %}</div>{% endif %}</div></td></tr></table>
- Create a file
comments.html
in thetemplates
directory:
{% extends "base.html" %} {% block content %}<table class="comments">{% for comment in root_comments %}<tr><td>{% include "partials/_comment.html" with comment=comment children=Trueindent=False %}</td></tr>{% endfor %}</table>{% endblock %}
- Create a file
reply.html
in thetemplates/sim
directory:
{% extends "base.html" %}{% block content %}<div id="reply-container"><div>{% include "partials/_comment.html" with comment=comment children=False indent=False %}</div><div><form method="post">{% csrf_token %}<textarea name="content" rows="8" cols="80" autofocus="true"></textarea><div><button type="submit" style={{ marginTop: '10px' }}>Submit</button></div></form></div></div>{% endblock %}
Add custom template tags
The Gravatar API expects an MD5 hash of the username. By using a hash, the API never sees any user data, but will return a consistent avatar.
- Add the
sim/templatetags
directory. - Create the
custom_tags.py
file within thesim/templatetags
folder.
from django import templateimport hashlibregister = template.Library()@register.filterdef md5(value):return hashlib.md5(value.encode('utf-8')).hexdigest()
Now our templates can use the md5
filter.
Add forms
- Copy the below into
sim/forms.py
:
from django import formsfrom .models import Commentclass CommentForm(forms.ModelForm):class Meta:model = Commentfields = ["content"]
Add views
- Copy the below into
sim/views.py
:
import uuidfrom django.contrib.auth.models import Userfrom django.shortcuts import render, redirectfrom django.contrib.auth import loginfrom .forms import CommentFormfrom .models import Commentdef comments(request):root_comments = Comment.objects.filter(parent=None)return render(request, "comments.html", {"root_comments": root_comments})def reply(request, comment_id):parent_comment = Comment.objects.get(pk=comment_id)if request.method == "POST":form = CommentForm(request.POST)if form.is_valid():new_comment = form.save(commit=False)new_comment.parent = parent_commentif request.user.is_authenticated:new_comment.author = request.userelse:# Create an anonymous user so that we don't have to be logged in# to make comments or replies.anonymous_username = f'Anonymous_{uuid.uuid4().hex[:8]}'anonymous_user, created = User.objects.get_or_create(username=anonymous_username)if created:anonymous_user.save()login(request, anonymous_user)new_comment.author = anonymous_usernew_comment.save()return redirect("comments")else:form = CommentForm()return render(request, "reply.html", {"comment": parent_comment, "form": form})
Urls
Update core.urls
with the below:
from django.contrib import adminfrom django.urls import include, pathurlpatterns = [path("admin/", admin.site.urls),path("", include("sim.urls")),]
Create a file urls.py
in sim
, containing:
from django.urls import pathfrom . import viewsurlpatterns = [path("", views.comments, name="comments"),path("reply/<int:comment_id>", views.reply, name="reply"),]
Add the database structure for storing comments
Add models.py
- Copy the below into
sim/models.py
:
from django.contrib.auth import get_user_modelfrom django.conf import settingsfrom django.db import modelsfrom django.utils import timezoneUser = get_user_model()class Comment(models. Model):content = models.TextField()created_at = models.DateTimeField(default=timezone.now, db_index=True)parent = models.ForeignKey("self",on_delete=models.CASCADE,related_name="children",db_index=True,null=True,blank=True,)author = models.ForeignKey(User, on_delete=models.CASCADE, related_name="comments", db_index=True)class Meta:ordering = ["created_at"]def is_root_comment(self):"""Check if this comment is a root comment (no parent)"""return self.parent is Nonedef get_children(self):"""Retrieve all direct child comments."""return Comment.objects.filter(parent=self)
- Run the below to create the database tables:
python manage.py migrate
Load comment data into your database
The slow way (Not recommended) 💤
Load data manually by:
-
creating a Django superuser and adding data through the Django admin, or
-
adding data through the Django shell, or
-
adding data directly into your database using SQL
-
The fast way (Recommended) 🏎
-
Load a batch of data into your database using the Django
loaddata
management command. -
Doing the fast way by using
loaddata
with yaml -
Create a file
comment_data.yaml
in the root folder. Here is some sample comment data that I've written to get you started:
Click to see the sample data (Scroll down to the copy button)
- model: auth.userpk: 1fields:username: 'PurrfectPaws'email: 'purrfectpaws@example.com'is_staff: falseis_active: truedate_joined: '2024-03-24T00:00:00Z'- model: auth.userpk: 2fields:username: 'TheCatWhisperer'email: 'thecatwhisperer@example.com'is_staff: falseis_active: truedate_joined: '2024-03-24T01:00:00Z'- model: auth.userpk: 3fields:username: 'FelinePhilosopher'email: 'felinephilosopher@example.com'is_staff: falseis_active: truedate_joined: '2024-03-24T01:30:00Z'- model: auth.userpk: 4fields:username: 'CatNapConnoisseur'email: 'catnapconnoisseur@example.com'is_staff: falseis_active: truedate_joined: '2024-03-24T02:00:00Z'- model: sim.commentpk: 1fields:content: 'Anyone else’s cat obsessed with knocking things off tables? Mine seems to think it’s his life mission 😂'created_at: '2024-03-24T02:00:00Z'parent: nullauthor: 1- model: sim.commentpk: 2fields:content: 'Gravity checks, obviously! Cats are just doing important scientific work. Mine is currently researching the flight patterns of pens.'created_at: '2024-03-24T02:15:00Z'parent: 1author: 2- model: sim.commentpk: 3fields:content: 'Haha, that’s one way to look at it. Next, they’ll be winning Nobel Prizes for their contributions to physics!'created_at: '2024-03-24T02:30:00Z'parent: 2author: 1- model: sim.commentpk: 4fields:content: 'Right? 😆 Meanwhile, my cat’s dissertation on “The Optimal Time to Demand Feeding: A Study Conducted at 3AM” is pending review.'created_at: '2024-03-24T02:45:00Z'parent: 3author: 2- model: sim.commentpk: 5fields:content: 'Does anyone else’s cat have an existential crisis at midnight or is it just mine? Staring into the void, meowing at shadows...'created_at: '2024-03-24T03:00:00Z'parent: nullauthor: 3- model: sim.commentpk: 6fields:content: 'Oh definitely, it’s their way of pondering the universe. Mine likes to present his findings at 5AM, loudly, by my bedside.'created_at: '2024-03-24T03:15:00Z'parent: 5author: 4- model: sim.commentpk: 7fields:content: 'I introduced a new toy to my cat today, and now I can’t find it. I suspect it’s under the couch, along with all those missing socks.'created_at: '2024-03-24T03:30:00Z'parent: nullauthor: 2- model: sim.commentpk: 8fields:content: 'Update: Found the toy. It was indeed under the couch, along with a treasure trove of socks and a single, inexplicable cucumber.'created_at: '2024-03-24T03:45:00Z'parent: 7author: 2- model: sim.commentpk: 9fields:content: 'The cucumber mystery deepens. Perhaps it’s a new cat currency we’re yet to understand.'created_at: '2024-03-24T04:00:00Z'parent: 8author: 3- model: sim.commentpk: 10fields:content: 'Mine too!'created_at: '2024-03-24T04:00:00Z'parent: 5author: 1
Load the data into your database
- Run the below to load the data into your Django database. It will overwrite any existing data:
python manage.py loaddata comment_data.yaml
Here's an earlier post that covers importing and exporting data with YAML in more detail: Simply add (and export) data from your Django database with YAML (3 mins) 🧮
Run our app
If you're running the app locally, run the below to start the server:
If running locally
python manage.py runserver
If running on Circumeo (Full online demo 🎪):
Here's a full demo of the app (using Circumeo). To use this:
- Visit the project fork page and click the "Create Fork" button.
- Migrations will run and the app will launch in about 10 seconds.
- To load our initial data (necessary to avoid a blank screen):
- open the Shell tab and click Connect.
- type
python3 manage.py loaddata comment_data.yaml
into the shell and press enter
Congrats - You've created comment threads with Django 🎉
You've just built an app using Django that has comment threads, just like many popular social websites.
Here are some future enhancements you might consider:
- Content moderation. Allow an admin to approve comments before they are published.
- Allow users to edit and delete their own comments. Instead of a simple textarea, use a rich text editor.
Here's a quick video of me using Photon Designer to expand the comment thread app (to add Reddit-style collapsible threads):