There are plenty out there but, as usual, I want to understand what I'm using. We'll build one from scratch. The approach is based on the Content Types framework. The only addition here is the integration in our django app and templating.
Prerequisites
- A basic django blogging app
- Some basic understanding of Django
ContentType app
The key to our "model-independent" part is the 'django.contrib.contenttypes'
app.
It is included by default in the Django apps.
First, create a fake_comments
app and add it to the settings file. Our basic Comment
model is
as follows
# fake_comments/models.py
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.conf import settings
from django.contrib.contenttypes.fields import GenericForeignKey
class Comment(models.Model):
# Content-object field
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
content_id = models.PositiveIntegerField(db_index=True)
# you can simply use content_obj = GenericForeignKey() when using the default names (content_type, object_id)
content_object = GenericForeignKey(ct_field="content_type", fk_field="content_id")
body = models.TextField('comment', max_length=settings.COMMENT_MAX_LENGTH)
# Metadata about the comment
class Meta:
indexes = [models.Index(fields=["content_type", "content_id"])]
db_table = 'fake_comments'
The Comment
model uses content_type
and content_id
to keep track of the type and id of the object that the comment
belongs to. content_object
provides a many-to-one relation with the parent object.
In this fashion the comment model does not have to be parent-model specific.
To access the comments from the parent (Post
) model you simply add the attribute:
comments = django.contrib.contenttypes.fields.GenericRelation(Comment, object_id_field="content_id")
The next step is to add a basic form to post comments and adjust our templates to display comments.
- Start by adding a
CommentAdmin
tofake_comments/admin.py
.
# fake_comments/admin.py
from django.contrib import admin
from fake_comments.models import Comment
@admin.register(Comment)
class CommentAdmin(admin.ModelAdmin):
list_display = ['body']
list_filter = ['content_type']
search_fields = ['body']
- and the rest of the setup
# fake_comments/forms.py
from django import forms
from django.contrib.contenttypes.models import ContentType
from .models import Comment
class CommentForm(forms.Form):
body = forms.CharField()
# fake_comments/views.py
from django.contrib.contenttypes.models import ContentType
# Create your views here.
from django.shortcuts import get_object_or_404
from django.http import HttpResponseRedirect
from .forms import CommentForm
from .models import Comment
def submit_comment(request, content_name, content_id):
content_type = get_object_or_404(ContentType, **dict(zip(["app_label", "model"], content_name.split('_'))))
model_class = content_type.model_class()
content_obj = get_object_or_404(model_class, pk=content_id)
if request.method == 'POST':
form = CommentForm(request.POST)
if form.is_valid():
comment = Comment(content_type=content_type, content_id=content_id, body=form.data['body'])
# or the following
# comment = Comment(content_object=content_obj, body=form.data['body'])
comment.save()
return HttpResponseRedirect(content_obj.get_abs_url())
The comment's POST request needs the comment's body, the id of the content it's related to, and the content name.
I used the format {app_label}_{model_name}
for content_name
. We only need to define content_object
or (content_type, content_id)
. Doing both is redundant.
# fake_comments/urls.py
from django.urls import path
from . import views
# important for url tag to recognize the app namespace
app_name = "fake_comments"
urlpatterns = [
path('<str:content_name>/<int:content_id>/comment/', views.submit_comment, name='submit_comment'),
]
<!--templates/comments/submit_comment.html-->
{% load i18n %}
<form action="{% url 'fake_comments:submit_comment' content_name content_id %}" method="post">
<p>
<textarea name="body" cols="45" rows="10" placeholder="Type comment here" maxlength="1000" required=""
id="id_body" aria-label="body"></textarea>
</p>
{% csrf_token %}
<button type="submit">Submit Comment</button>
</form>
- In your post template, you simply need to
{% include 'comments/submit_comment' %}
. - In your post view, add
content_name, content_id
to your context:
{
...,
'content_name': f'{post._meta.app_label}_{post._meta.model_name}',
'content_id': post.id,
...
}
I hope it all makes sense now. Unfortunately, any view in Django should direct you to a specific url (page refresh). The natural next step would be to post and get comments via javascript for a smoother experience... and some comments moderation.