Skip to content

Commit c9f8622

Browse files
committed
[#2831] Store contactform captcha values in session
1 parent 1914a9b commit c9f8622

File tree

5 files changed

+75
-74
lines changed

5 files changed

+75
-74
lines changed

src/open_inwoner/components/templates/components/Contact/ContactForm.html

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
</p>
3232
<label class="label captcha__label">
3333
<span class="captcha__check">
34-
{{ form_object.captcha.field.question }}
34+
{{ form_object.captcha.field.request_session.captcha_question }}
3535
<span class="captcha__input">{% field_as_widget form_object.captcha "input" form_id %}</span>
3636
</span>
3737
{% if form_object.captcha.errors %}

src/open_inwoner/openklant/forms.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,12 +52,13 @@ class ContactForm(forms.Form):
5252

5353
user: User
5454

55-
def __init__(self, user, *args, **kwargs):
55+
def __init__(self, user, request_session, *args, **kwargs):
5656
super().__init__(*args, **kwargs)
5757
self.user = user
5858

5959
config = OpenKlantConfig.get_solo()
6060
self.fields["subject"].queryset = config.contactformsubject_set.all()
61+
self.fields["captcha"].request_session = request_session
6162

6263
if self.user.is_authenticated:
6364
del self.fields["first_name"]

src/open_inwoner/openklant/views/contactform.py

+46-16
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import secrets
2+
13
from django.conf import settings
24
from django.contrib import messages
35
from django.urls import reverse
@@ -21,9 +23,36 @@
2123
from open_inwoner.utils.views import CommonPageMixin, LogMixin
2224

2325

26+
def generate_question_answer_pair(
27+
range_: tuple[int, int],
28+
operators: list[str],
29+
) -> tuple[str, int]:
30+
lower, upper = range_
31+
num1 = secrets.choice(range(lower, upper))
32+
num2 = secrets.choice(range(lower, upper))
33+
operator = secrets.choice(operators)
34+
35+
# exclude negative results
36+
num1, num2 = max(num1, num2), min(num1, num2)
37+
38+
question = _("What is {num1} {operator} {num2}?").format(
39+
num1=num1, operator=operator, num2=num2
40+
)
41+
42+
match operator:
43+
case "+":
44+
answer = num1 + num2
45+
case "-":
46+
answer = num1 - num2
47+
48+
return question, answer
49+
50+
2451
class ContactFormView(CommonPageMixin, LogMixin, BaseBreadcrumbMixin, FormView):
2552
form_class = ContactForm
26-
template_name = "pages/contactform/form_wrap.html" # inner ("structure") template rendered by CMS plugin
53+
template_name = (
54+
"pages/contactform/form_wrap.html" # inner ("structure") template rendered by CMS plugin
55+
)
2756

2857
@cached_property
2958
def crumbs(self):
@@ -44,7 +73,18 @@ def get_success_url(self):
4473

4574
def get_form_kwargs(self):
4675
kwargs = super().get_form_kwargs()
76+
77+
captcha_question = self.request.session.get("captcha_question")
78+
captcha_answer = self.request.session.get("captcha_answer")
79+
80+
if not captcha_question or not captcha_answer:
81+
captcha_question, captcha_answer = generate_question_answer_pair((1, 10), ["+", "-"])
82+
83+
self.request.session["captcha_question"] = captcha_question
84+
self.request.session["captcha_answer"] = captcha_answer
85+
4786
kwargs["user"] = self.request.user
87+
kwargs["request_session"] = self.request.session
4888
return kwargs
4989

5090
def get_initial(self):
@@ -106,9 +146,7 @@ def form_valid(self, form):
106146
user_email = api_user_email or user_email or form.cleaned_data.get("email")
107147

108148
if send_confirmation:
109-
send_contact_confirmation_mail(
110-
user_email, form.cleaned_data["subject"].subject
111-
)
149+
send_contact_confirmation_mail(user_email, form.cleaned_data["subject"].subject)
112150

113151
self.set_result_message(email_success or api_success)
114152

@@ -133,9 +171,7 @@ def register_by_email(self, form, recipient_email):
133171
success = template.send_email([recipient_email], context)
134172

135173
if success:
136-
self.log_system_action(
137-
"registered contactmoment by email", user=self.request.user
138-
)
174+
self.log_system_action("registered contactmoment by email", user=self.request.user)
139175
return True
140176
else:
141177
self.log_system_action(
@@ -156,9 +192,7 @@ def register_by_api(self, form, config: OpenKlantConfig) -> tuple[bool, str]:
156192
if self.request.user.is_authenticated and (
157193
self.request.user.bsn or self.request.user.kvk
158194
):
159-
klant = klanten_client.retrieve_klant(
160-
**get_fetch_parameters(self.request)
161-
)
195+
klant = klanten_client.retrieve_klant(**get_fetch_parameters(self.request))
162196
if klant:
163197
self.log_system_action(
164198
"retrieved klant for BSN or KVK user", user=self.request.user
@@ -216,9 +250,7 @@ def register_by_api(self, form, config: OpenKlantConfig) -> tuple[bool, str]:
216250
parts = [form.cleaned_data[k] for k in ("first_name", "infix", "last_name")]
217251
full_name = " ".join(p for p in parts if p)
218252

219-
text = _("{text}\n\nNaam: {full_name}").format(
220-
text=text, full_name=full_name
221-
)
253+
text = _("{text}\n\nNaam: {full_name}").format(text=text, full_name=full_name)
222254

223255
self.log_system_action(
224256
"could not retrieve or create klant for user, appended info to message",
@@ -253,9 +285,7 @@ def register_by_api(self, form, config: OpenKlantConfig) -> tuple[bool, str]:
253285
contactmoment = contactmoment_client.create_contactmoment(data, klant=klant)
254286

255287
if contactmoment:
256-
self.log_system_action(
257-
"registered contactmoment by API", user=self.request.user
258-
)
288+
self.log_system_action("registered contactmoment by API", user=self.request.user)
259289
return True, user_email
260290
else:
261291
self.log_system_action(

src/open_inwoner/utils/forms.py

+6-50
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import mimetypes
2-
import secrets
32

43
from django import forms
54
from django.conf import settings
@@ -11,9 +10,7 @@
1110

1211
class ErrorMessageMixin:
1312
default_error_messages = {
14-
"required": _(
15-
"Het verplichte veld {field_name} is niet (goed) ingevuld. Vul het veld in."
16-
)
13+
"required": _("Het verplichte veld {field_name} is niet (goed) ingevuld. Vul het veld in.")
1714
}
1815

1916
def __init__(self, *args, **kwargs):
@@ -30,9 +27,7 @@ def __init__(self, *args, **kwargs):
3027
error_messages = {}
3128
for key in self.default_error_messages.keys():
3229
if key in field_error_messages.keys():
33-
error_message = field_error_messages[key].format(
34-
field_name=f'"{field.label}"'
35-
)
30+
error_message = field_error_messages[key].format(field_name=f'"{field.label}"')
3631
else:
3732
error_message = self.default_error_messages[key].format(
3833
field_name=f'"{field.label}"'
@@ -56,9 +51,7 @@ def get_context(self, name, value, attrs):
5651
"""
5752
context = super().get_context(name, value, attrs)
5853
if self.is_initial(value):
59-
context["url"] = reverse(
60-
self.url_name, kwargs={"uuid": value.instance.uuid}
61-
)
54+
context["url"] = reverse(self.url_name, kwargs={"uuid": value.instance.uuid})
6255
return context
6356

6457

@@ -129,10 +122,7 @@ def allowed_mime_types(self):
129122

130123
@property
131124
def allowed_extensions(self):
132-
return [
133-
mimetypes.guess_extension(mt)[1:]
134-
for mt in self.allowed_mime_types.split(",")
135-
]
125+
return [mimetypes.guess_extension(mt)[1:] for mt in self.allowed_mime_types.split(",")]
136126

137127
def widget_attrs(self, widget):
138128
attrs = super().widget_attrs(widget)
@@ -169,51 +159,17 @@ def clean(self, *args, **kwargs):
169159
class MathCaptchaField(forms.Field):
170160
def __init__(
171161
self,
172-
range_: tuple = (1, 10),
173-
operators: list[str] | None = None,
174162
*args,
175163
**kwargs,
176164
):
177165
super().__init__(*args, **kwargs)
178166
self.widget = forms.NumberInput()
179-
self.range_ = range_
180-
self.operators = operators or ["+", "-"]
181-
self.question, self.answer = self.generate_question_answer_pair(
182-
self.range_, self.operators
183-
)
184-
185-
@staticmethod
186-
def generate_question_answer_pair(
187-
range_: tuple[int, int],
188-
operators: list[str],
189-
) -> tuple[str, int]:
190-
lower, upper = range_
191-
num1 = secrets.choice(range(lower, upper))
192-
num2 = secrets.choice(range(lower, upper))
193-
operator = secrets.choice(operators)
194-
195-
# exclude negative results
196-
num1, num2 = max(num1, num2), min(num1, num2)
197-
198-
question = _("What is {num1} {operator} {num2}?").format(
199-
num1=num1, operator=operator, num2=num2
200-
)
201-
202-
match operator:
203-
case "+":
204-
answer = num1 + num2
205-
case "-":
206-
answer = num1 - num2
207-
208-
return question, answer
209167

210168
def clean(self, value: str) -> str:
211169
if not value:
212170
raise forms.ValidationError(_("Dit veld is vereist."))
213-
if not isinstance(value, str):
214-
raise forms.ValidationError(_("Voer een geheel getal in."))
215-
if value.isspace():
171+
if not isinstance(value, int):
216172
raise forms.ValidationError(_("Voer een geheel getal in."))
217-
if int(value) != self.answer:
173+
if int(value) != self.request_session["captcha_answer"]:
218174
raise forms.ValidationError(_("Fout antwoord, probeer het opnieuw."))
219175
return value

src/open_inwoner/utils/tests/test_form_fields.py

+20-6
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,21 @@
22
from django.test import TestCase
33
from django.utils.translation import gettext as _
44

5+
from open_inwoner.openklant.views.contactform import generate_question_answer_pair
6+
57
from ..forms import MathCaptchaField
68

79

810
class MockForm(forms.Form):
9-
captcha = MathCaptchaField(range_=(4, 5), operators=["+"])
10-
captcha_2 = MathCaptchaField(range_=(4, 5), operators=["-"])
11+
captcha = MathCaptchaField()
12+
13+
def __init__(self, question, answer, *args, **kwargs):
14+
super().__init__(*args, **kwargs)
15+
16+
self.fields["captcha"].request_session = {
17+
"captcha_question": question,
18+
"captcha_answer": answer,
19+
}
1120

1221

1322
class MathCaptchaFieldUnitTest(TestCase):
@@ -24,29 +33,34 @@ def test_captcha_invalid(self):
2433
"reason": "wrong input type",
2534
},
2635
{
27-
"captcha": 42,
36+
"captcha": "42",
2837
"message": _("Voer een geheel getal in."),
2938
"reason": "wrong input type",
3039
},
3140
{
32-
"captcha": "42", # captcha only computes 2 numbers between 1 and 10
41+
"captcha": 42, # captcha only computes 2 numbers between 1 and 10
3342
"message": _("Fout antwoord, probeer het opnieuw."),
3443
"reason": "wrong answer",
3544
},
3645
]
46+
question, answer = generate_question_answer_pair((1, 10), ["+", "-"])
3747
for test_case in test_cases:
3848
with self.subTest(reason=test_case["reason"]):
3949
form = MockForm(
50+
question=question,
51+
answer=answer,
4052
data={
4153
"captcha": test_case["captcha"],
42-
"captcha_2": test_case["captcha"],
4354
},
4455
)
4556
self.assertFalse(form.is_valid())
4657
self.assertEqual(form.errors["captcha"], [test_case["message"]])
4758

4859
def test_captcha_valid(self):
60+
question, answer = generate_question_answer_pair((1, 10), ["+", "-"])
4961
form = MockForm(
50-
data={"captcha": "8", "captcha_2": "0"},
62+
question=question,
63+
answer=answer,
64+
data={"captcha": answer},
5165
)
5266
self.assertTrue(form.is_valid())

0 commit comments

Comments
 (0)