This article will demonstrate how to add some interactivity to your site by allowing your readers to comment, using the package Django Comments Xtd. Leave a comment to let me know what you think!
Building your own comment functionality without external packages is not very difficult, but will only provide you with basic functionality. Integrating with external players such as Disqus is another option. In this tutorial we will build our comment functionality with the package Django Comments Xtd, which extends the once official Django Comments Framework. It has a lot of the functionality you would expect from a comments framework, such as commenting on comments (threads), flagging, like/dislike, spam prevention etc.
The instructions tell us to install it first:
pip3 install django-comments-xtd
and add it to requirements.txt
. Check that you have django.contrib.sites
in your INSTALLED_APPS
and set the domain of the Site in your Django admin equal to localhost:8000
. Add
'django_comments_xtd',
'django_comments',
to your INSTALLED_APPS
after the application in which you use them. Make sure you have email configured in your settings file, since the application is going to use it. The application's tutorial gives a complete overview of the configurations; we will implement feedback (like/dislike) and threading, as well as add some functionality. There are some standard settings to add to our base.py
(replace example.com
with your domain):
COMMENTS_APP = 'django_comments_xtd'
COMMENTS_XTD_SALT = (b"Timendi causa est nescire. "
b"Aequam memento rebus in arduis servare mentem.")
COMMENTS_XTD_FROM_EMAIL = "noreply@example.com"
COMMENTS_XTD_CONTACT_EMAIL = "helpdesk@example.com"
We also need to add the url-space to our project's urls.py
(note: the tutorial uses raw string syntax r''
; we don't need that here because there are no special characters):
path('comments/', include('django_comments_xtd.urls')),
In our models.py
we need to add a get_absolute_url
method to our ArticlePage
model, since by default it doesn't have one. It does however have a get_url
method, so we simply add:
def get_absolute_url(self):
return self.get_url()
We will also slightly change the comment model in two ways. Firstly we will add a page
field to it, so that we can directly access the page from a comment. This makes it easy to list all comments per page, e.g. in admin, if we choose so. Secondly, if a registered user makes the comment, we automatically fill the user_name
field with the field display_name
of our user model. By default it is populated with the user's full name, but since we have the display_name
in our user model, we might as well use it.
from django_comments_xtd.models import XtdComment
class CustomComment(XtdComment):
page = ParentalKey(ArticlePage, on_delete=models.CASCADE, related_name='customcomments')
def save(self, *args, **kwargs):
if self.user:
self.user_name = self.user.display_name
self.page = ArticlePage.objects.get(pk=self.object_pk)
super(CustomComment, self).save(*args, **kwargs)
The ParentalKey
field speaks for itself. To save the page and the display name automatically we override the save()
method of the model. The Django comments model and therefore its extended version XtdComment already have fields user_name
, user
(with the current user model) and object_pk
, being the primary key of the object to which the comment is linked, in our case an ArticlePage
instance. Note: for non-registered users that submit comments, the field user
will not be filled, hence the conditional statement. We use these fields in the save()
method. We need to make this new model known in our settings with:
COMMENTS_XTD_MODEL = 'cms.models.CustomComment'
Showing all comments per page in Wagtail admin is now as simple as adding the following line to the content_panels
of our ArticlePage
model:
InlinePanel('customcomments', label=_("Comments")),
However with a lot of comments this might get a bit messy, so feel free to skip this. The comment overview in the traditional Django admin is much better. In order to have this, we need to add the following to our admin.py
:
from django_comments_xtd.admin import XtdCommentsAdmin
class CustomCommentAdmin(XtdCommentsAdmin):
list_display = ('cid', 'name', 'page', 'object_pk',
'ip_address', 'submit_date', 'followup', 'is_public',
'is_removed')
fieldsets = (
(None, {'fields': ('content_type', 'page', 'object_pk', 'site')}),
('Content', {'fields': ('user', 'user_name', 'user_email',
'user_url', 'comment', 'followup')}),
('Metadata', {'fields': ('submit_date', 'ip_address',
'is_public', 'is_removed')}),
)
admin.site.register(CustomComment, CustomCommentAdmin)
Compared to the example in the tutorial we have only added the page
field and removed a few fields.
Time to look at the templates. Since this is described very well in the tutorial we will go quickly. We add a comment form and all existing comments to the template which holds our article (in our case article_page.html
):
{% load wagtailimages_tags widget_tweaks comments comments_xtd static %}
<div class="container-fluid mt-4 comment-form">
{% render_comment_form for page %}
</div>
{% get_comment_count for page as comment_count %}
{% if comment_count %}
<hr>
<div class="container-fluid mt-4 media-list">
{% render_xtdcomment_tree for page allow_feedback show_feedback %}
</div>
{% endif %}
Django Comments Xtd provides three template tags here:
render_comment_form
to render the comment formget_comment_count
to retrieve the number of comments for the current object (in our case the page
object)render_xtdcomment_tree
to render all comments in threaded fashion for the current pageIt also provides two arguments:
allow_feedback
to enable like / dislike (thumbs up / down symbols)show_feedback
to show the number of likes and dislikesThe template tags are loaded at the top through comments
and comments_xtd
, together with other tags that we need for included templates (see below). The settings that need to be added to our settings file to make this work are:
COMMENTS_XTD_MAX_THREAD_LEVEL = 1 # default is 0
COMMENTS_XTD_LIST_ORDER = ('-thread_id', 'order') # default is ('thread_id', 'order')
COMMENTS_XTD_APP_MODEL_OPTIONS = {
'cms.articlepage': {
'allow_flagging': False,
'allow_feedback': True,
'show_feedback': True,
}
}
The first parameter allows replies on comments, but not replies on replies. Of course you can change this as you like. The second parameter makes comments appear in reverse chronological order. The third parameter disables flagging (explained in the tutorial) and allows feedback.
We also want to change two templates:
form.html
displays the comment form. Copy it and put it in a new directory /comments
in your app's /templates
directory.comment_tree.html
displays all threaded comments. Copy it and put it in a new directory /django_comments_xtd
in your app's /templates
directory.We will leave all other templates as they are, but feel free to change their styling as you see fit. Looking at form.html
we see a lot of elements: hidden fields, a honeypot (for anti-spam) two submit buttons (one for preview). Be careful not to remove anything vital! We will do the following:
account/form_field.html
template, which will give us the widget-tweaks
styling and feedback, and also simplify the template somewhat,class="needs-validation" novalidate
to our form
tag,offset
classes on the checkbox and submit buttons.With that our template form.html
becomes:
{% load i18n comments %}
<form method="POST" action="{% comment_form_target %}" class="needs-validation" novalidate>
{% csrf_token %}
<fieldset>
<input type="hidden" name="next" value="{% url 'comments-xtd-sent' %}"/>
<div class="alert alert-danger" data-comment-element="errors" hidden>
</div>
{% for field in form %}
{% if field.is_hidden %}<div>{{ field }}</div>{% endif %}
{% endfor %}
<div style="display:none">{{ form.honeypot }}</div>
<div class="form-group">
{% with field=form.comment %}{% include "account/form_field.html" %}{% endwith %}
</div>
{% if not request.user.is_authenticated %}
<div class="form-group">
{% with field=form.name %}{% include "account/form_field.html" %}{% endwith %}
</div>
{% endif %}
{% if not request.user.is_authenticated or not request.user.email %}
<div class="form-group">
{% with field=form.email %}{% include "account/form_field.html" %}{% endwith %}
</div>
{% endif %}
<div class="form-group custom-control custom-checkbox">
<small>
{{ form.followup }}
<label for="id_followup{% if cid %}_{{ cid }}{% endif %}" class="custom-control-label"> {{ form.followup.label }}</label>
</small>
</div>
</fieldset>
<div class="form-group">
<input type="submit" name="post" value="{% trans 'Send' %}" class="btn btn-outline-primary btn-sm" />
<input type="submit" name="preview" value="{% trans 'Preview' %}" class="btn btn-outline-secondary btn-sm" />
</div>
</form>
In the template comment_tree.html
we replace the CSS class name comment
by xtdcomment
, to prevent a clash with Wagtail Code Block, and remove a bit of spacing (pb-3
in line 11, to be exact). We will also add the user photo of our user model. The code replacing the current code for the photo becomes:
{% if item.comment.user.photo %}
<img alt="" src="{{ item.comment.user.photo.url }}" class="profile-image-thumbnail">
{% else %}
{{ item.comment.user_email|xtd_comment_gravatar }}
{% endif %}
Note: the like / dislike symbols are generated through FontAwesome, and although they seem to be in the static files of Django Comments Xtd, I couldn't get them to work. So I added the following to my base.html
template:
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.0.8/css/all.css">
Styling is as simple as possible and consistent with the rest of the site. We will left-align text and forms and use a width of 800px
. The first class below is generated in account/form_field.html
, all others have been defined explicitly. Add to the CSS file:
.form-control.input-field-comment {
width: 800px;
max-width: 100%;
height: 80px;
}
/* for the comment section on article pages */
.comment-form {
width: 800px;
max-width: 100%;
margin-left: 0;
}
.media-list {
width: 800px;
max-width: 100%;
margin-left: 0;
}
.xtdcomment {
border: 1px solid lightgrey;
margin-bottom: 10px;
padding: 5px;
}
/* for thumbnail image in comments */
.profile-image-thumbnail {
float: left;
width: 64px;
height: 64px;
max-width: 100%;
margin-right: 5px;
margin-bottom: 5px;
}
That's all. Migrate the database to incorporate the model changes and run the server. When you are logged in, you will only see a comment form field and a to-be-notified checkbox. When you are not logged in, you will see two additional fields for a name and an email. Submitting a comment requires verification by email. This can be switched off with COMMENTS_XTD_CONFIRM_EMAIL = False
, but then it is wise to take additional measures against spam and misuse, as described in the tutorial. Try submitting and previewing some comments, replying on comments, liking / disliking comments. You can view all comments in Django admin.
One inconvenience is that when a registered user wants to comment but is not logged in, he/she needs to go to the login page to log in and then come back to the specific article to comment. Read on you want to integrate the login into the article page to solve this. Also we will discuss Django Comments Xtd a bit more there.
Comment on this article (sign in first or confirm by name and email below)