Permalink
Browse files

Started to work on contact node owners form. Added math-captcha (http…

  • Loading branch information...
1 parent 4d7ebad commit 69a57f6599b077fe006cc92d2a284ec1566787e1 nemesisdesign committed Aug 1, 2011
View
@@ -0,0 +1,62 @@
+Metadata-Version: 1.0
+Name: django-math-captcha
+Version: 0.1
+Summary: Simple, secure math captcha for django forms
+Home-page: http://github.com/justquick/django-math-captcha
+Author: Justin Quick
+Author-email: justquick@gmail.com
+License: UNKNOWN
+Description: Simple Math Captcha
+ =========================
+
+ :Authors:
+ Justin Quick <justquick@gmail.com>
+ :Version: 0.1
+
+ Django Math Captcha is an easy way to add mathematical captcha verification to your already existing forms.
+ It asks you a simple math question (eg ``'1 + 2 ='``) and validates the form if your response is correct.
+ All you have to do is subclass either ``MathCaptchaForm`` or ``MathCaptchaModelForm`` in your own forms.
+
+ Use it in your forms::
+
+ from math_captcha import MathCaptchaModelForm
+ from myapp.models import Blog
+
+ class MyExistingForm(MathCaptchaModelForm): # instead of forms.ModelForm
+ #... extra fields here
+
+ class Meta:
+ model = Blog
+
+
+ Now you can be certain that the only users who create blogs are humans
+
+ Check out the example project for more practical use and tests.
+
+ Settings
+ ---------
+
+ Set the behavior of the math captcha interaction in your settings.py
+
+ ``MATH_CAPTCHA_NUMBERS``
+
+ A list of numbers to randomly choose from when generating the questions.
+ Defaults to ``[1,2,3,4,5]``.
+
+ ``MATH_CAPTCHA_OPERATORS``
+
+ List of mathematical operators to use. Default is only add (``+``) and subtract (``-``).
+ Available operators are: add (``+``), subtract (``-``), multiply (``*``), divide (``/``), and modulo (``%``)
+
+ ``MATH_CAPTCHA_QUESTION``
+
+ Question that appears on forms as a label for math questions. By default it is ``'Are you human?'``
+
+Platform: UNKNOWN
+Classifier: Development Status :: 4 - Beta
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: BSD License
+Classifier: Operating System :: OS Independent
+Classifier: Programming Language :: Python
+Classifier: Topic :: Utilities
View
@@ -0,0 +1 @@
+from forms import MathCaptchaForm, MathCaptchaModelForm
View
@@ -0,0 +1,20 @@
+from django.forms.fields import IntegerField
+from django.forms.widgets import TextInput
+from util import question, encode
+
+
+class MathWidget(TextInput):
+ """
+ Text input for a math captcha field. Stores hashed answer in hidden ``math_captcha_question`` field
+ """
+ def render(self, name, value, attrs):
+ aquestion = question()
+ value = super(MathWidget, self).render(name, value, attrs)
+ hidden = '<input type="hidden" value="%s" name="math_captcha_question"/>' % encode(aquestion)
+ return value.replace('<input', '%s %s = <input' % (hidden, aquestion))
+
+class MathField(IntegerField):
+ widget = MathWidget()
+
+ def __init__(self, *a, **kw):
+ super(MathField, self).__init__(None, 0, *a, **kw)
View
@@ -0,0 +1,70 @@
+from django import forms
+from fields import MathField
+from util import encode, decode
+import settings
+
+
+class NullWidget(forms.widgets.HiddenInput):
+ def render(self, *a, **kw):
+ return ''
+
+def math_clean(form):
+ """
+ Cleans a form, validating answer to math question in the process.
+ The given ``form`` must be an instance of either ``MathCaptchaModelForm`` or ``MathCaptchaForm``.
+ Answer keys are communicated in the ``math_captcha_question`` field which is evaluated to give the correct answer
+ after being validated against the ``SECRET_KEY``
+ """
+ try:
+ value = form.cleaned_data['math_captcha_field']
+ test_secret, question = decode(form.cleaned_data['math_captcha_question'])
+ assert len(test_secret) == 40 and question
+ except (TypeError, AssertionError):
+ # problem decoding, junky data
+ raise forms.ValidationError('Invalid token')
+ except KeyError:
+ return
+
+ if encode(question) != form.cleaned_data['math_captcha_question']:
+ # security problem, hack attempt
+ raise forms.ValidationError('Invalid token')
+ if eval(question) != value:
+ raise forms.ValidationError('Wrong answer, try again')
+
+
+class MathCaptchaModelForm(forms.ModelForm):
+ """
+ Subclass of ``django.forms.ModelForm`` which contains the math fields
+ Inherit this class in your forms to add math captcha support::
+
+ class MyForm(MathCaptchaModelForm):
+ field1 = forms.fields.CharField()
+ # ...
+
+ class Meta:
+ model = MyModel
+
+ """
+ math_captcha_field = MathField(label=settings.QUESTION)
+ math_captcha_question = forms.fields.CharField(widget=NullWidget())
+
+ def clean(self):
+ super(MathCaptchaModelForm, self).clean()
+ math_clean(self)
+
+class MathCaptchaForm(forms.Form):
+ """
+ Subclass of ``django.forms.Form`` which contains the math fields.
+ Inherit this class in your forms to add math captcha support::
+
+ class MyForm(MathCaptchaForm):
+ field1 = forms.fields.CharField()
+ # ...
+
+ """
+ math_captcha_field = MathField(label=settings.QUESTION)
+ math_captcha_question = forms.fields.CharField(widget=NullWidget())
+
+ def clean(self):
+ super(MathCaptchaForm, self).clean()
+ math_clean(self)
View
No changes.
View
@@ -0,0 +1,25 @@
+from django.test.testcases import TestCase
+from forms import MathCaptchaForm
+from util import encode
+
+class MathCaptchaTest(TestCase):
+ def test_unbound(self):
+ form = MathCaptchaForm()
+ self.assert_(form.as_p().endswith('<input type="text" name="math_captcha_field" id="id_math_captcha_field" /></p>'))
+
+ def test_bad_operation(self):
+ form = MathCaptchaForm({'math_captcha_field':'0', 'math_captcha_question':encode(' - 1')})
+ self.assert_(form.as_p().find('errorlist') > -1)
+
+ def test_success(self):
+ form = MathCaptchaForm({'math_captcha_field':'0', 'math_captcha_question':encode('1 - 1')})
+ self.assert_(form.as_p().find('errorlist') == -1)
+
+ def test_wrong_value(self):
+ form = MathCaptchaForm({'math_captcha_field':'1', 'math_captcha_question':encode('1 - 1')})
+ self.assert_(form.as_p().find('errorlist') > -1)
+
+ def test_negative_value(self):
+ form = MathCaptchaForm({'math_captcha_field':'-1', 'math_captcha_question':encode('0 - 1')})
+ self.assert_(form.as_p().find('errorlist') > -1)
+
View
@@ -0,0 +1,31 @@
+from django.utils.hashcompat import sha_constructor
+from django.conf import settings as djsettings
+from random import choice
+from binascii import hexlify, unhexlify
+import settings
+
+
+def question():
+ n1, n2 = choice(settings.NUMBERS), choice(settings.NUMBERS)
+
+ if n2 > n1:
+ # avoid negative answers
+ n1, n2 = n2, n1
+
+ return "%s %s %s %s ?" % (settings.QUESTION, n1, choice(settings.OPERATORS), n2)
+
+def encode(question):
+ """
+ Given a mathematical question, eg '1 - 2 + 3', the question is hashed
+ with the ``SECRET_KEY`` and the hex version of the question is appended.
+ To the end user it looks like an extra long hex string, but it cryptographically ensures
+ against any tampering.
+ """
+ return sha_constructor(djsettings.SECRET_KEY + question).hexdigest() + hexlify(question)
+
+def decode(answer):
+ """
+ Returns the SHA1 answer key and the math question text.
+ If the answer key passes, the question text is evaluated and compared to the user's answer.
+ """
+ return answer[:40], unhexlify(answer[40:])
View
No changes.
View
@@ -202,4 +202,32 @@ def save(self, commit=True):
self.node.set_password()
if commit:
self.node.save()
- return self.node
+ return self.node
+
+from math_captcha import MathCaptchaForm
+
+class ContactForm(MathCaptchaForm):
+ """
+ A form used to contact node owners
+ """
+
+ name = forms.CharField(max_length=50, min_length=4, widget=forms.TextInput)
+ email = forms.EmailField(max_length=500, min_length=8, widget=forms.TextInput)
+ text = forms.CharField(max_length=2000, widget=forms.Textarea)
+
+ def __init__(self, *args, **kwargs):
+ super(ContactForm, self).__init__(*args, **kwargs)
+ # css classes for fields
+ for v in self.fields:
+ self.fields[v].widget.attrs['class'] = 'text ui-widget-content ui-corner-all'
+
+ def clean(self):
+ ''' Strip values '''
+ super(ContactForm, self).clean()
+
+ # strip() values
+ for field in self.cleaned_data:
+ if isinstance(self.cleaned_data[field], basestring):
+ self.cleaned_data[field] = self.cleaned_data[field].strip()
+
+ return self.cleaned_data
@@ -0,0 +1,73 @@
+<div class="dialog-form ui-widget" >
+ <form id="node-form" class="ui-corner-all ui-widget-content" method="post" action="">
+ <h1>Contatta il nodo {{ node.name }}</h1>
+ <p>I campi contrassegnati con un asterisco sono obbligatori.</p>
+ <fieldset>
+ {% csrf_token %}
+ {% if form.non_field_errors %}
+ <div class="ui-state-error">
+ {{ form.non_field_errors }}
+ </div><br />
+ {% endif %}
+
+ <div class="fieldWrapper">
+ <label for="id_name">Tuo nome: <big>*</big><label>
+ {% if form.name.errors %}
+ <div class="ui-state-error">
+ {{ form.name.errors }}
+ </div>
+ {% endif %}
+ {{ form.name }}
+ </div>
+
+ <div class="fieldWrapper">
+ <label for="id_email">Email: <big>*</big><label>
+ {% if form.email.errors %}
+ <div class="ui-state-error">
+ {{ form.email.errors }}
+ </div>
+ {% endif %}
+ {{ form.email }}
+ </div>
+
+ <div class="fieldWrapper">
+ <label for="id_text">Testo: <big>*</big><label>
+ {% if form.text.errors %}
+ <div class="ui-state-error">
+ {{ form.text.errors }}
+ </div>
+ {% endif %}
+ {{ form.text }}
+ </div>
+
+ <div class="fieldWrapper">
+ {% if form.math_captcha_field.errors %}
+ <div class="ui-state-error">
+ {{ form.math_captcha_field.errors }}
+ </div>
+ {% endif %}
+ {% autoescape off %}{{ form.math_captcha_field }}{% endautoescape %}
+ </div>
+
+ <p style="overflow:hidden">
+ <input id="contact-form-submit" class="submit-button ui-priority-primary ui-corner-all ui-state-disabled hover" type="submit" value="Invia" />
+ <input id="contact-form-cancel" class="cancel-button ui-priority-primary ui-corner-all ui-state-disabled hover" type="button" value="Annulla" />
+ </p>
+ </fieldset>
+ </form>
+</div>
+
+<script>
+ $('#contact-form-submit, #contact-form-cancel').button();
+ var inputs = $("#node-form").find("input , select ,textarea");
+ $.each(inputs,function(){
+ $(this).bind({
+ focusin: function() {
+ $(this).toggleClass('ui-state-focus');
+ },
+ focusout: function() {
+ $(this).toggleClass('ui-state-focus');
+ }
+ });
+ });
+</script>
@@ -0,0 +1,5 @@
+{% extends 'overlay.html' %}
+{% block title %}Contatta nodo {{ node.name }} - Nodeshot{% endblock %}
+{% block content %}
+ {% include 'ajax/contact.html' %}
+{% endblock %}
View
@@ -16,4 +16,5 @@
url(r'^confirm/(?P<node_id>\d+)/(?P<activation_key>\w+)/$', 'nodeshot.views.confirm_node', name='nodeshot_confirm_node'),
url(r'^report_abuse/(?P<node_id>\d+)/(?P<email>.*)/$', 'nodeshot.views.report_abuse', name='nodeshot_report_abuse'),
url(r'^purge_expired/$', 'nodeshot.views.purge_expired', name='nodeshot_purge_expired'),
+ url(r'^contact/(?P<node_id>\d+)/$', 'nodeshot.views.contact', name='nodeshot_contact'),
)
Oops, something went wrong.

0 comments on commit 69a57f6

Please sign in to comment.