Skip to content

Commit

Permalink
Merge branch 'use-widget-templates' of https://github.com/ziima/djang…
Browse files Browse the repository at this point in the history
…o-simple-captcha into ziima-use-widget-templates
  • Loading branch information
mbi committed May 18, 2018
2 parents 45c689d + fcfa8c7 commit 0577e58
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 23 deletions.
2 changes: 1 addition & 1 deletion CHANGES
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Version 0.5.7 (unreleased)
--------------------------
* Use standard rendering of widgets (Issue #128, PR #133, thanks @ziima)
* Use templates for rendering of widgets (Issue #128, #134, PR #133, #139, thanks @ziima)
* Always defined audio context variable (PR #132, thanks @ziima)
* Test against Django 2.1a

Expand Down
12 changes: 11 additions & 1 deletion captcha/conf/settings.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import os
import warnings

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')))
Expand All @@ -22,8 +24,16 @@
CAPTCHA_IMAGE_TEMPLATE = getattr(settings, 'CAPTCHA_IMAGE_TEMPLATE', 'captcha/image.html')
CAPTCHA_HIDDEN_FIELD_TEMPLATE = getattr(settings, 'CAPTCHA_HIDDEN_FIELD_TEMPLATE', 'captcha/hidden_field.html')
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')

if getattr(settings, 'CAPTCHA_FIELD_TEMPLATE', None):
msg = ("CAPTCHA_FIELD_TEMPLATE setting is deprecated in favor of widget's template_name.")
warnings.warn(msg, DeprecationWarning)
CAPTCHA_FIELD_TEMPLATE = getattr(settings, 'CAPTCHA_FIELD_TEMPLATE', None)
if getattr(settings, 'CAPTCHA_OUTPUT_FORMAT', None):
msg = ("CAPTCHA_OUTPUT_FORMAT setting is deprecated in favor of widget's template_name.")
warnings.warn(msg, DeprecationWarning)
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)
Expand Down
62 changes: 44 additions & 18 deletions captcha/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,28 @@
from six import u


class CaptchaAnswerInput(TextInput):
"""Text input for captcha answer."""

# Use *args and **kwargs because signature changed in Django 1.11
def build_attrs(self, *args, **kwargs):
"""Disable automatic corrections and completions."""
attrs = super(CaptchaAnswerInput, self).build_attrs(*args, **kwargs)
attrs['autocapitalize'] = 'off'
attrs['autocomplete'] = 'off'
attrs['autocorrect'] = 'off'
attrs['spellcheck'] = 'false'
return attrs


class BaseCaptchaTextInput(MultiWidget):
"""
Base class for Captcha widgets
"""
def __init__(self, attrs=None):
widgets = (
HiddenInput(attrs),
TextInput(attrs),
CaptchaAnswerInput(attrs),
)
super(BaseCaptchaTextInput, self).__init__(widgets, attrs)

Expand Down Expand Up @@ -70,20 +84,23 @@ def refresh_url(self):


class CaptchaTextInput(BaseCaptchaTextInput):

template_name = 'captcha/widgets/captcha.html'

def __init__(self, attrs=None, field_template=None, id_prefix=None, generator=None, output_format=None):
self.id_prefix = id_prefix
self.generator = generator
if field_template is not None:
msg = ("CaptchaTextInput's field_template argument is deprecated in favor of widget's template_name.")
warnings.warn(msg, DeprecationWarning)
self.field_template = field_template or settings.CAPTCHA_FIELD_TEMPLATE
self.output_format = output_format or settings.CAPTCHA_OUTPUT_FORMAT
if self.output_format:
msg = ("CAPTCHA_OUTPUT_FORMAT setting and CaptchaTextInput's output_format argument are deprecated in "
"favor of CAPTCHA_FIELD_TEMPLATE and field_template, respectively.")
if output_format is not None:
msg = ("CaptchaTextInput's output_format argument is deprecated in favor of widget's template_name.")
warnings.warn(msg, DeprecationWarning)

if self.output_format is None and self.field_template is None:
raise ImproperlyConfigured(
'You MUST define CAPTCHA_FIELD_TEMPLATE setting. Please refer to '
'http://readthedocs.org/docs/django-simple-captcha/en/latest/usage.html#installation')
self.output_format = output_format or settings.CAPTCHA_OUTPUT_FORMAT
# Fallback to custom rendering in Django < 1.11
if not hasattr(self, '_render') and self.field_template is None and self.output_format is None:
self.field_template = 'captcha/field.html'

if self.output_format:
for key in ('image', 'hidden_field', 'text_field'):
Expand All @@ -107,6 +124,13 @@ def id_for_label(self, id_):
ret = '%s_%s' % (self.id_prefix, ret)
return ret

def get_context(self, name, value, attrs):
"""Add captcha specific variables to context."""
context = super(CaptchaTextInput, self).get_context(name, value, attrs)
context['image'] = self.image_url()
context['audio'] = self.audio_url()
return context

def format_output(self, rendered_widgets):
# hidden_field, text_field = rendered_widgets
if self.output_format:
Expand All @@ -123,22 +147,27 @@ def format_output(self, rendered_widgets):
'hidden_field': mark_safe(self.hidden_field),
'text_field': mark_safe(self.text_field)
}
return render_to_string(settings.CAPTCHA_FIELD_TEMPLATE, context)

def render(self, name, value, attrs=None, renderer=None):
self.fetch_captcha_store(name, value, attrs, self.generator)
return render_to_string(self.field_template, context)

def _direct_render(self, name, attrs):
"""Render the widget the old way - using field_template or output_format."""
context = {
'image': self.image_url(),
'name': name,
'key': self._key,
'id': u'%s_%s' % (self.id_prefix, attrs.get('id')) if self.id_prefix else attrs.get('id'),
'audio': self.audio_url(),
}

self.image_and_audio = render_to_string(settings.CAPTCHA_IMAGE_TEMPLATE, context)
self.hidden_field = render_to_string(settings.CAPTCHA_HIDDEN_FIELD_TEMPLATE, context)
self.text_field = render_to_string(settings.CAPTCHA_TEXT_FIELD_TEMPLATE, context)
return self.format_output(None)

def render(self, name, value, attrs=None, renderer=None):
self.fetch_captcha_store(name, value, attrs, self.generator)

if self.field_template or self.output_format:
return self._direct_render(name, attrs)

extra_kwargs = {}
if django.VERSION >= (1, 11):
Expand All @@ -147,9 +176,6 @@ def render(self, name, value, attrs=None, renderer=None):

return super(CaptchaTextInput, self).render(name, self._value, attrs=attrs, **extra_kwargs)

def _render(self, template_name, context, renderer=None):
return self.format_output(None)


class CaptchaField(MultiValueField):
def __init__(self, *args, **kwargs):
Expand Down
9 changes: 9 additions & 0 deletions captcha/templates/captcha/widgets/captcha.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% load i18n %}
{% spaceless %}
{% if audio %}
<a title="{% trans "Play CAPTCHA as audio file" %}" href="{{ audio }}">
{% endif %}
<img src="{{ image }}" alt="captcha" class="captcha" />
{% if audio %}</a>{% endif %}
{% endspaceless %}
{% include "django/forms/widgets/multiwidget.html" %}
17 changes: 16 additions & 1 deletion captcha/tests/tests.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# -*- coding: utf-8 -*-
import unittest

from captcha.conf import settings
from captcha.fields import CaptchaField, CaptchaTextInput
from captcha.models import CaptchaStore
Expand Down Expand Up @@ -292,9 +294,19 @@ def test_missing_value(self):
self.assertEqual(r.status_code, 200)
self.assertTrue(str(r.content).find('Form validated') > 0)

@unittest.skipUnless(django.VERSION < (1, 11), "Test only for Django < 1.11")
def test_autocomplete_off_django_110(self):
r = self.client.get(reverse('captcha-test'))
captcha_input = ('<input type="text" name="captcha_1" autocomplete="off" spellcheck="false" autocorrect="off" '
'autocapitalize="off" id="id_captcha_1" />')
self.assertContains(r, captcha_input, html=True)

@unittest.skipIf(django.VERSION < (1, 11), "Test only for Django >= 1.11")
def test_autocomplete_off(self):
r = self.client.get(reverse('captcha-test'))
self.assertTrue('<input autocapitalize="off" autocomplete="off" autocorrect="off" spellcheck="false" ' in six.text_type(r.content))
captcha_input = ('<input type="text" name="captcha_1" autocomplete="off" spellcheck="false" autocorrect="off" '
'autocapitalize="off" id="id_captcha_1" required />')
self.assertContains(r, captcha_input, html=True)

def test_autocomplete_not_on_hidden_input(self):
r = self.client.get(reverse('captcha-test'))
Expand Down Expand Up @@ -367,13 +379,16 @@ def test_multiple_fonts(self):

def test_template_overrides(self):
__current_test_mode_setting = settings.CAPTCHA_IMAGE_TEMPLATE
__current_field_template = settings.CAPTCHA_FIELD_TEMPLATE
settings.CAPTCHA_IMAGE_TEMPLATE = 'captcha_test/image.html'
settings.CAPTCHA_FIELD_TEMPLATE = 'captcha/field.html'

for urlname in ('captcha-test', 'captcha-test-model-form'):
settings.CAPTCHA_CHALLENGE_FUNCT = 'captcha.tests.trivial_challenge'
r = self.client.get(reverse(urlname))
self.assertTrue('captcha-template-test' in six.text_type(r.content))
settings.CAPTCHA_IMAGE_TEMPLATE = __current_test_mode_setting
settings.CAPTCHA_FIELD_TEMPLATE = __current_field_template

def test_math_challenge(self):
__current_test_mode_setting = settings.CAPTCHA_MATH_CHALLENGE_OPERATOR
Expand Down
18 changes: 16 additions & 2 deletions docs/advanced.rst
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ Defaults to: ``None``

(Used to default to: ``u'%(image)s %(hidden_field)s %(text_field)s'``)

Note: this settings is deprecated in favor of template-based field rendering, use ``CAPTCHA_FIELD_TEMPLATE`` instead (see the Rendering section below).
.. warning:: This setting is deprecated in favor of template-based widget rendering (see the Rendering section below).


CAPTCHA_TEST_MODE
Expand Down Expand Up @@ -194,6 +194,20 @@ Defaults to: 5
Rendering
+++++++++

``CaptchaTextInput`` supports the widget rendering using template introduced in Django 1.11.
To change the output HTML, change the ``template_name`` to a custom template or modify ``get_context`` method to provide further context.
See https://docs.djangoproject.com/en/dev/ref/forms/renderers/ for description of rendering API.
Keep in mind that ``CaptchaTextInput`` is a subclass of ``MultiWidget`` whic affects the context, see https://docs.djangoproject.com/en/2.0/ref/forms/widgets/#multiwidget.

.. attention:: To provide backwards compatibility, the old style rendering has priority over the widget templates.
If the ``CAPTCHA_FIELD_TEMPLATE`` or ``CAPTCHA_OUTPUT_FORMAT`` settings or ``field_templates`` or ``output_format`` parameter are set, the direct rendering gets higher priority.
If widget templates are ignored, make sure you're using Django >= 1.11 and disable these settings and parameters.

Old style rendering
-------------------

.. warning:: This rendering method is deprecated. Use Django >= 1.11 and widgets templates instead.

A CAPTCHA field is made up of three components:

* The actual image that the end user has to copy from
Expand All @@ -212,7 +226,7 @@ As of version 0.4.7 you can control how the individual components are rendered,
These templates can be overriden in your own ``templates`` folder, or you can change the actual template names by settings ``CAPTCHA_IMAGE_TEMPLATE``, ``CAPTCHA_TEXT_FIELD_TEMPLATE``, ``CAPTCHA_HIDDEN_FIELD_TEMPLATE`` and ``CAPTCHA_FIELD_TEMPLATE``, respectively.

Context
-------
~~~~~~~

The following context variables are passed to the three "individual" templates:

Expand Down

0 comments on commit 0577e58

Please sign in to comment.