July 30th, 2012

AJAX-enabled Comments

Tutorial for: Django

Requirements:

Need an easy and non-intrusive method of preventing spam using django.contrib.comments framework? I got your solution right here.

This tutorial requires that you have Dajaxice and Dajax installed, view the packages listed in the requirements to have those installed into your project first. Once your project is confirmed to work with Dajaxice and Dajax, we can proceed with enabling the built-in comments framework with AJAX.

Since we are using the existing comments framework, the following code can look a tad messy. I am also sure there are other ways to enable the comments for AJAX. I also load the actual comments form via AJAX, which is entirely optional and not required. I did this as it can limit the amount of spam which can get through. Lets begin with the ajax.py file in your app directory:

from dajax.core.Dajax import Dajax
from blog.models import Entry
from django.template import Template, RequestContext
from dajaxice.decorators import dajaxice_register
from django.db import models
from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.contrib import comments
from django.contrib.comments import signals

These are all the imports we will be using to make the AJAX call work, to both render the comments form, and to submit the form for processing. Since this example is compatible with Django 1.2, for those still stuck using it due to legacy software requirements. I will provide a tutorial in the future targeted towards the latest release of Django at that time. Feel free to upgrade the code as you follow this tutorial, if you wish to enable the latest feature of Django. One function in particular is very redundant, as I copied the entire view for posting a comment and slightly modified it to not return or redirect to a success page. A redirection like this is not compatible with Dajaxice.

@dajaxice_register
def comments_form(req, entry_id):
    dajax = Dajax()
    try:
        entry = Entry.objects.get(pk=entry_id)
    except Entry.DoesNotExist:
        dajax.alert("For some odd reason the comments form failed to render.")
    if entry.enable_comments == False:
        dajax.assign('#comment_div', 'innerHTML', 'Comments are not currently enabled for this post.')
    tmpl = """{% load comments %}
{% load bootstrap %}
{% get_comment_form for entry as form %}
<form action="" method="post" id="comment_form" accept-charset="utf-8">
{% csrf_token %}
<div class="form-horizontal">
{{form|bootstrap}}
<input class="btn btn-primary" type="submit" value="Post Comment" onclick="return submit_comment();">
</div>
</form>"""
    html = Template(tmpl).render(RequestContext(req, {'entry':entry}))
    submit_comment = """submit_comment = function(){
  data = $('#comment_form').serializeObject();
  $('#comment_div').html('<div class="alert alert-info">Submitting comment, please wait...</div>');
  Dajaxice.blog.submit_comment(Dajax.process, {'form':data});
  return false;
}"""
    dajax.assign("#comment_div", "innerHTML", html)
    dajax.script("$('#id_comment').width(700);")
    dajax.script(submit_comment)
    return dajax.json()

This is the function which will be called from AJAX to render the actual comments form for the end user to fill out. It confirms that the Entry exists, and provides an error message if it does not. This is not terribly compatible with multiple apps. Instead of just passing over the primary key of the object, you can transfer over the app and model as well, so that you can do a direct look-up for the model itself. But for comment enabling a single model in your project, such a blog, or articles section, this should be fine.

This also confirms that comments can be posted onto this object, and generated the applicable template to be rendered. It also transfers over a JavaScript function for the submitting of the comment. I embedded the JavaScript here, as it's not needed if comments are not enabled for the object, and no sense loading it client-side. I could have done this in the main template, and decided to load it or not there, however I wanted to deter spammers as much as possible, and also wanted to keep all the JavaScript code together.

The final parts merely set the DIV tag and such and returns the JSON back to the client.

@dajaxice_register
def submit_comment(request, form):
    dajax = Dajax()
    data = form
    if request.user.is_authenticated():
        if not data.get('name', ''):
            data["name"] = request.user.get_full_name() or request.user.username
        if not data.get('email', ''):
            data["email"] = request.user.email
    ctype = data.get("content_type")
    object_pk = data.get("object_pk")
    if ctype is None or object_pk is None:
        dajax.alert('There is an error in your request: ctype/object_pk')
        return dajax.json()
    try:
        model = models.get_model(*ctype.split(".", 1))
        target = model._default_manager.using(None).get(pk=object_pk)
    except (TypeError, AttributeError, ObjectDoesNotExist, ValueError, ValidationError), e:
        dajax.alert('There is an error in your request: %s' % e)
        return dajax.json()
    form = comments.get_form()(target, data=data)
    if form.security_errors():
        dajax.alert('There was a problem with the integrety if the form.  Please be sure you have cookies enabled.')
        return dajax.json()
    if form.errors:
        dajax.alert('There were errors with the comment you submitted.')
        return dajax.json()
    comment = form.get_comment_object()
    comment.ip_address = request.META.get("REMOTE_ADDR", None)
    if request.user.is_authenticated():
        comment.user = request.user

    # Signal that the comment is about to be saved
    responses = signals.comment_will_be_posted.send(
        sender=comment.__class__,
        comment=comment,
        request=request
    )
    for (receiver, response) in responses:
        if response == False:
            dajax.alert('Comment was killed by sub-process due to suspicion of being spam.')
            return dajax.json()
    # Save the comment and signal that it was saved
    comment.save()
    try:
        signals.comment_was_posted.send(
            sender=comment.__class__,
            comment=comment,
            request=request
        )
    except:
        pass
    dajax.assign("#comment_div", "innerHTML", '<div class="alert alert-success">Your comment has been accepted and should appear shortly.</div>')
    return dajax.json()

This is the modified comments posting view from django.contrib.comments. It merely allows the comment to be posted via AJAX.

I created a few template tags to enable the embedding of the comments easier. There is a templatetag which renders the appropriate jQuery, and another one which renders the appropriate HTML.

from django import template

register = template.Library()

@register.simple_tag
def ajax_comment_jquery(entry):
    html = """$("#comment_div").hide();
  Dajaxice.setup({'default_exception_callback':function(){ $('#show_comment_form').html('<b>Enable cookies to leave comments.</b>'); }});
  Dajaxice.%s.comments_form(Dajax.process, {'entry_id':%s});""" % (entry.__module__.split('.')[0], entry.pk)
    if entry.enable_comments:
        return html
    else:
        return ""

@register.simple_tag
def ajax_comment_html():
    return """<a class="btn" href="#" id="show_comment_form" onclick="$('#comment_div').fadeIn(); $('#show_comment_form').hide(); return false;">Post Comment</a><br/>
<div id="comment_div">Cookies are required in order to leave comments(they are used for CSRF protection)</div>"""

They are both simple tags which return a simple text string. I could have done this using an inclusion_tag, but I thought it would have been overkill, as I am only returning very simple code. The first templatetag can be used with multiple apps with ease, as it detects the app's name from the model.

That is basically it, the next thing you need to do is load the above template library and use the tags in the appropriate places on your model's detail page.

Feb. 8, 2013, 2:54 p.m. - stefania

You should participate in a contest for probably the greatest blogs on the web. I will advocate this website!

About Me

My Photo
Names Kevin, hugely into UNIX technologies, not just Linux. I've dabbled with the demons, played with the Sun, and now with the Penguins.




Kevin Veroneau Consulting Services
Do you require the services of a Django contractor? Do you need both a website and hosting services? Perhaps I can help.

This Month

If you like what you read, please consider donating to help with hosting costs, and to fund future books to review.

Python Powered | © 2012-2013 Kevin Veroneau