Login

Math Captcha Field and Widget

Author:
btaylordesign
Posted:
September 8, 2011
Language:
Python
Version:
1.3
Score:
1 (after 1 ratings)

This math captcha field and widget was inspired by Justin Quick's django-math-captcha, but I wanted to make it into one form field and not have anything in settings.py. I removed the division and modulo operators to avoid people having to input fractions, and just randomly select the operator.

It leverages Django's built-in MultiValueField to set up the hidden hashed answer to compare the user's input to instead of rendering strings for the fields like django-math-captcha does.

Unit tests soon to follow, but this is used in production at: http://btaylorweb.com/. Enjoy!

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
from binascii import hexlify, unhexlify
from random import randint, choice

from django.conf import settings
from django.core.exceptions import ValidationError
from django import forms
from django.forms.fields import MultiValueField, IntegerField, CharField
from django.template.defaultfilters import mark_safe
from django.utils.hashcompat import sha_constructor


class MathCaptchaWidget(forms.MultiWidget):
    def __init__(self, attrs=None):
        self.attrs = attrs or {}
        widgets = (
            forms.TextInput(attrs={'size' : '5'}), #this is the answer input field
            forms.HiddenInput() #this is the hashed answer field to compare to
        )
        super(MathCaptchaWidget, self).__init__(widgets, attrs)

    def decompress(self, value):
        if value:
            """
            Split the initial value set by the field that implements
            this field and return the double. These values get bound
            to the fields.
            """
            question, hashed_answer = value.split('|')
            return [question, hashed_answer]
        return [None, None]

    def format_output(self, rendered_widgets):
        return u'%s%s' % (rendered_widgets[0], rendered_widgets[1])


class MathCaptchaField(MultiValueField):
    widget = MathCaptchaWidget

    def __init__(self, start_int=0, end_int=10, *args, **kwargs):
        #set up error messages
        errors = self.default_error_messages.copy()
        if 'error_messages' in kwargs:
            errors.update(kwargs['error_messages'])
        localize = kwargs.get('localize', False)

        #set integers for question
        x = randint(start_int, end_int)
        y = randint(start_int, end_int)

        #avoid negatives
        if y > x:
            x, y = y, x

        #set up question
        operator = choice('+,-,*'.split(','))
        question = '%i %s %i' % (x, operator, y)

        #make multiplication operator more human-readable
        operator_for_label = '×' if operator == '*' else operator

        #set label for field
        kwargs['label'] = mark_safe('What is %i %s %i' % (x, operator_for_label, y))

        #hash answer and set initial value of form
        hashed_answer = sha_constructor(settings.SECRET_KEY + \
            question).hexdigest() + hexlify(question)
        kwargs['initial'] = '%s|%s' % ('', hashed_answer)

        #set fields
        fields = (
            IntegerField(min_value=0, localize=localize),
            CharField(max_length=255)
        )
        super(MathCaptchaField, self).__init__(fields, *args, **kwargs)

    def compress(self, data_list):
        """Compress takes the place of clean with MultiValueFields"""
        if data_list:
            answer = data_list[0]
            #unhash and eval question. Compare to answer.
            unhashed_answer = eval(unhexlify(data_list[1][40:]))
            if answer != unhashed_answer:
                raise ValidationError(u'Please check your math and try again.')
            return answer
        return None


###
Example usage:

from django import forms
from math_captcha_field import MathCaptchaField

class ContactForm(forms.Form):
    name = forms.CharField(max_length=75)
    #...
    captcha = MathCaptchaField(required=True)

`Optionally, MathCaptchaField has parameters for
the starting and ending integers for the range of
random numbers to select from. Defaults are:
start_int=10, end_int=10.`

More like this

  1. Template tag - list punctuation for a list of items by shapiromatron 9 months, 1 week ago
  2. JSONRequestMiddleware adds a .json() method to your HttpRequests by cdcarter 9 months, 2 weeks ago
  3. Serializer factory with Django Rest Framework by julio 1 year, 4 months ago
  4. Image compression before saving the new model / work with JPG, PNG by Schleidens 1 year, 4 months ago
  5. Help text hyperlinks by sa2812 1 year, 5 months ago

Comments

Please login first before commenting.