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.
Here's a video of our final product with comments 🖊️:
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 pyyaml
django-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.py
INSTALLED_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>
<img
src="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">
<img
src="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=True
indent=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 template
import hashlib
register = template.Library()
@register.filter
def 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 forms
from .models import Comment
class CommentForm(forms.ModelForm):
class Meta:
model = Comment
fields = ["content"]
Add views
- Copy the below into
sim/views.py
:
import uuid
from django.contrib.auth.models import User
from django.shortcuts import render, redirect
from django.contrib.auth import login
from .forms import CommentForm
from .models import Comment
def 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_comment
if request.user.is_authenticated:
new_comment.author = request.user
else:
# 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_user
new_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 admin
from django.urls import include, path
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("sim.urls")),
]
Create a file urls.py
in sim
, containing:
from django.urls import path
from . import views
urlpatterns = [
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_model
from django.conf import settings
from django.db import models
from django.utils import timezone
User = 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 None
def 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.user
pk: 1
fields:
username: 'PurrfectPaws'
email: 'purrfectpaws@example.com'
is_staff: false
is_active: true
date_joined: '2024-03-24T00:00:00Z'
- model: auth.user
pk: 2
fields:
username: 'TheCatWhisperer'
email: 'thecatwhisperer@example.com'
is_staff: false
is_active: true
date_joined: '2024-03-24T01:00:00Z'
- model: auth.user
pk: 3
fields:
username: 'FelinePhilosopher'
email: 'felinephilosopher@example.com'
is_staff: false
is_active: true
date_joined: '2024-03-24T01:30:00Z'
- model: auth.user
pk: 4
fields:
username: 'CatNapConnoisseur'
email: 'catnapconnoisseur@example.com'
is_staff: false
is_active: true
date_joined: '2024-03-24T02:00:00Z'
- model: sim.comment
pk: 1
fields:
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: null
author: 1
- model: sim.comment
pk: 2
fields:
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: 1
author: 2
- model: sim.comment
pk: 3
fields:
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: 2
author: 1
- model: sim.comment
pk: 4
fields:
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: 3
author: 2
- model: sim.comment
pk: 5
fields:
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: null
author: 3
- model: sim.comment
pk: 6
fields:
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: 5
author: 4
- model: sim.comment
pk: 7
fields:
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: null
author: 2
- model: sim.comment
pk: 8
fields:
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: 7
author: 2
- model: sim.comment
pk: 9
fields:
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: 8
author: 3
- model: sim.comment
pk: 10
fields:
content: 'Mine too!'
created_at: '2024-03-24T04:00:00Z'
parent: 5
author: 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.
P. S Want to build Django frontend faster? ⚡️
I'm building Photon Designer. It's a visual editor that puts the 'fast' in 'very fast.'
When I'm using Photon Designer, I create clean Django UI faster than light escaping a black hole (in a metaphorical, non-literal way).
Here's a quick video of me using Photon Designer to expand the comment thread app (to add Reddit-style collapsible threads):