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.`