-
Notifications
You must be signed in to change notification settings - Fork 319
/
fields.py
274 lines (233 loc) · 9.57 KB
/
fields.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
import warnings
import django
from django.core.exceptions import ImproperlyConfigured
from django.forms import ValidationError
from django.forms.fields import CharField, MultiValueField
from django.forms.widgets import HiddenInput, MultiWidget, TextInput
from django.template.loader import render_to_string
from django.utils import timezone
from django.utils.safestring import mark_safe
from captcha.conf import settings
from captcha.models import CaptchaStore
from six import u
if django.VERSION < (1, 10): # NOQA
from django.core.urlresolvers import reverse, NoReverseMatch # NOQA
else: # NOQA
from django.urls import reverse, NoReverseMatch # NOQA
if django.VERSION >= (3, 0):
from django.utils.translation import gettext_lazy as ugettext_lazy
else:
from django.utils.translation import ugettext_lazy
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), CaptchaAnswerInput(attrs))
super(BaseCaptchaTextInput, self).__init__(widgets, attrs)
def decompress(self, value):
if value:
return value.split(",")
return [None, None]
def fetch_captcha_store(self, name, value, attrs=None, generator=None):
"""
Fetches a new CaptchaStore
This has to be called inside render
"""
try:
reverse("captcha-image", args=("dummy",))
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"
)
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("")]
self._key = key
self.id_ = self.build_attrs(attrs).get("id", None)
def id_for_label(self, id_):
if id_:
return id_ + "_1"
return id_
def image_url(self):
return reverse("captcha-image", kwargs={"key": self._key})
def audio_url(self):
return (
reverse("captcha-audio", kwargs={"key": self._key})
if settings.CAPTCHA_FLITE_PATH
else None
)
def refresh_url(self):
return reverse("captcha-refresh")
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
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)
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"):
if "%%(%s)s" % key not in self.output_format:
raise ImproperlyConfigured(
"All of %s must be present in your CAPTCHA_OUTPUT_FORMAT setting. Could not find %s"
% (
", ".join(
[
"%%(%s)s" % k
for k in ("image", "hidden_field", "text_field")
]
),
"%%(%s)s" % key,
)
)
super(CaptchaTextInput, self).__init__(attrs)
def build_attrs(self, *args, **kwargs):
ret = super(CaptchaTextInput, self).build_attrs(*args, **kwargs)
if self.id_prefix and "id" in ret:
ret["id"] = "%s_%s" % (self.id_prefix, ret["id"])
return ret
def id_for_label(self, id_):
ret = super(CaptchaTextInput, self).id_for_label(id_)
if self.id_prefix and "id" in ret:
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:
ret = self.output_format % {
"image": self.image_and_audio,
"hidden_field": self.hidden_field,
"text_field": self.text_field,
}
return ret
elif self.field_template:
context = {
"image": mark_safe(self.image_and_audio),
"hidden_field": mark_safe(self.hidden_field),
"text_field": mark_safe(self.text_field),
}
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):
# https://docs.djangoproject.com/en/1.11/ref/forms/widgets/#django.forms.Widget.render
extra_kwargs["renderer"] = renderer
return super(CaptchaTextInput, self).render(
name, self._value, attrs=attrs, **extra_kwargs
)
class CaptchaField(MultiValueField):
def __init__(self, *args, **kwargs):
fields = (CharField(show_hidden_initial=True), CharField())
if "error_messages" not in kwargs or "invalid" not in kwargs.get(
"error_messages"
):
if "error_messages" not in kwargs:
kwargs["error_messages"] = {}
kwargs["error_messages"].update(
{"invalid": ugettext_lazy("Invalid CAPTCHA")}
)
kwargs["widget"] = kwargs.pop(
"widget",
CaptchaTextInput(
output_format=kwargs.pop("output_format", None),
id_prefix=kwargs.pop("id_prefix", None),
generator=kwargs.pop("generator", None),
),
)
super(CaptchaField, self).__init__(fields, *args, **kwargs)
def compress(self, data_list):
if data_list:
return ",".join(data_list)
return None
def clean(self, value):
super(CaptchaField, self).clean(value)
response, value[1] = (value[1] or "").strip().lower(), ""
if not settings.CAPTCHA_GET_FROM_POOL:
CaptchaStore.remove_expired()
if settings.CAPTCHA_TEST_MODE and response.lower() == "passed":
# automatically pass the test
try:
# try to delete the captcha based on its hash
CaptchaStore.objects.get(hashkey=value[0]).delete()
except CaptchaStore.DoesNotExist:
# ignore errors
pass
elif not self.required and not response:
pass
else:
try:
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