Sunday, November 25, 2007

ReCAPTCHA and Django FreeComments

My little family blog has been discovered by spammers. They have started leaving comments on my Django powered blog. Not very often, but enough to be annoying. I heard about reCAPTCHA and thought it was a fabulous idea, so I decided that was how I wanted to up the ante for the spammers on my blog. Integrating reCAPTCHA with the Django freecomments wasn’t as easy as I thought it would be though, so I want to write down how I made it work in case it will help somebody else down the road.

First of all, I did my due diligence and googled for help. I didn’t find much, but I found an email, a snippet, and a blog that got me going in the right direction. While I’m citing references here, I also made use of the comments page on the Django wiki, and the request and response documentation too. The reCAPTCHA api docs were vital as well.

OK, after doing my reading googling I got to work. I registered for a set of reCAPTCHA keys, downloaded the recaptcha-client python library. I did the easy install thing to get it, but after doing just that my scripts still couldn’t import the module. I ended up just putting captcha.py in my Django project directory.

I use the freecomments in exactly the same way that the Django blog does (they supplied all the code for me, it was the easiest thing to do), so I’ll just talk in terms of that (I’m not ready to share my family blog with the world just yet). The Django blog has a comment form at the end of each entry (scroll all the way down to see it). Typing your comment here and clicking the Preview Comment button takes you to a preview page (go ahead and try it, you won’t be posting a comment at this point). It’s on the preview page where you find the final submit button. This is where I decided the captcha would go. Spambots can preview comments freely, but to submit, the captcha test has to be passed. I kind of wanted to require the captcha for even previewing, but that was going to be too difficult, and probably too annoying for legitimate commentators.

The way to make this work was to wrap the post_free_comment view with a view of my own in order to evaluate the captcha before allowing a comment to be posted. Here’s the whole thing:

def postfree_wrapper( request, extra_context = {} ):
    if not request.POST:
        raise Http404, _("Only POSTs are allowed")

    extra_context["recaptcha_error"] = False
    if 'preview' in request.POST:
        # no captcha test needed, but supply the captcha html
        extra_context["recaptcha_html"] = captcha.displayhtml(
            settings.RECAPTCHA_PUB_KEY )
        return post_free_comment(request, extra_context)

    if 'post' in request.POST:
        # test captcha before submitting comment

        try:
            recaptcha_challenge_field, recaptcha_response_field = \
                request.POST['recaptcha_challenge_field'], \
                request.POST['recaptcha_response_field']
        except keyError:
            raise Http404, _("No recaptcha fields submitted")
        check_captcha = captcha.submit(recaptcha_challenge_field,
                                       recaptcha_response_field,
                                       settings.RECAPTCHA_PRIV_KEY, 
                                       request.META['REMOTE_ADDR'])

        if check_captcha.is_valid is False:
            extra_context["recaptcha_error"] = True
            #extra_context["recaptcha_error_code"] = check_captcha.error_code
            extra_context["recaptcha_html"] = captcha.displayhtml(
                settings.RECAPTCHA_PUB_KEY, False, check_captcha.error_code )

            # modify the POST object so that it has a 'preview' key in
            # it.  This will cause the post_free_comment method to
            # *not* save the comment, instead it will redisplay the
            # comment preview.
            mutable_post = request.POST.copy()
            mutable_post['preview'] = True
            request.POST = mutable_post

        return post_free_comment(request, extra_context)

    raise Http404, _("No preview or post from comment form")

I figured out how to do most of this from looking at the actual post_free_comment view. To briefly explain, the view is used whether the comment is being previewed or posted. If it’s a preview, I generate the captcha html using captcha.py and add it to the response context, and forward everything on to the real post_free_comment view. It does all the real work, and my template will be able to display the captcha html. If it’s a post, I check the captcha using captcha.py. Then, if the captcha was valid I just forward everything on to the real post_free_comment view and it handles validation and posting of the comment. If the captcha was not valid, I add a recaptcha_error to the context for my template, generate new captcha html, and then do a little trick to get the real post_free_comment view to treat the request as a preview instead of a post. That was the real trick to all of this that I feel clever about.

To get this view called I had to modify my urls.py and add this ahead of the comments urls include:

 ( r'^comments/postfree/', 'postfree_wrapper' ),

Next I needed to modify my free_preview.html template to display the captcha and some helpful text above it. To do that, I only had to add this to my template:

{% if recaptcha_error %}
    <p>Oops!  Those must have been too hard to read.  Please try again.</p>
{% else %}
    <p>If your comment looks good, simply type the words in the box to
          prove you aren't an evil robot, and then click Post.</p>
{% endif %}
{{ recaptcha_html }}

I put this right above the “Post public comment” button of the form. You can see an entire free_preview.html template on the wiki (scroll down a ways). Mine looks an awful lot like that one, with only the above addition.

Lastly, add your reCAPTCHA keys to your settings.py, like so:

RECAPTCHA_PUB_KEY = ''thisisabiglongpublickeythatyougetfromrecapthca
RECAPTCHA_PRIV_KEY = 'andthisisabiglongprivatekeythatyoualsogetfromrecaptcha'

And that's it. I hope that helps some other poor soul who wants to add reCAPTCHA to their Django powered blog, but who doesn't have the spare time laying on a couch with their leg elevated after surgery to figure it all out like I did.

4 comments:

spr said...

Since you're actually using free_comments, you should have a look at comment_utils by James Bennett. It does some nice work with dealing with spam.

Bryan said...

Hey, that's pretty cool. I think I saw some of that on djangosnippets when I was searching for help. Looks like it's more for dealing with comments after they've already been posted though.

zgoda said...

This is one of possible approaches. Comment utils do great job on my blog (an on few sites I know of). I use Akismet as a base for automatic moderation, as provided in base comment utils package. It seems much less obtrusive for commenters than captchas.

Anonymous said...

Thanks! I'm converting my site from CodeIgniter to Django. Your info helped a lot.