[Python-modules-commits] [django-simple-captcha] 01/05: New upstream version 0.5.5

Brian May bam at moszumanska.debian.org
Mon Jul 3 22:34:21 UTC 2017


This is an automated email from the git hooks/post-receive script.

bam pushed a commit to branch debian/master
in repository django-simple-captcha.

commit d57d986464c1042b3ee1cd38bddd8e240ebf837f
Author: Brian May <bam at debian.org>
Date:   Tue Jul 4 08:26:04 2017 +1000

    New upstream version 0.5.5
---
 CHANGES                                            |  23 +++++++
 PKG-INFO                                           |   2 +-
 README.rst                                         |   2 +-
 captcha/__init__.py                                |   4 +-
 captcha/conf/settings.py                           |   9 ++-
 captcha/fields.py                                  |  37 ++++++-----
 captcha/helpers.py                                 |   4 +-
 captcha/locale/pl/LC_MESSAGES/django.mo            | Bin 720 -> 721 bytes
 captcha/locale/pl/LC_MESSAGES/django.po            |   2 +-
 captcha/locale/zh_Hans/LC_MESSAGES/django.mo       | Bin 0 -> 643 bytes
 captcha/locale/zh_Hans/LC_MESSAGES/django.po       |  32 ++++++++++
 captcha/management/commands/captcha_clean.py       |   4 +-
 captcha/management/commands/captcha_create_pool.py |  27 ++++++++
 captcha/models.py                                  |  43 ++++++++-----
 captcha/templates/captcha/text_field.html          |   2 +-
 captcha/tests/tests.py                             |  68 +++++++++++++++++++--
 captcha/tests/urls.py                              |   3 +-
 captcha/tests/views.py                             |  13 ++++
 captcha/views.py                                   |   4 +-
 django_simple_captcha.egg-info/PKG-INFO            |   2 +-
 django_simple_captcha.egg-info/SOURCES.txt         |   3 +
 docs/advanced.rst                                  |  24 ++++++++
 docs/conf.py                                       |   4 +-
 docs/index.rst                                     |   2 +-
 docs/usage.rst                                     |   1 +
 testproject/settings.py                            |   3 -
 testproject/urls.py                                |  13 ++--
 testproject/views.py                               |   7 +--
 tox.ini                                            |  13 ++--
 29 files changed, 278 insertions(+), 73 deletions(-)

diff --git a/CHANGES b/CHANGES
index b6335ca..2ce446d 100644
--- a/CHANGES
+++ b/CHANGES
@@ -1,3 +1,26 @@
+Version 0.5.5
+-------------
+* I messed the 0.5.4 release, re-releasing as 0.5.5
+
+Version 0.5.4
+-------------
+* Removed a couple gremlins (PR #113, thanks @Pawamoy)
+* Added autocapitalize="off", autocorrect="off" and spellcheck="false" to the genreated field (PR #116, thanks @rdonnelly)
+* Test against Django 1.11
+* Drop support of Django 1.7 ("it'll probably still work")
+
+Version 0.5.3
+-------------
+* Ability to pass a per-field challenge generator function (Fixes #109)
+* Added a feature to get captchas from a data pool of pre-created captchas (PR #110, thanks @skozan)
+* Cleanup to remove old code handling timezones for no longer supported Django versions
+* Fix for "Size must be a tuple" issue with Pillow 3.4.0 (Fixes #111)
+
+Version 0.5.2
+-------------
+* Use any mutliplication uperator instead of "*". (Fixes #77 via PR #104, thanks @honsdomi and @isergey)
+* Test against Django 1.10
+
 Version 0.5.1
 -------------
 * Fine tuning MANIFEST.in
diff --git a/PKG-INFO b/PKG-INFO
index 50dc3e5..48fb03c 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: django-simple-captcha
-Version: 0.5.1
+Version: 0.5.5
 Summary: A very simple, yet powerful, Django captcha application
 Home-page: https://github.com/mbi/django-simple-captcha
 Author: Marco Bonetti
diff --git a/README.rst b/README.rst
index 3691552..03a74c8 100644
--- a/README.rst
+++ b/README.rst
@@ -8,7 +8,7 @@ Django Simple Captcha
 
 Django Simple Captcha is an extremely simple, yet highly customizable Django application to add captcha images to any Django form.
 
-.. image:: http://django-simple-captcha.googlecode.com/files/Captcha3.png
+.. image:: http://django-simple-captcha.readthedocs.io/en/latest/_images/captcha3.png
 
 Features
 ++++++++
diff --git a/captcha/__init__.py b/captcha/__init__.py
index 7c4a195..e0acc65 100644
--- a/captcha/__init__.py
+++ b/captcha/__init__.py
@@ -1,10 +1,10 @@
 import re
 
-VERSION = (0, 5, 1)
+VERSION = (0, 5, 5)
 
 
 def get_version(svn=False):
-    "Returns the version as a human-format string."
+    "Return the version as a human-format string."
     return '.'.join([str(i) for i in VERSION])
 
 
diff --git a/captcha/conf/settings.py b/captcha/conf/settings.py
index 569372a..c8ab588 100644
--- a/captcha/conf/settings.py
+++ b/captcha/conf/settings.py
@@ -1,4 +1,4 @@
-import os
+import os
 from django.conf import settings
 
 CAPTCHA_FONT_PATH = getattr(settings, 'CAPTCHA_FONT_PATH', os.path.normpath(os.path.join(os.path.dirname(__file__), '..', 'fonts/Vera.ttf')))
@@ -23,6 +23,9 @@ CAPTCHA_HIDDEN_FIELD_TEMPLATE = getattr(settings, 'CAPTCHA_HIDDEN_FIELD_TEMPLATE
 CAPTCHA_TEXT_FIELD_TEMPLATE = getattr(settings, 'CAPTCHA_TEXT_FIELD_TEMPLATE', 'captcha/text_field.html')
 CAPTCHA_FIELD_TEMPLATE = getattr(settings, 'CAPTCHA_FIELD_TEMPLATE', 'captcha/field.html')
 CAPTCHA_OUTPUT_FORMAT = getattr(settings, 'CAPTCHA_OUTPUT_FORMAT', None)
+CAPTCHA_MATH_CHALLENGE_OPERATOR = getattr(settings, 'CAPTCHA_MATH_CHALLENGE_OPERATOR', '*')
+CAPTCHA_GET_FROM_POOL = getattr(settings, 'CAPTCHA_GET_FROM_POOL', False)
+CAPTCHA_GET_FROM_POOL_TIMEOUT = getattr(settings, 'CAPTCHA_GET_FROM_POOL_TIMEOUT', 5)
 
 CAPTCHA_TEST_MODE = getattr(settings, 'CAPTCHA_TEST_MODE', getattr(settings, 'CATPCHA_TEST_MODE', False))
 
@@ -38,8 +41,8 @@ def _callable_from_string(string_or_callable):
         return getattr(__import__('.'.join(string_or_callable.split('.')[:-1]), {}, {}, ['']), string_or_callable.split('.')[-1])
 
 
-def get_challenge():
-    return _callable_from_string(CAPTCHA_CHALLENGE_FUNCT)
+def get_challenge(generator=None):
+    return _callable_from_string(generator or CAPTCHA_CHALLENGE_FUNCT)
 
 
 def noise_functions():
diff --git a/captcha/fields.py b/captcha/fields.py
index 47f253d..f442a2e 100644
--- a/captcha/fields.py
+++ b/captcha/fields.py
@@ -1,11 +1,12 @@
-from captcha.conf import settings
-from captcha.models import CaptchaStore, get_safe_now
+from captcha.conf import settings
+from captcha.models import CaptchaStore
 from django.core.exceptions import ImproperlyConfigured
 from django.core.urlresolvers import reverse, NoReverseMatch
 from django.forms import ValidationError
 from django.forms.fields import CharField, MultiValueField
 from django.forms.widgets import TextInput, MultiWidget, HiddenInput
 from django.utils.translation import ugettext_lazy
+from django.utils import timezone
 from django.template.loader import render_to_string
 from django.utils.safestring import mark_safe
 from six import u
@@ -27,7 +28,7 @@ class BaseCaptchaTextInput(MultiWidget):
             return value.split(',')
         return [None, None]
 
-    def fetch_captcha_store(self, name, value, attrs=None):
+    def fetch_captcha_store(self, name, value, attrs=None, generator=None):
         """
         Fetches a new CaptchaStore
         This has to be called inside render
@@ -37,7 +38,10 @@ class BaseCaptchaTextInput(MultiWidget):
         except NoReverseMatch:
             raise ImproperlyConfigured('Make sure you\'ve included captcha.urls as explained in the INSTALLATION section on http://readthedocs.org/docs/django-simple-captcha/en/latest/usage.html#installation')
 
-        key = CaptchaStore.generate_key()
+        if settings.CAPTCHA_GET_FROM_POOL:
+            key = CaptchaStore.pick()
+        else:
+            key = CaptchaStore.generate_key(generator)
 
         # these can be used by format_output and render
         self._value = [key, u('')]
@@ -64,7 +68,7 @@ class CaptchaTextInput(BaseCaptchaTextInput):
         self._args = kwargs
         self._args['output_format'] = self._args.get('output_format') or settings.CAPTCHA_OUTPUT_FORMAT
         self._args['field_template'] = self._args.get('field_template') or settings.CAPTCHA_FIELD_TEMPLATE
-        self._args['id_prefix'] = self._args.get('id_prefix')
+        # self._args['id_prefix'] = self._args.get('id_prefix')
 
         if self._args['output_format'] is None and self._args['field_template'] is None:
             raise ImproperlyConfigured('You MUST define either CAPTCHA_FIELD_TEMPLATE or CAPTCHA_OUTPUT_FORMAT setting. Please refer to http://readthedocs.org/docs/django-simple-captcha/en/latest/usage.html#installation')
@@ -79,8 +83,8 @@ class CaptchaTextInput(BaseCaptchaTextInput):
 
         super(CaptchaTextInput, self).__init__(attrs)
 
-    def build_attrs(self, extra_attrs=None, **kwargs):
-        ret = super(CaptchaTextInput, self).build_attrs(extra_attrs, **kwargs)
+    def build_attrs(self, *args, **kwargs):
+        ret = super(CaptchaTextInput, self).build_attrs(*args, **kwargs)
         if self._args.get('id_prefix') and 'id' in ret:
             ret['id'] = '%s_%s' % (self._args.get('id_prefix'), ret['id'])
         return ret
@@ -92,14 +96,14 @@ class CaptchaTextInput(BaseCaptchaTextInput):
         return ret
 
     def format_output(self, rendered_widgets):
-        hidden_field, text_field = rendered_widgets
-
+        # hidden_field, text_field = rendered_widgets
         if self._args['output_format']:
-            return self._args['output_format'] % {
+            ret = self._args['output_format'] % {
                 'image': self.image_and_audio,
                 'hidden_field': self.hidden_field,
                 'text_field': self.text_field
             }
+            return ret
 
         elif self._args['field_template']:
             context = {
@@ -110,7 +114,7 @@ class CaptchaTextInput(BaseCaptchaTextInput):
             return render_to_string(settings.CAPTCHA_FIELD_TEMPLATE, context)
 
     def render(self, name, value, attrs=None):
-        self.fetch_captcha_store(name, value, attrs)
+        self.fetch_captcha_store(name, value, attrs, self._args.get('generator'))
 
         context = {
             'image': self.image_url(),
@@ -127,6 +131,9 @@ class CaptchaTextInput(BaseCaptchaTextInput):
 
         return super(CaptchaTextInput, self).render(name, self._value, attrs=attrs)
 
+    def _render(self, template_name, context, renderer=None):
+        return self.format_output(None)
+
 
 class CaptchaField(MultiValueField):
     def __init__(self, *args, **kwargs):
@@ -141,7 +148,8 @@ class CaptchaField(MultiValueField):
 
         kwargs['widget'] = kwargs.pop('widget', CaptchaTextInput(
             output_format=kwargs.pop('output_format', None),
-            id_prefix=kwargs.pop('id_prefix', None)
+            id_prefix=kwargs.pop('id_prefix', None),
+            generator=kwargs.pop('generator', None)
         ))
 
         super(CaptchaField, self).__init__(fields, *args, **kwargs)
@@ -154,7 +162,8 @@ class CaptchaField(MultiValueField):
     def clean(self, value):
         super(CaptchaField, self).clean(value)
         response, value[1] = (value[1] or '').strip().lower(), ''
-        CaptchaStore.remove_expired()
+        if not settings.CAPTCHA_GET_FROM_POOL:
+            CaptchaStore.remove_expired()
         if settings.CAPTCHA_TEST_MODE and response.lower() == 'passed':
             # automatically pass the test
             try:
@@ -167,7 +176,7 @@ class CaptchaField(MultiValueField):
             pass
         else:
             try:
-                CaptchaStore.objects.get(response=response, hashkey=value[0], expiration__gt=get_safe_now()).delete()
+                CaptchaStore.objects.get(response=response, hashkey=value[0], expiration__gt=timezone.now()).delete()
             except CaptchaStore.DoesNotExist:
                 raise ValidationError(getattr(self, 'error_messages', {}).get('invalid', ugettext_lazy('Invalid CAPTCHA')))
         return value
diff --git a/captcha/helpers.py b/captcha/helpers.py
index 1e8902d..f4a5ce1 100644
--- a/captcha/helpers.py
+++ b/captcha/helpers.py
@@ -12,7 +12,9 @@ def math_challenge():
     if operands[0] < operands[1] and '-' == operator:
         operands = (operands[1], operands[0])
     challenge = '%d%s%d' % (operands[0], operator, operands[1])
-    return '%s=' % (challenge), text_type(eval(challenge))
+    return '{}='.format(
+        challenge.replace('*', settings.CAPTCHA_MATH_CHALLENGE_OPERATOR)
+    ), text_type(eval(challenge))
 
 
 def random_char_challenge():
diff --git a/captcha/locale/pl/LC_MESSAGES/django.mo b/captcha/locale/pl/LC_MESSAGES/django.mo
index 8f2f73a..2bfa36b 100644
Binary files a/captcha/locale/pl/LC_MESSAGES/django.mo and b/captcha/locale/pl/LC_MESSAGES/django.mo differ
diff --git a/captcha/locale/pl/LC_MESSAGES/django.po b/captcha/locale/pl/LC_MESSAGES/django.po
index 5bcbf40..8af4b94 100644
--- a/captcha/locale/pl/LC_MESSAGES/django.po
+++ b/captcha/locale/pl/LC_MESSAGES/django.po
@@ -29,4 +29,4 @@ msgstr "Niepoprawnie wpisana CAPTCHA"
 
 #: tests/tests.py:125
 msgid "This field is required."
-msgstr "To pole jest wymagane"
+msgstr "To pole jest wymagane."
diff --git a/captcha/locale/zh_Hans/LC_MESSAGES/django.mo b/captcha/locale/zh_Hans/LC_MESSAGES/django.mo
new file mode 100644
index 0000000..1e7b47d
Binary files /dev/null and b/captcha/locale/zh_Hans/LC_MESSAGES/django.mo differ
diff --git a/captcha/locale/zh_Hans/LC_MESSAGES/django.po b/captcha/locale/zh_Hans/LC_MESSAGES/django.po
new file mode 100644
index 0000000..e85dcf9
--- /dev/null
+++ b/captcha/locale/zh_Hans/LC_MESSAGES/django.po
@@ -0,0 +1,32 @@
+# django-simple-captcha Chinese Simplified translation.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Ming Chen <c.ming at live.com>, 2013.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: django-simple-captcha 0.3.6\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2013-03-01 05:04+0800\n"
+"PO-Revision-Date: 2013-03-01 05:04+0800\n"
+"Last-Translator: Ming Chen <c.ming at live.com>\n"
+"Language-Team: zh_cn Ming Chen <c.ming at live.com>\n"
+"Language: zh_cn\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+
+#: fields.py:49
+msgid "Play CAPTCHA as audio file"
+msgstr "使用语音方式播放认证码"
+
+#: fields.py:66 fields.py:93 tests/__init__.py:69 tests/__init__.py:198
+#: tests/__init__.py:205
+msgid "Invalid CAPTCHA"
+msgstr "认证码错误"
+
+#: tests/__init__.py:95
+msgid "This field is required."
+msgstr "这个字段是必须的"
+
diff --git a/captcha/management/commands/captcha_clean.py b/captcha/management/commands/captcha_clean.py
index 88f28e4..9d9cdf5 100644
--- a/captcha/management/commands/captcha_clean.py
+++ b/captcha/management/commands/captcha_clean.py
@@ -1,5 +1,5 @@
 from django.core.management.base import BaseCommand
-from captcha.models import get_safe_now
+from django.utils import timezone
 import sys
 
 
@@ -9,7 +9,7 @@ class Command(BaseCommand):
     def handle(self, **options):
         from captcha.models import CaptchaStore
         verbose = int(options.get('verbosity'))
-        expired_keys = CaptchaStore.objects.filter(expiration__lte=get_safe_now()).count()
+        expired_keys = CaptchaStore.objects.filter(expiration__lte=timezone.now()).count()
         if verbose >= 1:
             print("Currently %d expired hashkeys" % expired_keys)
         try:
diff --git a/captcha/management/commands/captcha_create_pool.py b/captcha/management/commands/captcha_create_pool.py
new file mode 100644
index 0000000..fe862af
--- /dev/null
+++ b/captcha/management/commands/captcha_create_pool.py
@@ -0,0 +1,27 @@
+from django.core.management.base import BaseCommand
+from django.db import transaction
+from captcha.models import CaptchaStore
+
+
+class Command(BaseCommand):
+
+    help = "Create a pool of random captchas."
+
+    def add_arguments(self, parser):
+        parser.add_argument('--pool-size',
+                            type=int,
+                            default=1000,
+                            help='Number of new captchas to create, default=1000')
+        parser.add_argument('--cleanup-expired',
+                            action='store_true',
+                            default=True,
+                            help='Cleanup expired captchas after creating new ones')
+
+    @transaction.atomic()
+    def handle(self, **options):
+        verbose = int(options.get('verbosity'))
+        count = options.get('pool_size')
+        CaptchaStore.create_pool(count)
+        verbose and self.stdout.write('Created %d new captchas\n' % count)
+        options.get('cleanup_expired') and CaptchaStore.remove_expired()
+        options.get('cleanup_expired') and verbose and self.stdout.write('Expired captchas cleaned up\n')
diff --git a/captcha/models.py b/captcha/models.py
index 32c79ab..dd329fe 100644
--- a/captcha/models.py
+++ b/captcha/models.py
@@ -1,11 +1,12 @@
 from captcha.conf import settings as captcha_settings
 from django.db import models
-from django.conf import settings
+from django.utils import timezone
 from django.utils.encoding import smart_text
 import datetime
 import random
 import time
 import hashlib
+import logging
 
 
 # Heavily based on session key generation in Django
@@ -16,15 +17,7 @@ else:
     randrange = random.randrange
 MAX_RANDOM_KEY = 18446744073709551616     # 2 << 63
 
-
-def get_safe_now():
-    try:
-        from django.utils.timezone import utc
-        if settings.USE_TZ:
-            return datetime.datetime.utcnow().replace(tzinfo=utc)
-    except:
-        pass
-    return datetime.datetime.now()
+logger = logging.getLogger(__name__)
 
 
 class CaptchaStore(models.Model):
@@ -36,7 +29,7 @@ class CaptchaStore(models.Model):
     def save(self, *args, **kwargs):
         self.response = self.response.lower()
         if not self.expiration:
-            self.expiration = get_safe_now() + datetime.timedelta(minutes=int(captcha_settings.CAPTCHA_TIMEOUT))
+            self.expiration = timezone.now() + datetime.timedelta(minutes=int(captcha_settings.CAPTCHA_TIMEOUT))
         if not self.hashkey:
             key_ = (
                 smart_text(randrange(0, MAX_RANDOM_KEY)) +
@@ -52,12 +45,34 @@ class CaptchaStore(models.Model):
         return self.challenge
 
     def remove_expired(cls):
-        cls.objects.filter(expiration__lte=get_safe_now()).delete()
+        cls.objects.filter(expiration__lte=timezone.now()).delete()
     remove_expired = classmethod(remove_expired)
 
     @classmethod
-    def generate_key(cls):
-        challenge, response = captcha_settings.get_challenge()()
+    def generate_key(cls, generator=None):
+        challenge, response = captcha_settings.get_challenge(generator)()
         store = cls.objects.create(challenge=challenge, response=response)
 
         return store.hashkey
+
+    @classmethod
+    def pick(cls):
+        if not captcha_settings.CAPTCHA_GET_FROM_POOL:
+            return cls.generate_key()
+
+        def fallback():
+            logger.error("Couldn't get a captcha from pool, generating")
+            return cls.generate_key()
+
+        # Pick up a random item from pool
+        minimum_expiration = timezone.now() + datetime.timedelta(minutes=int(captcha_settings.CAPTCHA_GET_FROM_POOL_TIMEOUT))
+        store = cls.objects.filter(expiration__gt=minimum_expiration).order_by('?').first()
+
+        return (store and store.hashkey) or fallback()
+
+    @classmethod
+    def create_pool(cls, count=1000):
+        assert count > 0
+        while count > 0:
+            cls.generate_key()
+            count -= 1
diff --git a/captcha/templates/captcha/text_field.html b/captcha/templates/captcha/text_field.html
index 783e93d..fee60f4 100644
--- a/captcha/templates/captcha/text_field.html
+++ b/captcha/templates/captcha/text_field.html
@@ -1 +1 @@
-<input autocomplete="off" id="{{id}}_1" name="{{name}}_1" type="text" />
\ No newline at end of file
+<input autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" id="{{id}}_1" name="{{name}}_1" type="text" />
diff --git a/captcha/tests/tests.py b/captcha/tests/tests.py
index d7292eb..e2dbc8f 100644
--- a/captcha/tests/tests.py
+++ b/captcha/tests/tests.py
@@ -1,22 +1,25 @@
 # -*- coding: utf-8 -*-
 from captcha.conf import settings
 from captcha.fields import CaptchaField, CaptchaTextInput
-from captcha.models import CaptchaStore, get_safe_now
+from captcha.models import CaptchaStore
+from django.core import management
 from django.core.exceptions import ImproperlyConfigured
 from django.core.urlresolvers import reverse
 from django.test import TestCase, override_settings
 from django.utils.translation import ugettext_lazy
+from django.utils import timezone
 import datetime
 import json
 import re
 import six
+from testfixtures import LogCapture
 import os
 try:
     from cStringIO import StringIO
 except ImportError:
     from io import BytesIO as StringIO
 
-from six import u
+from six import u, text_type
 
 try:
     from PIL import Image
@@ -108,7 +111,7 @@ class CaptchaCase(TestCase):
             self.assertFormError(r, 'form', 'captcha', ugettext_lazy('Invalid CAPTCHA'))
 
     def test_deleted_expired(self):
-        self.default_store.expiration = get_safe_now() - datetime.timedelta(minutes=5)
+        self.default_store.expiration = timezone.now() - datetime.timedelta(minutes=5)
         self.default_store.save()
         hash_ = self.default_store.hashkey
         r = self.client.post(reverse('captcha-test'), dict(captcha_0=hash_, captcha_1=self.default_store.response, subject='xxx', sender='asasd at asdasd.com'))
@@ -205,6 +208,11 @@ class CaptchaCase(TestCase):
         r = self.client.get(reverse('test_per_form_format'))
         self.assertTrue('testPerFieldCustomFormatString' in str(r.content))
 
+    def test_custom_generator(self):
+        r = self.client.get(reverse('test_custom_generator'))
+        hash_, response = self.__extract_hash_and_response(r)
+        self.assertEqual(response, u'111111')
+
     def test_issue31_proper_abel(self):
         settings.CAPTCHA_OUTPUT_FORMAT = u('%(image)s %(hidden_field)s %(text_field)s')
         r = self.client.get(reverse('captcha-test'))
@@ -280,12 +288,12 @@ class CaptchaCase(TestCase):
 
     def test_autocomplete_off(self):
         r = self.client.get(reverse('captcha-test'))
-        self.assertTrue('<input autocomplete="off" ' in six.text_type(r.content))
+        self.assertTrue('<input autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" ' in six.text_type(r.content))
 
     def test_autocomplete_not_on_hidden_input(self):
         r = self.client.get(reverse('captcha-test'))
-        self.assertFalse('autocomplete="off" type="hidden" name="captcha_0"' in six.text_type(r.content))
-        self.assertFalse('autocomplete="off" id="id_captcha_0" name="captcha_0" type="hidden"' in six.text_type(r.content))
+        self.assertFalse('autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" type="hidden" name="captcha_0"' in six.text_type(r.content))
+        self.assertFalse('autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" id="id_captcha_0" name="captcha_0" type="hidden"' in six.text_type(r.content))
 
     def test_transparent_background(self):
         __current_test_mode_setting = settings.CAPTCHA_BACKGROUND_COLOR
@@ -361,6 +369,54 @@ class CaptchaCase(TestCase):
             self.assertTrue('captcha-template-test' in six.text_type(r.content))
         settings.CAPTCHA_IMAGE_TEMPLATE = __current_test_mode_setting
 
+    def test_math_challenge(self):
+        __current_test_mode_setting = settings.CAPTCHA_MATH_CHALLENGE_OPERATOR
+        settings.CAPTCHA_MATH_CHALLENGE_OPERATOR = '~'
+        helper = 'captcha.helpers.math_challenge'
+        challenge, response = settings._callable_from_string(helper)()
+
+        while settings.CAPTCHA_MATH_CHALLENGE_OPERATOR not in challenge:
+            challenge, response = settings._callable_from_string(helper)()
+
+        self.assertEqual(response, text_type(eval(challenge.replace(settings.CAPTCHA_MATH_CHALLENGE_OPERATOR, '*')[:-1])))
+        settings.CAPTCHA_MATH_CHALLENGE_OPERATOR = __current_test_mode_setting
+
+    def test_get_from_pool(self):
+        __current_test_get_from_pool_setting = settings.CAPTCHA_GET_FROM_POOL
+        __current_test_get_from_pool_timeout_setting = settings.CAPTCHA_GET_FROM_POOL_TIMEOUT
+        __current_test_timeout_setting = settings.CAPTCHA_TIMEOUT
+        settings.CAPTCHA_GET_FROM_POOL = True
+        settings.CAPTCHA_GET_FROM_POOL_TIMEOUT = 5
+        settings.CAPTCHA_TIMEOUT = 90
+        CaptchaStore.objects.all().delete()  # Delete objects created during SetUp
+        POOL_SIZE = 10
+        CaptchaStore.create_pool(count=POOL_SIZE)
+        self.assertEqual(CaptchaStore.objects.count(), POOL_SIZE)
+        pool = CaptchaStore.objects.values_list('hashkey', flat=True)
+        random_pick = CaptchaStore.pick()
+        self.assertIn(random_pick, pool)
+        # pick() should not create any extra captcha
+        self.assertEqual(CaptchaStore.objects.count(), POOL_SIZE)
+        settings.CAPTCHA_GET_FROM_POOL = __current_test_get_from_pool_setting
+        settings.CAPTCHA_GET_FROM_POOL_TIMEOUT = __current_test_get_from_pool_timeout_setting
+        settings.CAPTCHA_TIMEOUT = __current_test_timeout_setting
+
+    def test_captcha_create_pool(self):
+        CaptchaStore.objects.all().delete()  # Delete objects created during SetUp
+        POOL_SIZE = 10
+        management.call_command('captcha_create_pool', pool_size=POOL_SIZE, verbosity=0)
+        self.assertEqual(CaptchaStore.objects.count(), POOL_SIZE)
+
+    def test_empty_pool_fallback(self):
+        __current_test_get_from_pool_setting = settings.CAPTCHA_GET_FROM_POOL
+        settings.CAPTCHA_GET_FROM_POOL = True
+        CaptchaStore.objects.all().delete()  # Delete objects created during SetUp
+        with LogCapture() as l:
+            CaptchaStore.pick()
+        l.check(('captcha.models', 'ERROR', "Couldn't get a captcha from pool, generating"),)
+        self.assertEqual(CaptchaStore.objects.count(), 1)
+        settings.CAPTCHA_GET_FROM_POOL = __current_test_get_from_pool_setting
+
 
 def trivial_challenge():
     return 'trivial', 'trivial'
diff --git a/captcha/tests/urls.py b/captcha/tests/urls.py
index 5f8eb34..eaf114e 100644
--- a/captcha/tests/urls.py
+++ b/captcha/tests/urls.py
@@ -1,6 +1,6 @@
 from django.conf.urls import url, include
 from .views import (
-    test, test_model_form, test_custom_error_message, test_per_form_format, test_non_required, test_id_prefix
+    test, test_model_form, test_custom_error_message, test_per_form_format, test_non_required, test_id_prefix, test_custom_generator
 )
 
 urlpatterns = [
@@ -8,6 +8,7 @@ urlpatterns = [
     url(r'test-modelform/$', test_model_form, name='captcha-test-model-form'),
     url(r'test2/$', test_custom_error_message, name='captcha-test-custom-error-message'),
     url(r'test3/$', test_per_form_format, name='test_per_form_format'),
+    url(r'custom-generator/$', test_custom_generator, name='test_custom_generator'),
     url(r'test-non-required/$', test_non_required, name='captcha-test-non-required'),
     url(r'test-id-prefix/$', test_id_prefix, name='captcha-test-id-prefix'),
     url(r'', include('captcha.urls')),
diff --git a/captcha/tests/views.py b/captcha/tests/views.py
index 230d478..84f30ea 100644
--- a/captcha/tests/views.py
+++ b/captcha/tests/views.py
@@ -85,6 +85,19 @@ def test_model_form(request):
     return _test(request, CaptchaTestModelForm)
 
 
+def test_custom_generator(request):
+    class CaptchaTestModelForm(forms.ModelForm):
+        subject = forms.CharField(max_length=100)
+        sender = forms.EmailField()
+        captcha = CaptchaField(generator=lambda: ('111111', '111111'))
+
+        class Meta:
+            model = User
+            fields = ('subject', 'sender', 'captcha', )
+
+    return _test(request, CaptchaTestModelForm)
+
+
 def test_custom_error_message(request):
     class CaptchaTestErrorMessageForm(forms.Form):
         captcha = CaptchaField(
diff --git a/captcha/views.py b/captcha/views.py
index c3517de..28a8249 100644
--- a/captcha/views.py
+++ b/captcha/views.py
@@ -34,7 +34,7 @@ from_top = 4
 
 def getsize(font, text):
     if hasattr(font, 'getoffset'):
-        return [x + y for x, y in zip(font.getsize(text), font.getoffset(text))]
+        return tuple([x + y for x, y in zip(font.getsize(text), font.getoffset(text))])
     else:
         return font.getsize(text)
 
@@ -162,7 +162,7 @@ def captcha_refresh(request):
     if not request.is_ajax():
         raise Http404
 
-    new_key = CaptchaStore.generate_key()
+    new_key = CaptchaStore.pick()
     to_json_response = {
         'key': new_key,
         'image_url': captcha_image_url(new_key),
diff --git a/django_simple_captcha.egg-info/PKG-INFO b/django_simple_captcha.egg-info/PKG-INFO
index 50dc3e5..48fb03c 100644
--- a/django_simple_captcha.egg-info/PKG-INFO
+++ b/django_simple_captcha.egg-info/PKG-INFO
@@ -1,6 +1,6 @@
 Metadata-Version: 1.1
 Name: django-simple-captcha
-Version: 0.5.1
+Version: 0.5.5
 Summary: A very simple, yet powerful, Django captcha application
 Home-page: https://github.com/mbi/django-simple-captcha
 Author: Marco Bonetti
diff --git a/django_simple_captcha.egg-info/SOURCES.txt b/django_simple_captcha.egg-info/SOURCES.txt
index f2522f6..cc4628d 100644
--- a/django_simple_captcha.egg-info/SOURCES.txt
+++ b/django_simple_captcha.egg-info/SOURCES.txt
@@ -48,9 +48,12 @@ captcha/locale/uk/LC_MESSAGES/django.mo
 captcha/locale/uk/LC_MESSAGES/django.po
 captcha/locale/zh_CN/LC_MESSAGES/django.mo
 captcha/locale/zh_CN/LC_MESSAGES/django.po
+captcha/locale/zh_Hans/LC_MESSAGES/django.mo
+captcha/locale/zh_Hans/LC_MESSAGES/django.po
 captcha/management/__init__.py
 captcha/management/commands/__init__.py
 captcha/management/commands/captcha_clean.py
+captcha/management/commands/captcha_create_pool.py
 captcha/migrations/0001_initial.py
 captcha/migrations/__init__.py
 captcha/south_migrations/0001_initial.py
diff --git a/docs/advanced.rst b/docs/advanced.rst
index 54b69d0..ca38124 100644
--- a/docs/advanced.rst
+++ b/docs/advanced.rst
@@ -63,6 +63,13 @@ See Generators below for a list of available generators and a guide on how to wr
 
 Defaults to: ``'captcha.helpers.random_char_challenge'``
 
+CAPTCHA_MATH_CHALLENGE_OPERATOR
+-------------------------------
+
+When using the ``math_challenge``, lets you choose the multiplication operator. Use lowercase ``'x'`` for cross sign.
+
+Defaults to: ``'*'`` (asterisk sign)
+
 CAPTCHA_NOISE_FUNCTIONS
 ------------------------
 
@@ -154,6 +161,22 @@ Use this for testing purposes. Warning: do NOT set this to True in production.
 Defaults to: False
 
 
+CAPTCHA_GET_FROM_POOL
+---------------------
+
+By default, `django-simple-captcha` generates a new captcha when needed and stores it in the database. This occurs in a `HTTP GET request`, which may not be wished. This default behavior may also conflict with a load balanced infrastructure, where there is more than one database to read data from. If this setting is `True`, when a new captcha is needed, a random one will be just read from a pool of captchas saved previously in the database. In this case, the custom management command `ca [...]
+
+Defaults to: False
+
+
+CAPTCHA_GET_FROM_POOL_TIMEOUT
+-----------------------------
+
+This is a timeout value in minutes used only if `CAPTCHA_GET_FROM_POOL` (see above) is `True`. When picking up randomly from the pool, this setting will prevent to pick up a captcha that expires sooner than `CAPTCHA_GET_FROM_POOL_TIMEOUT`.
+
+Defaults to: 5
+
+
 Rendering
 +++++++++
 
@@ -192,6 +215,7 @@ The ``captcha/field.html`` template receives the following context:
 
 Note: these elements have been marked as safe, you can render them straight into your template.
 
+.. _generators_ref:
 
 Generators and modifiers
 ++++++++++++++++++++++++
diff --git a/docs/conf.py b/docs/conf.py
index 2c130b9..6b62f62 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -44,14 +44,14 @@ master_doc = 'index'
 
 # General information about the project.
 project = u('Django Simple Captcha')
-copyright = u('2011-2015 Marco Bonetti')
+copyright = u('2011-2016 Marco Bonetti')
 
 # The version info for the project you're documenting, acts as replacement for
 # |version| and |release|, also used in various other places throughout the
 # built documents.
 #
 # The short X.Y version.
-version = '0.5.1'
+version = '0.5.5'
 # The full version, including alpha/beta/rc tags.
 release = version
 
diff --git a/docs/index.rst b/docs/index.rst
index 962f9bb..06cedfc 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -20,7 +20,7 @@ Features
 Requirements
 ++++++++++++
 
-* Django 1.7+
+* Django 1.8+
 * A recent version of Pillow compiled with FreeType support
 * Flite is required for text-to-speech (audio) output, but not mandatory
 
diff --git a/docs/usage.rst b/docs/usage.rst
index 91b9039..b5cc306 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -82,6 +82,7 @@ Passing arguments to the field
 
 * ``output_format`` will let you format the layout of the rendered field. Defaults to the value defined in : :ref:`output_format_ref`.
 * ``id_prefix`` Optional prefix that will be added to the ID attribute in the generated fields and labels, to be used when e.g. several Captcha fields are being displayed on a same page. (added in version 0.4.4)
+* ``generator`` Optional callable or module path to callable that will be used to generate the challenge and the response, e.g. ``generator='path.to.generator_function'`` or ``generator=lambda: ('LOL', 'LOL')``, see also :ref:`generators_ref`. Defaults to whatever is defined in ``settings.CAPTCHA_CHALLENGE_FUNCT``.
 
 Example usage for ajax form
 ---------------------------
diff --git a/testproject/settings.py b/testproject/settings.py
index dbd6911..635a3cc 100644
--- a/testproject/settings.py
+++ b/testproject/settings.py
@@ -67,9 +67,6 @@ TEMPLATES = [
     },
 ]
 
-TEMPLATE_DIRS = ('templates',)
-
-# Django 1.4 TZ support
 USE_TZ = True
 SECRET_KEY = 'empty'
 
diff --git a/testproject/urls.py b/testproject/urls.py
index 9d85bbb..20a7459 100644
--- a/testproject/urls.py
+++ b/testproject/urls.py
@@ -1,10 +1,11 @@
 try:
-    from django.conf.urls import patterns, include, url
+    from django.conf.urls import include, url
 except ImportError:
-    from django.conf.urls.defaults import patterns, include, url
+    from django.conf.urls.defaults import include, url
 
-urlpatterns = patterns(
-    '',
-    url(r'^$', 'views.home'),
+from .views import home
+
+urlpatterns = [
+    url(r'^$', home),
     url(r'^captcha/', include('captcha.urls')),
-)
+]
diff --git a/testproject/views.py b/testproject/views.py
index 1fa471e..735016e 100644
--- a/testproject/views.py
+++ b/testproject/views.py
@@ -1,7 +1,6 @@
-from django.template import RequestContext
 from django.http import HttpResponseRedirect
 from forms import CaptchaForm
-from django.shortcuts import render_to_response
+from django.shortcuts import render
 
 
 def home(request):
@@ -12,6 +11,4 @@ def home(request):
     else:
         form = CaptchaForm()
 
-    return render_to_response('home.html', dict(
-        form=form
-    ), context_instance=RequestContext(request))
+    return render(request, 'home.html', {'form': form})
diff --git a/tox.ini b/tox.ini
index 724394f..a95a7a5 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,7 +1,6 @@
 [tox]
 envlist =
-        py27-django17,
-        {py27,py35}-django{18,19},
+        {py27,py36}-django{18,19,110,111},
         gettext,flake8,docs
 
 skipsdist = True
@@ -16,15 +15,17 @@ setenv =
         PYTHONDONTWRITEBYTECODE=1
 
 deps =
-        django17: Django==1.7.11
         django18: Django==1.8.7
         django19: Django==1.9
+        django110: Django==1.10
+        django111: Django==1.11
 
-        py27-django{17,18,19}: python-memcached
-        py35-django{18,19}: python3-memcached
+        py27-django{18,19,110,111}: python-memcached
+        py36-django{18,19,110,111}: python3-memcached
         Pillow
         six
         south
+        testfixtures
 
 [testenv:gettext]
 changedir = captcha/locale/
@@ -50,7 +51,7 @@ commands =
 
 [testenv:flake8]
 basepython = python
-deps = flake8
+deps = flake8==2.4.1
 commands=
     flake8 {toxinidir}/captcha
 

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/python-modules/packages/django-simple-captcha.git



More information about the Python-modules-commits mailing list