├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── Makefile ├── README.rst ├── django_secureform ├── __init__.py ├── forms │ └── __init__.py ├── models.py ├── tests.py └── views.py ├── requirements.txt ├── settings.py ├── setup.py └── tests.py /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | env: 5 | - DJANGO=1.3 6 | - DJANGO=1.4 7 | - DJANGO=1.5 8 | - DJANGO=1.6 9 | - DJANGO=1.7 10 | - DJANGO=1.8 11 | - DJANGO=1.9 12 | install: 13 | - pip install --timeout=30 -q Django==$DJANGO 14 | - pip install --timeout=30 pep8 15 | - pip install --timeout=30 https://github.com/dcramer/pyflakes/tarball/master 16 | - pip install --timeout=30 -r requirements.txt 17 | - pip install --timeout=30 -q -e . 18 | before_script: 19 | - make verify 20 | script: 21 | - make test 22 | notifications: 23 | slack: smartfile:tbDIPzVJIPBpSz29kQw6b8RQ 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, SmartFile, Ben Timby 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | python tests.py 3 | 4 | verify: 5 | pyflakes django_secureform 6 | pep8 --exclude=migrations --ignore=E501,E225 django_secureform 7 | 8 | install: 9 | python setup.py install 10 | 11 | publish: 12 | python setup.py register 13 | python setup.py sdist upload 14 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | .. figure:: https://travis-ci.org/smartfile/django-secureform.png 2 | :alt: Travis CI Status 3 | :target: https://travis-ci.org/smartfile/django-secureform 4 | 5 | A `SmartFile`_ Open Source project. `Read more`_ about how SmartFile 6 | uses and contributes to Open Source software. 7 | 8 | .. figure:: http://www.smartfile.com/images/logo.jpg 9 | :alt: SmartFile 10 | 11 | Introduction 12 | ------------ 13 | 14 | Provides protection against spammers and scammers. 15 | 16 | Installation 17 | ------------ 18 | 19 | Install using pip: 20 | 21 | :: 22 | 23 | 24 | pip install django-secureform 25 | 26 | Then install the application into your Django project in settings.py. There are also optional settings 27 | which will affect the behavior of SecureForm instances. 28 | 29 | :: 30 | 31 | INSTALLED_APPS += ('django_secureform', ) 32 | 33 | # If you wish to use an encryption key other than Django's SECRET_KEY 34 | SECUREFORM_CRYPT_KEY = 'super-secret encryption key' 35 | 36 | # This is the name of the hidden field added to the form to contain 37 | # security data. 38 | SECUREFORM_FIELD_NAME = 'foobar' 39 | 40 | # The number of seconds allowed between form rendering and submittal. 41 | SECUREFORM_TTL = 300 42 | 43 | # The number of honeypot fields added to the form. 44 | SECUREFORM_HONEYPOTS = 1 45 | 46 | # By default, jQuery is needed to hide honeypots. If you already 47 | # use jQuery in your app, you can disable this feature (preventing 48 | # a duplicate script reference to jQuery). 49 | SECUREFORM_INCLUDE_JQUERY = False 50 | 51 | Usage 52 | ----- 53 | 54 | :: 55 | 56 | from django_secureform.forms import SecureForm 57 | 58 | 59 | # Define your form class as usual. 60 | class MySecureForm(SecureForm): 61 | class Meta: 62 | # Override options in settings.py for this class. 63 | include_jquery = False 64 | 65 | name = forms.CharField() 66 | 67 | 68 | Unit Testing 69 | ------------ 70 | 71 | If you want to write unit tests for forms that derive from SecureForm, you will 72 | need to let this application know you are testing. SecureForm looks for 73 | settings.TESTING to evaluate to True. If so, it disables the security allowing 74 | the Django test client to send POST data using the original field names. 75 | 76 | In the future, I would rather provide tools so that testing can happen with 77 | security enabled, but this is a quick workaround. Our test framework uses an 78 | environment variable to set settings.TESTING. For example, in settings.py... 79 | 80 | :: 81 | 82 | import os 83 | 84 | TESTING = True if 'TESTING' in os.environ else False 85 | 86 | .. _SmartFile: http://www.smartfile.com/ 87 | .. _Read more: http://www.smartfile.com/open-source.html 88 | -------------------------------------------------------------------------------- /django_secureform/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartfile/django-secureform/3b7a8b90550327f370ea02c6886220b2db0517b5/django_secureform/__init__.py -------------------------------------------------------------------------------- /django_secureform/forms/__init__.py: -------------------------------------------------------------------------------- 1 | import django 2 | import time 3 | import string 4 | 5 | from Crypto.Random import random 6 | from Crypto.Cipher import Blowfish 7 | 8 | from django import forms 9 | from django.conf import settings 10 | from django.core.cache import cache 11 | from django.forms import widgets 12 | 13 | from django.forms.forms import pretty_name 14 | from django.forms.forms import NON_FIELD_ERRORS 15 | from django.forms.forms import BoundField 16 | from django.forms.forms import DeclarativeFieldsMetaclass 17 | from django.utils.translation import ugettext as _ 18 | from django.utils.safestring import mark_safe 19 | 20 | if django.VERSION < (1, 7): 21 | from django.forms.util import ErrorDict 22 | else: 23 | from django.forms.utils import ErrorDict 24 | 25 | try: 26 | # Django <= 1.6 backwards compatibility 27 | from django.utils import simplejson as json 28 | except ImportError: 29 | # Django >= 1.7 30 | import json 31 | 32 | 33 | # Chars that are safe to use in field names. 34 | SAFE_CHARS = string.ascii_letters + string.digits 35 | 36 | JQUERY_TAG = '' 37 | SCRIPT_TAG = '''''' 43 | 44 | # Allow the user to specify an encryption key separate from the Django 45 | # SECRET_KEY if they wish. 46 | DEFAULT_CRYPT_KEY = getattr(settings, 'SECUREFORM_CRYPT_KEY', getattr(settings, 'SECRET_KEY', None)) 47 | # The user can override the secure field name. 48 | DEFAULT_FIELD_NAME = getattr(settings, 'SECUREFORM_FIELD_NAME', 'secure') 49 | # Give the user twenty minutes to fill out the form. 50 | DEFAULT_FORM_TTL = getattr(settings, 'SECUREFORM_TTL', 1200) 51 | # Inject random fields that are hidden from the user (and should be blank). 52 | DEFAULT_HONEYPOTS = getattr(settings, 'SECUREFORM_HONEYPOTS', 2) 53 | # Include jQuery, used to hide honeypots (probably could do the job without it). 54 | DEFAULT_INCLUDE_JQUERY = getattr(settings, 'SECUREFORM_INCLUDE_JQUERY', True) 55 | 56 | 57 | def random_name(choices=SAFE_CHARS, length=16): 58 | return ''.join(random.sample(choices, length)) 59 | 60 | 61 | def testing(): 62 | """ 63 | Detects if we are running under Django unit tests. If so, security is 64 | disabled. 65 | """ 66 | return getattr(settings, 'TESTING', False) 67 | 68 | 69 | class SecureFormException(Exception): 70 | 'Base exception for security faults.' 71 | 72 | 73 | class StaleFormException(SecureFormException): 74 | 'Raised if a form\'s timestamp is too old.' 75 | 76 | 77 | class ReplayedFormException(SecureFormException): 78 | 'Raised if a form\'s nonce value has been seen before.' 79 | 80 | 81 | class HoneypotFormException(SecureFormException): 82 | 'Raised if a honeypot field receives a value.' 83 | 84 | 85 | class InvalidFormException(SecureFormException): 86 | 'Raised if a fields do not map properly.' 87 | 88 | 89 | class HoneypotField(forms.CharField): 90 | 'Just a CharField that we can easily identify with isinstance().' 91 | def __init__(self, *args, **kwargs): 92 | kwargs.update({ 93 | 'required': False, 94 | 'max_length': 16, 95 | }) 96 | super(HoneypotField, self).__init__(*args, **kwargs) 97 | 98 | 99 | class InitialValueField(forms.CharField): 100 | '''A field that always assumes the initial value. Used for the "secure" field 101 | so that we can always control it's value.''' 102 | def bound_data(self, data, initial): 103 | return initial 104 | 105 | 106 | class SecureBoundField(BoundField): 107 | '''The "secure" flavor of the BoundField. Handles translations between 108 | secure names and rightful names.''' 109 | 110 | def _errors(self): 111 | '''Translates errors from field's secure name to the regular name (which is 112 | how errors are stored in the form.)''' 113 | name = self.form._secure_field_map.get(self.name) 114 | return self.form.errors.get(name, self.form.error_class()) 115 | errors = property(_errors) 116 | 117 | def _data(self): 118 | '''Get data using the secure field name. Ensures bound forms are not reset to 119 | blank values.''' 120 | name = self.form._secure_field_map.get(self.name) 121 | return self.field.widget.value_from_datadict(self.form.data, self.form.files, name) 122 | data = property(_data) 123 | 124 | def value(self): 125 | """ 126 | Returns the value for this BoundField, using the initial value if 127 | the form is not bound or the data otherwise. Takes care of secure name 128 | conversion. 129 | """ 130 | name = self.form._secure_field_map.get(self.name) 131 | initial = self.form.initial.get(name, self.field.initial) 132 | if not self.form.is_bound: 133 | data = initial 134 | if callable(data): 135 | data = data() 136 | else: 137 | data = self.field.bound_data( 138 | self.data, initial 139 | ) 140 | return self.field.prepare_value(data) 141 | 142 | 143 | class SecureFormOptions(object): 144 | 'Contains options for the SecureForm instance.' 145 | def __init__(self, options=None): 146 | self.secure_field_name = getattr(options, 'secure_field_name', DEFAULT_FIELD_NAME) 147 | self.form_ttl = getattr(options, 'form_ttl', DEFAULT_FORM_TTL) 148 | self.honeypots = getattr(options, 'honeypots', DEFAULT_HONEYPOTS) 149 | self.include_jquery = getattr(options, 'include_jquery', DEFAULT_INCLUDE_JQUERY) 150 | 151 | 152 | class SecureFormMetaclass(DeclarativeFieldsMetaclass): 153 | 'Metaclass to collect the options from the special Meta class.' 154 | def __new__(cls, name, bases, attrs): 155 | new_class = super(SecureFormMetaclass, cls).__new__(cls, name, bases, attrs) 156 | new_class._meta = SecureFormOptions(getattr(new_class, 'Meta', None)) 157 | return new_class 158 | 159 | 160 | class SecureFormBase(forms.Form): 161 | """This form is meant to defeat spam bots. It does this using a couple of techniques. 162 | 163 | 1. First of all, it will randomize the form field names. 164 | 2. It will add a hidden field which contains an encrypted map of random field names 165 | to actual field names. It will also contain a timestamp and nonce value to defeat 166 | replay attacks. 167 | 3. It will create two canary or honeypot fields that are expected to be left blank. 168 | 4. It will include a snippet of javascript that hides the two honeypot fields using CSS. 169 | 170 | This should be effective to block spammers of multiple varieties. 171 | 172 | 1. Spambots that simply replay the form submission will be foiled by replay protection. 173 | 2. Spambots that fetch then post the form back will be foiled by the honeypot fields 174 | unless they have a full javascript engine. 175 | 3. Humans on the other hand will still be able to submit the form, whether they are 176 | legitimate or not. However, this activity will be cost prohibitive for spammers. 177 | """ 178 | 179 | def __init__(self, *args, **kwargs): 180 | super(SecureFormBase, self).__init__(*args, **kwargs) 181 | # Use defaults, unless the caller overrode them. 182 | crypt_key = kwargs.pop('crypt_key', DEFAULT_CRYPT_KEY) 183 | self.crypt = Blowfish.new(crypt_key) 184 | self.fields[self._meta.secure_field_name] = InitialValueField(required=False, widget=widgets.HiddenInput) 185 | self.__secured = False 186 | self._secure_field_map = {} 187 | 188 | def __iter__(self): 189 | '''Iterates through the form fields, after ensuring that the security data, 190 | including all additional fields are in place.''' 191 | if not self.__secured: 192 | # Only secure on the first iteration. 193 | self.__secured = True 194 | self.secure_data() 195 | for name in self.fields: 196 | yield self[name] 197 | 198 | def __getitem__(self, name): 199 | 'Returns a SecureBoundField with the given name.' 200 | if not testing(): 201 | try: 202 | return SecureBoundField(self, self.fields[name], name) 203 | except KeyError: 204 | raise KeyError('Key %r not found in Form' % name) 205 | return super(SecureFormBase, self).__getitem__(name) 206 | 207 | def _script(self): 208 | '''Generates the JavaScript necessary for hiding the honeypots or an empty string 209 | if no honeypots are requested.''' 210 | if not self._meta.honeypots: 211 | return '' 212 | honeypots = [n for (n, f) in self.fields.items() if isinstance(f, HoneypotField)] 213 | func = random_name(choices=string.letters) 214 | name = random_name(choices=string.letters, length=2) 215 | obs = [] 216 | for honeypot in honeypots: 217 | orig = [c for c in honeypot] 218 | shuf = random.sample(orig, len(orig)) 219 | pmap = map(shuf.index, orig) 220 | obs.extend([ 221 | 'var %s = [\'%s\'];' % (name, '\', \''.join(shuf)), 222 | '%s(%s);' % (func, '+'.join(['%s[%s]' % (name, p) for p in pmap])), 223 | ]) 224 | scripts = [ 225 | SCRIPT_TAG % dict(function=func, obfuscated='\n'.join(obs)) 226 | ] 227 | if self._meta.include_jquery: 228 | scripts.insert(0, JQUERY_TAG) 229 | return mark_safe('\n'.join(scripts)) 230 | script = property(_script) 231 | 232 | def decode_data(self): 233 | '''The workhorse for validating inbound POST or GET data. It will verify the TTL and 234 | nonce. If those are valid, then the fields are converted back to their rightful names 235 | and while the honeypots are checked to ensure they are empty.''' 236 | if not self.is_bound: 237 | return 238 | if testing(): 239 | return 240 | cleaned_data = {} 241 | secure = self.data[self._meta.secure_field_name] 242 | secure = self.crypt.decrypt(secure.decode('hex')).rstrip() 243 | secure = json.loads(secure) 244 | timestamp = secure['t'] 245 | if timestamp < time.time() - self._meta.form_ttl: 246 | # Form data is too old, reject the form. 247 | raise StaleFormException(_('The form data is more than %s seconds old.') % 248 | self._meta.form_ttl) 249 | nonce = secure['n'] 250 | if cache.get(nonce) is not None: 251 | # Our nonce is in our cache, it has been seen, possible replay! 252 | raise ReplayedFormException(_('This form has already been submitted.')) 253 | # We only need to keep the nonce around for as long as the TTL (timeout). After 254 | # that, the timestamp check will refuse the form. That is the whole idea behind 255 | # the TTL/timeout, we can't guarantee the cache's availability long-term. 256 | cache.set(nonce, nonce, self._meta.form_ttl) 257 | self._secure_field_map = secure['f'] 258 | for sname, name in self._secure_field_map.items(): 259 | if name == self._meta.secure_field_name: 260 | cleaned_data[name] = self.data[name] 261 | continue 262 | if name is None: 263 | # This field is a honeypot. 264 | if self.data.get(sname): 265 | # Having a value in the honeypot field is bad news! 266 | raise HoneypotFormException(_('Unexpected value in form field.')) 267 | continue 268 | try: 269 | cleaned_data[name] = self.data[sname] 270 | except KeyError: 271 | # The field is missing from the data, that is OK, regular validation 272 | # will catch this if the field is required. 273 | pass 274 | self.data = cleaned_data 275 | 276 | def _clean_secure(self): 277 | '''Uses decode_data() to convert fields back to their rightful names. Turns exceptions 278 | into validation errors.''' 279 | try: 280 | self.decode_data() 281 | except SecureFormException, e: 282 | self._errors[NON_FIELD_ERRORS] = self.error_class([str(e)]) 283 | except Exception, e: 284 | self._errors[NON_FIELD_ERRORS] = self.error_class([_('Form verification failed. Please try again.')]) 285 | 286 | def full_clean(self): 287 | 'Does secureform validation, then regular validation.' 288 | self._errors = ErrorDict() 289 | self._clean_secure() 290 | if not self._errors: 291 | super(SecureFormBase, self).full_clean() 292 | 293 | def secure_data(self): 294 | 'Prepares the secure data before the form is rendered.' 295 | if testing(): 296 | return 297 | # Empty out the previous map, we will generate a new one. 298 | self._secure_field_map = {} 299 | labels = [] 300 | for name in self.fields.keys(): 301 | if name == self._meta.secure_field_name: 302 | continue 303 | sname = random_name() 304 | field = self.fields.pop(name) 305 | self._secure_field_map[sname] = name 306 | self.fields[sname] = field 307 | # Preserve the field name unless there is an explicit label: 308 | if not field.label: 309 | # Pretty-up the name, just like BoundField. 310 | field.label = pretty_name(name) 311 | # We keep a list of labels to use for our honeypots (if requested). 312 | if self._meta.honeypots: 313 | labels.append(field.label) 314 | # Add in some honeypots (if asked to). 315 | for i in range(1, self._meta.honeypots): 316 | sname = random_name() 317 | self._secure_field_map[sname] = None 318 | # Don't always put the honeypot fields at the end of the form. 319 | i = random.randint(0, len(self.fields) - 1) 320 | # Give the honeypot a label cloned from a legit field. 321 | if django.VERSION < (1, 7): 322 | self.fields.insert(i, sname, HoneypotField(label=random.choice(labels))) 323 | else: 324 | import collections 325 | fields = collections.OrderedDict() 326 | for index, (key, value) in enumerate(self.fields.items()): 327 | if index == i: 328 | fields.update({sname: HoneypotField(label=random.choice(labels))}) 329 | fields.update({key: value}) 330 | self.fields = fields 331 | secure = { 332 | # We preserve the time stamp, this lets us enforce the TTL. 333 | 't': time.time(), 334 | # The nonce is just a random value that we can remember to ensure no replays. 335 | 'n': random_name(), 336 | # And finally, the map of secure field names to rightful field names. 337 | 'f': self._secure_field_map, 338 | } 339 | secure = json.dumps(secure) 340 | # Pad to length divisible by 8. 341 | secure += ' ' * (8 - (len(secure) % 8)) 342 | secure = self.crypt.encrypt(secure) 343 | self.fields[self._meta.secure_field_name].initial = secure.encode('hex') 344 | 345 | 346 | class SecureForm(SecureFormBase): 347 | __metaclass__ = SecureFormMetaclass 348 | -------------------------------------------------------------------------------- /django_secureform/models.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/smartfile/django-secureform/3b7a8b90550327f370ea02c6886220b2db0517b5/django_secureform/models.py -------------------------------------------------------------------------------- /django_secureform/tests.py: -------------------------------------------------------------------------------- 1 | """ 2 | This file demonstrates writing tests using the unittest module. These will pass 3 | when you run "manage.py test". 4 | 5 | Replace this with more appropriate tests for your application. 6 | """ 7 | 8 | from django.test import TestCase 9 | 10 | 11 | class SimpleTest(TestCase): 12 | def test_basic_addition(self): 13 | """ 14 | Tests that 1 + 1 always equals 2. 15 | """ 16 | self.assertEqual(1 + 1, 2) 17 | -------------------------------------------------------------------------------- /django_secureform/views.py: -------------------------------------------------------------------------------- 1 | # Create your views here. 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pycrypto 2 | Django>=1.2.3 -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | SECUREFORM_CRYPT_KEY = 'test' 2 | SECRET_KEY = 'h6yvc20z5riu!xiy=mt!+^^(7+g3ua2pswb7omp(mte)wrc__#' 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/bin/env python 2 | 3 | import os 4 | from distutils.core import setup 5 | 6 | name = 'django-secureform' 7 | version = '0.3' 8 | release = '2' 9 | versrel = version + '-' + release 10 | readme = os.path.join(os.path.dirname(__file__), 'README.rst') 11 | long_description = file(readme).read() 12 | 13 | setup( 14 | name = name, 15 | version = versrel, 16 | description = 'Provides protection against spammers and scammers.', 17 | long_description = long_description, 18 | author = 'Ben Timby', 19 | author_email = 'btimby@gmail.com', 20 | maintainer = 'Ben Timby', 21 | maintainer_email = 'btimby@gmail.com', 22 | url = 'http://github.com/smartfile/' + name + '/', 23 | license = 'GPLv3', 24 | packages = [ 25 | "django_secureform", 26 | "django_secureform.forms", 27 | ], 28 | classifiers = ( 29 | 'Development Status :: 4 - Beta', 30 | 'Intended Audience :: Developers', 31 | 'Operating System :: OS Independent', 32 | 'Programming Language :: Python', 33 | 'Topic :: Software Development :: Libraries :: Python Modules', 34 | ), 35 | ) 36 | -------------------------------------------------------------------------------- /tests.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | os.environ['DJANGO_SETTINGS_MODULE'] = 'settings' 4 | 5 | import django 6 | if django.VERSION >= (1, 7): 7 | django.setup() 8 | 9 | from django import forms 10 | from django.db import models 11 | from django.forms.forms import NON_FIELD_ERRORS 12 | from django_secureform.forms import SecureForm 13 | 14 | 15 | def get_form_sname(form, name): 16 | for sname, v in form._secure_field_map.items(): 17 | if v and v == name: 18 | return sname 19 | raise KeyError(name) 20 | 21 | 22 | def get_form_honeypot(form): 23 | for sname, v in form._secure_field_map.items(): 24 | if v is None: 25 | return sname 26 | raise Exception('No honeypots found.') 27 | 28 | 29 | def get_form_secure_data(form): 30 | # We must copy over the security data. 31 | return form._meta.secure_field_name, form[form._meta.secure_field_name].value() 32 | 33 | 34 | class BasicForm(SecureForm): 35 | name = forms.CharField(required=True, max_length=16) 36 | 37 | 38 | class FormTestCase(unittest.TestCase): 39 | klass = BasicForm 40 | 41 | def setUp(self): 42 | self.form = self.klass() 43 | self.form.secure_data() 44 | 45 | def assertIn(self, value, iterable): 46 | self.assertTrue(value in iterable, '%s did not occur in %s' % (value, 47 | iterable)) 48 | 49 | def getForm(self, **kwargs): 50 | data = dict((get_form_secure_data(self.form), )) 51 | for n, v in kwargs.items(): 52 | data[get_form_sname(self.form, n)] = v 53 | return self.klass(data=data) 54 | 55 | 56 | class BasicTestCase(FormTestCase): 57 | def test_valid(self): 58 | post = self.getForm(name='foobar') 59 | self.assertTrue(post.is_valid()) 60 | 61 | def test_missing(self): 62 | post = self.getForm() 63 | self.assertFalse(post.is_valid()) 64 | self.assertIn('name', post._errors) 65 | 66 | def test_replay(self): 67 | post = self.getForm(name='foobar') 68 | post.is_valid() 69 | post = self.getForm(name='foobar') 70 | self.assertFalse(post.is_valid()) 71 | self.assertIn(NON_FIELD_ERRORS, post._errors) 72 | self.assertIn('This form has already been submitted.', post._errors[NON_FIELD_ERRORS]) 73 | 74 | def test_honeypot(self): 75 | honeypot = get_form_honeypot(self.form) 76 | data = dict((get_form_secure_data(self.form), )) 77 | data[honeypot] = 'mmm, hunny!' 78 | data[get_form_sname(self.form, 'name')] = 'foobar' 79 | post = self.klass(data=data) 80 | self.assertFalse(post.is_valid()) 81 | self.assertIn(NON_FIELD_ERRORS, post._errors) 82 | self.assertIn('Unexpected value in form field.', post._errors[NON_FIELD_ERRORS]) 83 | 84 | 85 | if __name__ == '__main__': 86 | unittest.main() 87 | --------------------------------------------------------------------------------