Accessible Captchas in Django

July 6th, 2008

While we were implementing the GNOME.Asia summit website (will be available on www.gnome.asia soon), we were confronted with the problem of captcha accessibility. Despite Recaptcha’s claim of being accessible by providing sound captchas, the sound quality turned out to be rather poor and I actually did not succeed in using it even after listening to it four times. Based on this solution implemented in Rails, I reimpleted a similar idea for question captchas in Django. The code is fairly simple, based on a model which contains questions and the corresponding answers, and an implementation of a Django newforms field which can be used in the form. Some example questions can be found here.

models.py

from django.db import models
from django.utils.translation import ugettext_lazy as _
 
class AccessibleCaptcha(models.Model):
    question = models.CharField( max_length=255)
    answer = models.CharField(max_length=255)
 
    question.verbose_name = _(u'Question')
    answer.verbose_name = _(u'Answer')
 
    def __unicode__(self):
        return self.question

fields.py

from random import randint
from django.newforms import *
from django.utils.translation import ugettext_lazy as _
from django.utils.safestring import mark_safe
from captcha.models import AccessibleCaptcha
 
class AccessibleCaptchaWidget(Widget):
 
    def __init__(self, options={}, *args, **kwargs):
        super(AccessibleCaptchaWidget, self).__init__(*args, **kwargs)
 
    def render(self, name, value, attrs=None):
        index = randint(0, AccessibleCaptcha.objects.count()-1)
        accessible_captcha = AccessibleCaptcha.objects.all()[index]
        question = accessible_captcha.question
        return mark_safe(u'''<strong>%(question)s</strong> 
<input name="%(name)s" value="%(index)d" type="hidden" />
<input name="%(name)s" id="id_%(name)s" type="text" />'''
                % {'name':'captcha', 'index':index, 'question':question})
 
    def value_from_datadict(self, data, name, prefix):
        return data.getlist(prefix)
 
class AccessibleCaptchaField(Field):
    def __init__(self, required=True, label=None, help_text=None,
            options={}, *args, **kwargs):
        super(AccessibleCaptchaField, self).__init__(required=required,
            widget=AccessibleCaptchaWidget(options=options),
            label=label, help_text=help_text, *args, **kwargs
            )
 
    def clean(self, value):
        errormsg = gettext(u'You did not enter the correct answer. Please try again.')
        if not isinstance(value, (list, tuple)):
            raise ValidationError(errormsg)
        #get the right answer from the database
        #value[0] contains the id of the hidden field, value[1] contains the user input
        index = int(value[0])
        answer = AccessibleCaptcha.objects.all()[index].answer
        if not value[1].lower() ==  answer.lower():
            raise ValidationError(errormsg)
        return True

Usage

captcha = AccessibleCaptchaField(label=_(u'Are you human?'))

Leave a Reply