├── .gitignore ├── README.rst ├── example_project ├── __init__.py ├── app │ ├── __init__.py │ ├── forms.py │ ├── models.py │ ├── tests.py │ └── views.py ├── example.db ├── manage.py ├── settings.py ├── templates │ └── form.html └── urls.py ├── math_captcha ├── __init__.py ├── __init__.pyc ├── fields.py ├── fields.pyc ├── forms.py ├── forms.pyc ├── models.py ├── settings.py ├── tests.py ├── util.py ├── util.pyc └── views.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Simple Math Captcha 2 | ========================= 3 | 4 | :Authors: 5 | Justin Quick 6 | :Version: 0.1 7 | 8 | Django Math Captcha is an easy way to add mathematical captcha verification to your already existing forms. 9 | It asks you a simple math question (eg ``'1 + 2 ='``) and validates the form if your response is correct. 10 | All you have to do is subclass either ``MathCaptchaForm`` or ``MathCaptchaModelForm`` in your own forms. 11 | 12 | Extending your forms:: 13 | 14 | from math_captcha import MathCaptchaModelForm 15 | from myapp.forms import BlogForm 16 | 17 | class MyExistingForm(BlogForm,MathCaptchaModelForm): # instead of forms.ModelForm 18 | #... extra fields here 19 | 20 | 21 | Now you can be certain that the only users who create blogs are humans. 22 | 23 | Check out the example project for more practical use and tests. 24 | 25 | Using with other apps 26 | ---------------------- 27 | 28 | If you are running an app like, say `django-contact-form`_ and want to add math captcha here is how to go about it. 29 | 30 | Add the following in your ``urls.py``:: 31 | 32 | from contact_form.forms import ContactForm 33 | from math_captcha.forms import MathCaptchaForm 34 | 35 | class CaptchaContactForm(ContactForm,MathCaptchaForm): 36 | pass 37 | 38 | urlpatterns = patterns('', 39 | ... 40 | url(r'^contact/$','contact_form.views.contact_form',{'form_class':CaptchaContactForm},name='contact_form'), 41 | url(r'^contact/sent/$','django.views.generic.simple.direct_to_template',{ 'template': 'contact_form/contact_form_sent.html' },name='contact_form_sent'), 42 | ... 43 | ) 44 | 45 | Now the contact form will block robots who cant do math. 46 | 47 | .. _django-contact-form: http://bitbucket.org/ubernostrum/django-contact-form 48 | 49 | Settings 50 | --------- 51 | 52 | Set the behavior of the math captcha interaction in your ``settings.py`` 53 | 54 | ``MATH_CAPTCHA_NUMBERS = (1,2,3,4,5)`` 55 | 56 | A list of numbers to randomly choose from when generating the questions. 57 | Defaults to 1 through 5. 58 | 59 | ``MATH_CAPTCHA_OPERATORS = '+-'`` 60 | 61 | String containing mathematical operators to use. Default is only add (``+``) and subtract (``-``). 62 | Available operators are: add (``+``), subtract (``-``), multiply (``*``), divide (``/``), and modulo (``%``) 63 | 64 | ``MATH_CAPTCHA_QUESTION = 'Are you human?'`` 65 | 66 | Question that appears on forms as a label for math questions. By default it is ``'Are you human?'`` 67 | -------------------------------------------------------------------------------- /example_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justquick/django-math-captcha/4742f5fe8e88c8a2c97b4363ecda917313f724c0/example_project/__init__.py -------------------------------------------------------------------------------- /example_project/app/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justquick/django-math-captcha/4742f5fe8e88c8a2c97b4363ecda917313f724c0/example_project/app/__init__.py -------------------------------------------------------------------------------- /example_project/app/forms.py: -------------------------------------------------------------------------------- 1 | from django import forms 2 | from math_captcha.forms import MathCaptchaModelForm 3 | from models import Model 4 | 5 | class Form(MathCaptchaModelForm): 6 | class Meta: 7 | model = Model -------------------------------------------------------------------------------- /example_project/app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | class Model(models.Model): 4 | field = models.TextField(help_text='Im a text field',blank=True,null=True) -------------------------------------------------------------------------------- /example_project/app/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates two different styles of tests (one doctest and one 3 | unittest). These will both pass when you run "manage.py test". 4 | 5 | Replace these with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | class SimpleTest(TestCase): 11 | def test_basic_addition(self): 12 | """ 13 | Tests that 1 + 1 always equals 2. 14 | """ 15 | self.failUnlessEqual(1 + 1, 2) 16 | 17 | __test__ = {"doctest": """ 18 | Another way to test that 1 + 1 is equal to 2. 19 | 20 | >>> 1 + 1 == 2 21 | True 22 | """} 23 | 24 | -------------------------------------------------------------------------------- /example_project/app/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render_to_response 2 | from forms import Form 3 | 4 | def form(request): 5 | if request.method == 'POST': 6 | form = Form(request.POST) 7 | if form.is_valid(): 8 | return render_to_response('form.html',{'m':'woo hoo'}) 9 | else: 10 | form = Form() 11 | 12 | return render_to_response('form.html', { 13 | 'form': form, 14 | }) 15 | -------------------------------------------------------------------------------- /example_project/example.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justquick/django-math-captcha/4742f5fe8e88c8a2c97b4363ecda917313f724c0/example_project/example.db -------------------------------------------------------------------------------- /example_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from django.core.management import execute_manager 3 | try: 4 | import settings # Assumed to be in the same directory. 5 | except ImportError: 6 | import sys 7 | sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 | sys.exit(1) 9 | 10 | if __name__ == "__main__": 11 | execute_manager(settings) 12 | -------------------------------------------------------------------------------- /example_project/settings.py: -------------------------------------------------------------------------------- 1 | # Django settings for example_project project. 2 | import sys 3 | 4 | sys.path.insert(0, '..') 5 | 6 | DEBUG = True 7 | TEMPLATE_DEBUG = DEBUG 8 | 9 | ADMINS = ( 10 | # ('Your Name', 'your_email@domain.com'), 11 | ) 12 | 13 | MANAGERS = ADMINS 14 | 15 | DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'. 16 | DATABASE_NAME = 'example.db' # Or path to database file if using sqlite3. 17 | DATABASE_USER = '' # Not used with sqlite3. 18 | DATABASE_PASSWORD = '' # Not used with sqlite3. 19 | DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3. 20 | DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3. 21 | 22 | # Local time zone for this installation. Choices can be found here: 23 | # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name 24 | # although not all choices may be available on all operating systems. 25 | # If running in a Windows environment this must be set to the same as your 26 | # system time zone. 27 | TIME_ZONE = 'America/Chicago' 28 | 29 | # Language code for this installation. All choices can be found here: 30 | # http://www.i18nguy.com/unicode/language-identifiers.html 31 | LANGUAGE_CODE = 'en-us' 32 | 33 | SITE_ID = 1 34 | 35 | # If you set this to False, Django will make some optimizations so as not 36 | # to load the internationalization machinery. 37 | USE_I18N = False 38 | 39 | # Absolute path to the directory that holds media. 40 | # Example: "/home/media/media.lawrence.com/" 41 | MEDIA_ROOT = '' 42 | 43 | # URL that handles the media served from MEDIA_ROOT. Make sure to use a 44 | # trailing slash if there is a path component (optional in other cases). 45 | # Examples: "http://media.lawrence.com", "http://example.com/media/" 46 | MEDIA_URL = '' 47 | 48 | # URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a 49 | # trailing slash. 50 | # Examples: "http://foo.com/media/", "/media/". 51 | ADMIN_MEDIA_PREFIX = '/media/' 52 | 53 | # Make this unique, and don't share it with anybody. 54 | SECRET_KEY = 's(x!=*7wr&r((ah@35#@i-n4qqwz!a^61#e+d#&8686tz^c-k=' 55 | 56 | # List of callables that know how to import templates from various sources. 57 | TEMPLATE_LOADERS = ( 58 | 'django.template.loaders.filesystem.load_template_source', 59 | 'django.template.loaders.app_directories.load_template_source', 60 | # 'django.template.loaders.eggs.load_template_source', 61 | ) 62 | 63 | MIDDLEWARE_CLASSES = ( 64 | 'django.middleware.common.CommonMiddleware', 65 | 'django.contrib.sessions.middleware.SessionMiddleware', 66 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 67 | ) 68 | 69 | ROOT_URLCONF = 'urls' 70 | 71 | TEMPLATE_DIRS = ( 72 | 'templates', 73 | # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". 74 | # Always use forward slashes, even on Windows. 75 | # Don't forget to use absolute paths, not relative paths. 76 | ) 77 | 78 | INSTALLED_APPS = ( 79 | 'django.contrib.auth', 80 | 'django.contrib.contenttypes', 81 | 'django.contrib.sessions', 82 | 'django.contrib.sites', 83 | 'math_captcha', 84 | 'app', 85 | ) 86 | -------------------------------------------------------------------------------- /example_project/templates/form.html: -------------------------------------------------------------------------------- 1 |
{{ m }} 2 | {{ form.as_p }} 3 | 4 |
-------------------------------------------------------------------------------- /example_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls.defaults import * 2 | 3 | # Uncomment the next two lines to enable the admin: 4 | # from django.contrib import admin 5 | # admin.autodiscover() 6 | 7 | urlpatterns = patterns('', 8 | (r'', 'app.views.form'), 9 | ) 10 | -------------------------------------------------------------------------------- /math_captcha/__init__.py: -------------------------------------------------------------------------------- 1 | from forms import MathCaptchaForm, MathCaptchaModelForm -------------------------------------------------------------------------------- /math_captcha/__init__.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justquick/django-math-captcha/4742f5fe8e88c8a2c97b4363ecda917313f724c0/math_captcha/__init__.pyc -------------------------------------------------------------------------------- /math_captcha/fields.py: -------------------------------------------------------------------------------- 1 | from django.forms.fields import IntegerField 2 | from django.forms.widgets import TextInput 3 | from django.utils.safestring import mark_safe 4 | 5 | from util import question, encode 6 | 7 | 8 | class MathWidget(TextInput): 9 | """ 10 | Text input for a math captcha field. Stores hashed answer in hidden ``math_captcha_question`` field 11 | """ 12 | def render(self, name, value, attrs): 13 | aquestion = question() 14 | value = super(MathWidget, self).render(name, value, attrs) 15 | hidden = '' % encode(aquestion) 16 | return mark_safe(value.replace('

')) 9 | 10 | def test_bad_operation(self): 11 | form = MathCaptchaForm({'math_captcha_field':'0', 'math_captcha_question':encode(' - 1')}) 12 | self.assert_(form.as_p().find('errorlist') > -1) 13 | 14 | def test_success(self): 15 | form = MathCaptchaForm({'math_captcha_field':'0', 'math_captcha_question':encode('1 - 1')}) 16 | self.assert_(form.as_p().find('errorlist') == -1) 17 | 18 | def test_wrong_value(self): 19 | form = MathCaptchaForm({'math_captcha_field':'1', 'math_captcha_question':encode('1 - 1')}) 20 | self.assert_(form.as_p().find('errorlist') > -1) 21 | 22 | def test_negative_value(self): 23 | form = MathCaptchaForm({'math_captcha_field':'-1', 'math_captcha_question':encode('0 - 1')}) 24 | self.assert_(form.as_p().find('errorlist') > -1) 25 | 26 | -------------------------------------------------------------------------------- /math_captcha/util.py: -------------------------------------------------------------------------------- 1 | from hashlib import sha1 2 | from django.conf import settings as djsettings 3 | from random import choice 4 | from binascii import hexlify, unhexlify 5 | 6 | import settings 7 | 8 | 9 | def question(): 10 | n1, n2 = choice(settings.NUMBERS), choice(settings.NUMBERS) 11 | 12 | if n2 > n1: 13 | # avoid negative answers 14 | n1, n2 = n2, n1 15 | 16 | return "%s %s %s" % (n1, choice(settings.OPERATORS), n2) 17 | 18 | def encode(question): 19 | """ 20 | Given a mathematical question, eg '1 - 2 + 3', the question is hashed 21 | with the ``SECRET_KEY`` and the hex version of the question is appended. 22 | To the end user it looks like an extra long hex string, but it cryptographically ensures 23 | against any tampering. 24 | """ 25 | return sha1(djsettings.SECRET_KEY + question).hexdigest() + hexlify(question) 26 | 27 | def decode(answer): 28 | """ 29 | Returns the SHA1 answer key and the math question text. 30 | If the answer key passes, the question text is evaluated and compared to the user's answer. 31 | """ 32 | return answer[:40], unhexlify(answer[40:]) 33 | -------------------------------------------------------------------------------- /math_captcha/util.pyc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justquick/django-math-captcha/4742f5fe8e88c8a2c97b4363ecda917313f724c0/math_captcha/util.pyc -------------------------------------------------------------------------------- /math_captcha/views.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/justquick/django-math-captcha/4742f5fe8e88c8a2c97b4363ecda917313f724c0/math_captcha/views.py -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from distutils.core import setup 2 | 3 | 4 | setup(name='django-math-captcha', 5 | version='0.1', 6 | description='Simple, secure math captcha for django forms', 7 | long_description=open('README.rst').read(), 8 | author='Justin Quick', 9 | author_email='justquick@gmail.com', 10 | url='http://github.com/justquick/django-math-captcha', 11 | packages=['math_captcha'], 12 | classifiers=['Development Status :: 4 - Beta', 13 | 'Environment :: Web Environment', 14 | 'Intended Audience :: Developers', 15 | 'License :: OSI Approved :: BSD License', 16 | 'Operating System :: OS Independent', 17 | 'Programming Language :: Python', 18 | 'Topic :: Utilities'], 19 | ) 20 | --------------------------------------------------------------------------------