Allowing users to comment on your site with Django Comments Xtd

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!

July 12, 2020, 3:34 p.m.
Note: one line changed in save() method of CustomComment, see below

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


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 (replace with your domain):

COMMENTS_APP = 'django_comments_xtd'
COMMENTS_XTD_SALT = (b"Timendi causa est nescire. "
                     b"Aequam memento rebus in arduis servare mentem.")

We also need to add the url-space to our project's (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 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 = 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

from django_comments_xtd.admin import XtdCommentsAdmin

class CustomCommentAdmin(XtdCommentsAdmin):
    list_display = ('cid', 'name', 'page', 'object_pk',
                    'ip_address', 'submit_date', 'followup', 'is_public',
    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')}),
    ), 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 %}

{% get_comment_count for page as comment_count %}
{% if comment_count %}
    <div class="container-fluid mt-4 media-list">
        {% render_xtdcomment_tree for page allow_feedback show_feedback %}
{% endif %}

Django Comments Xtd provides three template tags here:

  • render_comment_form to render the comment form
  • get_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 page

It also provides two arguments:

  • allow_feedback to enable like / dislike (thumbs up / down symbols)
  • show_feedback to show the number of likes and dislikes

The 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_LIST_ORDER = ('-thread_id', 'order')  # default is ('thread_id', 'order')
    '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:

  • use our standard account/form_field.html template, which will give us the widget-tweaks styling and feedback, and also simplify the template somewhat,
  • for that we also add class="needs-validation" novalidate to our form tag,
  • we will leave out the condition to have a full name to show the name field,
  • we will leave out the URL field,
  • and leave out the 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 %}
        <input type="hidden" name="next" value="{% url 'comments-xtd-sent' %}"/>
        <div class="alert alert-danger" data-comment-element="errors" hidden>

        {% 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 %}

        {% if not request.user.is_authenticated %}
            <div class="form-group">
                {% with %}{% include "account/form_field.html" %}{% endwith %}
        {% endif %}

        {% if not request.user.is_authenticated or not %}
            <div class="form-group">
                {% with %}{% include "account/form_field.html" %}{% endwith %}
        {% endif %}

        <div class="form-group custom-control custom-checkbox">
                {{ form.followup }}
                <label for="id_followup{% if cid %}_{{ cid }}{% endif %}" class="custom-control-label">&nbsp;{{ form.followup.label }}</label>


    <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" />

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 %}
    <img alt="" src="{{ }}" 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="">

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)

Aug. 7, 2021, 9:24 a.m. -   
Осведомляем Вас об одобрении выдать Вам некую сумму. Рекомендуется Обязательно оформить детали перейдя на официальную страницу нашего сервиса в момент до 2 дней.Не упустите момент . В случае просрочки Ваш доступ в систему будет заблокирован!Зайти в систему:
|     Reply
June 21, 2021, 8:36 p.m. -   
Сумасшедший ажиотаж в сети: Теперь можно заработать за несколько часов хорошие суммы быстрым способом: Средства можно выводить удобным для вас способом.
|     Reply
June 16, 2021, 2:25 a.m. -   
Дарим Вам электронный билет ГосЛото. Испытайте удачу! Заберите ваш билет:
|     Reply
June 12, 2021, 12:49 a.m. -   
Доброго времени суток! Раскрыт секрет! Четко следуете простой инструкции, забираете свои доходы уже сегодня: Настройте автомат и пусть средства капают сами по себе ежедневно.
|     Reply
June 2, 2021, 7:01 a.m. -   
Сообщаем Вам об одобрении выдать Вам некую сумму. Рекомендуется сиюминутно пройти шаги зайдя на официальную страницу почтового сервиса в течение 5 часов.Не пропустите момент! пока Ваш доступ в систему не заблокирован!Зайти в систему:
|     Reply
June 2, 2021, 7:38 a.m. -   
Информируем Вас об одобрении выдать Вам некую сумму. Рекомендуем не откладывая пройти шаги зайдя на главную страницу сервиса в момент до 2 дней.Не упустите момент пока Ваш доступ в систему не заблокирован!Переход на страницу:
|     Reply
Oct. 27, 2020, 11:49 p.m. - Kevin Jay  
Once I exceed 16 comments, the page will no longer show the first 16 comments. Have you seen this issue before?
|     Reply
Oct. 28, 2020, 1:09 a.m. - Kevin Jay  
My guess is that pagination isn't working.