Coverage for /home/marco/Code/django-simple-captcha/captcha/fields.py: 89.15%
107 statements
« prev ^ index » next coverage.py v7.6.5, created at 2024-11-15 09:22 +0100
« prev ^ index » next coverage.py v7.6.5, created at 2024-11-15 09:22 +0100
1from django.core.exceptions import ImproperlyConfigured
2from django.forms import ValidationError
3from django.forms.fields import CharField, MultiValueField
4from django.forms.widgets import HiddenInput, MultiWidget, TextInput
5from django.urls import NoReverseMatch, reverse
6from django.utils import timezone
7from django.utils.translation import gettext_lazy
9from captcha.conf import settings
10from captcha.models import CaptchaStore
13class CaptchaHiddenInput(HiddenInput):
14 """Hidden input for the captcha key."""
16 # Use *args and **kwargs because signature changed in Django 1.11
17 def build_attrs(self, *args, **kwargs):
18 """Disable autocomplete to prevent problems on page reload."""
20 attrs = super().build_attrs(*args, **kwargs)
21 attrs["autocomplete"] = "off"
22 return attrs
25class CaptchaAnswerInput(TextInput):
26 """Text input for captcha answer."""
28 # Use *args and **kwargs because signature changed in Django 1.11
29 def build_attrs(self, *args, **kwargs):
30 """Disable automatic corrections and completions."""
31 attrs = super().build_attrs(*args, **kwargs)
32 attrs["autocapitalize"] = "off"
33 attrs["autocomplete"] = "off"
34 attrs["autocorrect"] = "off"
35 attrs["spellcheck"] = "false"
36 return attrs
39class BaseCaptchaTextInput(MultiWidget):
40 """
41 Base class for Captcha widgets
42 """
44 def __init__(self, attrs=None):
45 widgets = (CaptchaHiddenInput(attrs), CaptchaAnswerInput(attrs))
46 super().__init__(widgets, attrs)
48 def decompress(self, value):
49 if value:
50 return value.split(",")
51 return [None, None]
53 def fetch_captcha_store(self, name, value, attrs=None, generator=None):
54 """
55 Fetches a new CaptchaStore
56 This has to be called inside render
57 """
58 try:
59 reverse("captcha-image", args=("dummy",))
60 except NoReverseMatch:
61 raise ImproperlyConfigured(
62 "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"
63 )
65 if settings.CAPTCHA_GET_FROM_POOL: 65 ↛ 66line 65 didn't jump to line 66 because the condition on line 65 was never true
66 key = CaptchaStore.pick()
67 else:
68 key = CaptchaStore.generate_key(generator)
70 # these can be used by format_output and render
71 self._value = [key, ""]
72 self._key = key
73 self.id_ = self.build_attrs(attrs).get("id", None)
75 def id_for_label(self, id_):
76 if id_: 76 ↛ 78line 76 didn't jump to line 78 because the condition on line 76 was always true
77 return id_ + "_1"
78 return id_
80 def image_url(self):
81 return reverse("captcha-image", kwargs={"key": self._key})
83 def audio_url(self):
84 return (
85 reverse("captcha-audio", kwargs={"key": self._key})
86 if settings.CAPTCHA_FLITE_PATH
87 else None
88 )
90 def refresh_url(self):
91 return reverse("captcha-refresh")
94class CaptchaTextInput(BaseCaptchaTextInput):
95 template_name = "captcha/widgets/captcha.html"
97 def __init__(
98 self,
99 attrs=None,
100 id_prefix=None,
101 generator=None,
102 # output_format=None,
103 ):
104 self.id_prefix = id_prefix
105 self.generator = generator
106 super().__init__(attrs)
108 def build_attrs(self, *args, **kwargs):
109 ret = super().build_attrs(*args, **kwargs)
110 if self.id_prefix and "id" in ret:
111 ret["id"] = "%s_%s" % (self.id_prefix, ret["id"])
112 return ret
114 def id_for_label(self, id_):
115 ret = super().id_for_label(id_)
116 if self.id_prefix and "id" in ret:
117 ret = "%s_%s" % (self.id_prefix, ret)
118 return ret
120 def get_context(self, name, value, attrs):
121 """Add captcha specific variables to context."""
122 context = super().get_context(name, value, attrs)
123 context["image"] = self.image_url()
124 context["audio"] = self.audio_url()
125 return context
127 def render(self, name, value, attrs=None, renderer=None):
128 self.fetch_captcha_store(name, value, attrs, self.generator)
130 extra_kwargs = {}
131 extra_kwargs["renderer"] = renderer
133 return super().render(name, self._value, attrs=attrs, **extra_kwargs)
136class CaptchaField(MultiValueField):
137 def __init__(self, *args, **kwargs):
138 fields = (CharField(show_hidden_initial=True), CharField())
139 if "error_messages" not in kwargs or "invalid" not in kwargs.get(
140 "error_messages"
141 ):
142 if "error_messages" not in kwargs: 142 ↛ 144line 142 didn't jump to line 144 because the condition on line 142 was always true
143 kwargs["error_messages"] = {}
144 kwargs["error_messages"].update(
145 {"invalid": gettext_lazy("Invalid CAPTCHA")}
146 )
148 kwargs["widget"] = kwargs.pop(
149 "widget",
150 CaptchaTextInput(
151 id_prefix=kwargs.pop("id_prefix", None),
152 generator=kwargs.pop("generator", None),
153 ),
154 )
156 super().__init__(fields, *args, **kwargs)
158 def compress(self, data_list):
159 if data_list:
160 return ",".join(data_list)
161 return None
163 def clean(self, value):
164 super().clean(value)
165 response, value[1] = (value[1] or "").strip().lower(), ""
166 if not settings.CAPTCHA_GET_FROM_POOL: 166 ↛ 168line 166 didn't jump to line 168 because the condition on line 166 was always true
167 CaptchaStore.remove_expired()
168 if settings.CAPTCHA_TEST_MODE and response.lower() == "passed":
169 # automatically pass the test
170 try:
171 # try to delete the captcha based on its hash
172 CaptchaStore.objects.get(hashkey=value[0]).delete()
173 except CaptchaStore.DoesNotExist:
174 # ignore errors
175 pass
176 elif not self.required and not response:
177 pass
178 else:
179 try:
180 CaptchaStore.objects.get(
181 response=response, hashkey=value[0], expiration__gt=timezone.now()
182 ).delete()
183 except CaptchaStore.DoesNotExist:
184 raise ValidationError(
185 getattr(self, "error_messages", {}).get(
186 "invalid", gettext_lazy("Invalid CAPTCHA")
187 )
188 )
189 return value