├── .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 |
--------------------------------------------------------------------------------