From 1c58c918697709bd43d1167a2a0ac04427eee04c Mon Sep 17 00:00:00 2001 From: Jen Kuenning Date: Thu, 8 Aug 2024 10:15:16 -0400 Subject: [PATCH 1/8] initial commit for turnstile module approach --- concordia/context_processors.py | 29 +++++++ concordia/settings_ecs.py | 5 ++ concordia/settings_template.py | 11 +++ .../forms/widgets/turnstile_widget.html | 2 + concordia/turnstile/LICENSE | 21 +++++ concordia/turnstile/__init__.py | 0 concordia/turnstile/fields.py | 78 +++++++++++++++++++ concordia/turnstile/widgets.py | 30 +++++++ 8 files changed, 176 insertions(+) create mode 100644 concordia/templates/forms/widgets/turnstile_widget.html create mode 100644 concordia/turnstile/LICENSE create mode 100644 concordia/turnstile/__init__.py create mode 100644 concordia/turnstile/fields.py create mode 100644 concordia/turnstile/widgets.py diff --git a/concordia/context_processors.py b/concordia/context_processors.py index 74731351d..d6274bb2c 100644 --- a/concordia/context_processors.py +++ b/concordia/context_processors.py @@ -32,3 +32,32 @@ def site_navigation(request): def maintenance_mode_frontend_available(request): value = cache.get("maintenance_mode_frontend_available", False) return {"maintenance_mode_frontend_available": value} + + +def turnstile_default_settings(request): + """ + Expose turnstile default settings to the default template context + - Cloudflare Turnstile + """ + + return { + "TURN_JS_API_URL": getattr( + settings, + "TURNSTILE_JS_API_URL", + "https://challenges.cloudflare.com/turnstile/v0/api.js", + ), + "TURN_VERIFY_URL": getattr( + settings, + "TURNSTILE_VERIFY_URL", + "https://challenges.cloudflare.com/turnstile/v0/siteverify", + ), + "TURN_SITEKEY": getattr( + settings, "TURNSTILE_SITEKEY", "1x00000000000000000000AA" + ), + "TURN_SECRET": getattr( + settings, "TURNSTILE_SECRET", "1x0000000000000000000000000000000AA" + ), + "TURN_TIMEOUT": getattr(settings, "TURNSTILE_TIMEOUT", 5), + "TURN_DEFAULT_CONFIG": getattr(settings, "TURNSTILE_DEFAULT_CONFIG", {}), + "TURN_PROXIES": getattr(settings, "TURNSTILE_PROXIES", {}), + } diff --git a/concordia/settings_ecs.py b/concordia/settings_ecs.py index abe0b6b6c..8faf99de2 100644 --- a/concordia/settings_ecs.py +++ b/concordia/settings_ecs.py @@ -22,6 +22,11 @@ DATABASES["default"].update({"PASSWORD": postgres_secret["password"]}) + cf_turnstile_secret_json = get_secret("crowd/%s/Turnstile" % ENV_NAME) + cf_turnstile_secret = json.loads(cf_turnstile_secret_json) + TURNSTILE_SITEKEY = cf_turnstile_secret["TurnstileSiteKey"] + TURNSTILE_SECRET = cf_turnstile_secret["TurnstileSecret"] + smtp_secret_json = get_secret("concordia/SMTP") smtp_secret = json.loads(smtp_secret_json) EMAIL_HOST = smtp_secret["Hostname"] diff --git a/concordia/settings_template.py b/concordia/settings_template.py index d5d452939..8a9383593 100644 --- a/concordia/settings_template.py +++ b/concordia/settings_template.py @@ -115,6 +115,7 @@ "exporter", "importer", "captcha", + "turnstile", "prometheus_metrics.apps.PrometheusMetricsConfig", "robots", "django_celery_beat", @@ -161,6 +162,7 @@ "concordia.context_processors.system_configuration", "concordia.context_processors.site_navigation", "concordia.context_processors.maintenance_mode_frontend_available", + "concordia.context_processors.turnstile_default_settings", ], "libraries": { "staticfiles": "django.templatetags.static", @@ -317,6 +319,15 @@ "concordia.authentication_backends.EmailOrUsernameModelBackend" ] +# Turnstile settings +TURN_JS_API_URL = os.environ.get("TURN_JS_API_URL", "") +TURN_VERIFY_URL = os.environ.get("TURN_VERIFY_URL", "") +TURN_SITEKEY = os.environ.get("TURN_SITEKEY", "") +TURN_SECRET = os.environ.get("TURN_SECRET", "") +TURN_TIMEOUT = os.environ.get("TURN_TIMEOUT", "") +TURN_DEFAULT_CONFIG = os.environ.get("TURN_DEFAULT_CONFIG", "") +TURN_PROXIES = os.environ.get("TURN_PROXIES", "") + CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.random_char_challenge" #: Anonymous sessions require captcha validation every day by default: ANONYMOUS_CAPTCHA_VALIDATION_INTERVAL = 86400 diff --git a/concordia/templates/forms/widgets/turnstile_widget.html b/concordia/templates/forms/widgets/turnstile_widget.html new file mode 100644 index 000000000..ac634a870 --- /dev/null +++ b/concordia/templates/forms/widgets/turnstile_widget.html @@ -0,0 +1,2 @@ + +
diff --git a/concordia/turnstile/LICENSE b/concordia/turnstile/LICENSE new file mode 100644 index 000000000..ed371f7ee --- /dev/null +++ b/concordia/turnstile/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2022 Zhang Minghan + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/concordia/turnstile/__init__.py b/concordia/turnstile/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/concordia/turnstile/fields.py b/concordia/turnstile/fields.py new file mode 100644 index 000000000..3903aa9d9 --- /dev/null +++ b/concordia/turnstile/fields.py @@ -0,0 +1,78 @@ +# Originally from +# https://github.com/AndrejZbin/django-hcaptcha/blob/master/hcaptcha/fields.py + +import inspect +import json +from urllib.error import HTTPError +from urllib.parse import urlencode +from urllib.request import ProxyHandler, Request, build_opener + +from django import forms +from django.utils.translation import gettext_lazy as _ +from settings import ( + TURN_DEFAULT_CONFIG, + TURN_PROXIES, + TURN_SECRET, + TURN_TIMEOUT, + TURN_VERIFY_URL, +) +from turnstile.widgets import TurnstileWidget + + +class TurnstileField(forms.Field): + widget = TurnstileWidget + default_error_messages = { + "error_turnstile": _("Turnstile could not be verified."), + "invalid_turnstile": _("Turnstile could not be verified."), + "required": _("Please prove you are a human."), + } + + def __init__(self, **kwargs): + superclass_parameters = inspect.signature(super().__init__).parameters + superclass_kwargs = {} + widget_settings = TURN_DEFAULT_CONFIG.copy() + for key, value in kwargs.items(): + if key in superclass_parameters: + superclass_kwargs[key] = value + else: + widget_settings[key] = value + + widget_url_settings = {} + for prop in filter(lambda p: p in widget_settings, ("onload", "render", "hl")): + widget_url_settings[prop] = widget_settings[prop] + del widget_settings[prop] + self.widget_settings = widget_settings + + super().__init__(**superclass_kwargs) + + self.widget.extra_url = widget_url_settings + + def widget_attrs(self, widget): + attrs = super().widget_attrs(widget) + for key, value in self.widget_settings.items(): + attrs["data-%s" % key] = value + return attrs + + def validate(self, value): + super().validate(value) + opener = build_opener(ProxyHandler(TURN_PROXIES)) + post_data = urlencode( + { + "secret": TURN_SECRET, + "response": value, + } + ).encode() + request = Request(TURN_VERIFY_URL, post_data) + try: + response = opener.open(request, timeout=TURN_TIMEOUT) + except HTTPError as exc: + raise forms.ValidationError( + self.error_messages["error_turnstile"], code="error_turnstile" + ) from exc + + response_data = json.loads(response.read().decode("utf-8")) + + if not response_data.get("success"): + raise forms.ValidationError( + self.error_messages["invalid_turnstile"], code="invalid_turnstile" + ) diff --git a/concordia/turnstile/widgets.py b/concordia/turnstile/widgets.py new file mode 100644 index 000000000..b715fec77 --- /dev/null +++ b/concordia/turnstile/widgets.py @@ -0,0 +1,30 @@ +# Originally from +# https://github.com/AndrejZbin/django-hcaptcha/blob/master/hcaptcha/widgets.py + +from urllib.parse import urlencode + +from django import forms +from settings import TURN_JS_API_URL, TURN_SITEKEY + + +class TurnstileWidget(forms.Widget): + template_name = "forms/widgets/turnstile_widget.html" + + def __init__(self, *args, **kwargs): + self.extra_url = {} + super().__init__(*args, **kwargs) + + def value_from_datadict(self, data, files, name): + return data.get("cf-turnstile-response") + + def build_attrs(self, base_attrs, extra_attrs=None): + attrs = super().build_attrs(base_attrs, extra_attrs) + attrs["data-sitekey"] = TURN_SITEKEY + return attrs + + def get_context(self, name, value, attrs): + context = super().get_context(name, value, attrs) + context["api_url"] = TURN_JS_API_URL + if self.extra_url: + context["api_url"] += "?" + urlencode(self.extra_url) + return context From 614c6644bc42fdb4ee9257c0253ad76cf1953180 Mon Sep 17 00:00:00 2001 From: Jen Kuenning Date: Wed, 14 Aug 2024 10:43:25 -0400 Subject: [PATCH 2/8] start integrating turnstile into concordia forms and views --- concordia/forms.py | 6 ++++++ concordia/settings_template.py | 5 ++--- concordia/turnstile/fields.py | 5 +++-- concordia/turnstile/widgets.py | 3 ++- concordia/views.py | 3 +++ 5 files changed, 16 insertions(+), 6 deletions(-) diff --git a/concordia/forms.py b/concordia/forms.py index 09d470198..06d3122bc 100644 --- a/concordia/forms.py +++ b/concordia/forms.py @@ -12,6 +12,8 @@ from django_registration.forms import RegistrationForm from django_registration.signals import user_activated +from .turnstile.fields import TurnstileField + User = get_user_model() @@ -119,6 +121,10 @@ def __init__(self, *, request, **kwargs): super().__init__(**kwargs) +class TurnstileForm(forms.Form): + turnstile = TurnstileField() + + class ContactUsForm(forms.Form): referrer = forms.CharField( label="Referring Page", widget=forms.HiddenInput(), required=False diff --git a/concordia/settings_template.py b/concordia/settings_template.py index 8a9383593..ecf1fb04b 100644 --- a/concordia/settings_template.py +++ b/concordia/settings_template.py @@ -115,7 +115,6 @@ "exporter", "importer", "captcha", - "turnstile", "prometheus_metrics.apps.PrometheusMetricsConfig", "robots", "django_celery_beat", @@ -325,8 +324,8 @@ TURN_SITEKEY = os.environ.get("TURN_SITEKEY", "") TURN_SECRET = os.environ.get("TURN_SECRET", "") TURN_TIMEOUT = os.environ.get("TURN_TIMEOUT", "") -TURN_DEFAULT_CONFIG = os.environ.get("TURN_DEFAULT_CONFIG", "") -TURN_PROXIES = os.environ.get("TURN_PROXIES", "") +TURN_DEFAULT_CONFIG = os.environ.get("TURN_DEFAULT_CONFIG", {}) +TURN_PROXIES = os.environ.get("TURN_PROXIES", {}) CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.random_char_challenge" #: Anonymous sessions require captcha validation every day by default: diff --git a/concordia/turnstile/fields.py b/concordia/turnstile/fields.py index 3903aa9d9..767e4b40e 100644 --- a/concordia/turnstile/fields.py +++ b/concordia/turnstile/fields.py @@ -9,14 +9,15 @@ from django import forms from django.utils.translation import gettext_lazy as _ -from settings import ( + +from ..settings_template import ( TURN_DEFAULT_CONFIG, TURN_PROXIES, TURN_SECRET, TURN_TIMEOUT, TURN_VERIFY_URL, ) -from turnstile.widgets import TurnstileWidget +from ..turnstile.widgets import TurnstileWidget class TurnstileField(forms.Field): diff --git a/concordia/turnstile/widgets.py b/concordia/turnstile/widgets.py index b715fec77..40ee4d286 100644 --- a/concordia/turnstile/widgets.py +++ b/concordia/turnstile/widgets.py @@ -4,7 +4,8 @@ from urllib.parse import urlencode from django import forms -from settings import TURN_JS_API_URL, TURN_SITEKEY + +from ..settings_template import TURN_JS_API_URL, TURN_SITEKEY class TurnstileWidget(forms.Widget): diff --git a/concordia/views.py b/concordia/views.py index 7287299b6..bf5e21ab2 100644 --- a/concordia/views.py +++ b/concordia/views.py @@ -70,6 +70,7 @@ ActivateAndSetPasswordForm, AllowInactivePasswordResetForm, ContactUsForm, + TurnstileForm, UserLoginForm, UserNameForm, UserProfileForm, @@ -1512,6 +1513,8 @@ def get_context_data(self, **kwargs): ctx["undo_available"] = asset.can_rollback()[0] if transcription else False ctx["redo_available"] = asset.can_rollforward()[0] if transcription else False + ctx["turnstile_form"] = TurnstileForm() + return ctx From 7bc2a9b7fc66ff6e2e65857859894d9af33f85bf Mon Sep 17 00:00:00 2001 From: Jen Kuenning Date: Wed, 14 Aug 2024 12:38:11 -0400 Subject: [PATCH 3/8] Update the url for Originally from reference. --- concordia/turnstile/fields.py | 2 +- concordia/turnstile/widgets.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/concordia/turnstile/fields.py b/concordia/turnstile/fields.py index 767e4b40e..b0b6ff3a6 100644 --- a/concordia/turnstile/fields.py +++ b/concordia/turnstile/fields.py @@ -1,5 +1,5 @@ # Originally from -# https://github.com/AndrejZbin/django-hcaptcha/blob/master/hcaptcha/fields.py +# https://github.com/zmh-program/django-turnstile/blob/main/turnstile/fields.py import inspect import json diff --git a/concordia/turnstile/widgets.py b/concordia/turnstile/widgets.py index 40ee4d286..f8ceaaac2 100644 --- a/concordia/turnstile/widgets.py +++ b/concordia/turnstile/widgets.py @@ -1,5 +1,5 @@ # Originally from -# https://github.com/AndrejZbin/django-hcaptcha/blob/master/hcaptcha/widgets.py +# https://github.com/zmh-program/django-turnstile/blob/main/turnstile/fields.py from urllib.parse import urlencode From 7a88bc7721b4af0efd89b31389ce6cc6c05ba9f2 Mon Sep 17 00:00:00 2001 From: Jen Kuenning Date: Fri, 16 Aug 2024 10:54:13 -0400 Subject: [PATCH 4/8] First attempt at adding form widget to form and view and template. --- concordia/templates/transcriptions/asset_detail.html | 6 +++++- .../templates/transcriptions/asset_detail/editor.html | 1 + concordia/views.py | 8 ++++++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/concordia/templates/transcriptions/asset_detail.html b/concordia/templates/transcriptions/asset_detail.html index d832a69da..4e860545e 100644 --- a/concordia/templates/transcriptions/asset_detail.html +++ b/concordia/templates/transcriptions/asset_detail.html @@ -42,6 +42,7 @@ + @@ -61,7 +62,10 @@ {% block main_content %} {% flag_enabled 'ADVERTISE_ACTIVITY_UI' as ADVERTISE_ACTIVITY_UI %} - +
+
diff --git a/concordia/views.py b/concordia/views.py index bf5e21ab2..1521febf2 100644 --- a/concordia/views.py +++ b/concordia/views.py @@ -1379,6 +1379,14 @@ def dispatch(self, request, *args, **kwargs): ) return redirect(campaign) + def post(self, request, *args, **kwargs): + if request.method == "POST": + form = TurnstileForm(request.POST) + if form.is_valid(): + return self.form_valid(form) + else: + return self.form_valid(form) + def get_queryset(self): asset_qs = Asset.objects.published().filter( item__project__campaign__slug=self.kwargs["campaign_slug"], From 0d0c778ed348cacd736a8f2976b716dad87b42d2 Mon Sep 17 00:00:00 2001 From: Jen Kuenning Date: Mon, 19 Aug 2024 12:39:42 -0400 Subject: [PATCH 5/8] settings is now passing TURN values, moved view to save transcription - still not the right place.... --- concordia/settings_template.py | 18 +++++++++------- .../transcriptions/asset_detail.html | 6 +----- .../transcriptions/asset_detail/editor.html | 2 +- concordia/views.py | 21 ++++++++++++------- 4 files changed, 26 insertions(+), 21 deletions(-) diff --git a/concordia/settings_template.py b/concordia/settings_template.py index ecf1fb04b..5f9781cb4 100644 --- a/concordia/settings_template.py +++ b/concordia/settings_template.py @@ -319,13 +319,17 @@ ] # Turnstile settings -TURN_JS_API_URL = os.environ.get("TURN_JS_API_URL", "") -TURN_VERIFY_URL = os.environ.get("TURN_VERIFY_URL", "") -TURN_SITEKEY = os.environ.get("TURN_SITEKEY", "") -TURN_SECRET = os.environ.get("TURN_SECRET", "") -TURN_TIMEOUT = os.environ.get("TURN_TIMEOUT", "") -TURN_DEFAULT_CONFIG = os.environ.get("TURN_DEFAULT_CONFIG", {}) -TURN_PROXIES = os.environ.get("TURN_PROXIES", {}) +TURN_JS_API_URL = { + "TURNSTILE_JS_API_URL": "https://challenges.cloudflare.com/turnstile/v0/api.js" +} +TURN_VERIFY_URL = { + "TURNSTILE_VERIFY_URL": "https://challenges.cloudflare.com/turnstile/v0/siteverify", +} +TURN_SITEKEY = {"TURNSTILE_SITEKEY": "1x00000000000000000000AA"} +TURN_SECRET = {"TURNSTILE_SECRET": "1x0000000000000000000000000000000AA"} +TURN_TIMEOUT = {"TURNSTILE_TIMEOUT": 5} +TURN_DEFAULT_CONFIG = {"TURNSTILE_DEFAULT_CONFIG": {}} +TURN_PROXIES = {"TURNSTILE_PROXIES": {}} CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.random_char_challenge" #: Anonymous sessions require captcha validation every day by default: diff --git a/concordia/templates/transcriptions/asset_detail.html b/concordia/templates/transcriptions/asset_detail.html index 4e860545e..d832a69da 100644 --- a/concordia/templates/transcriptions/asset_detail.html +++ b/concordia/templates/transcriptions/asset_detail.html @@ -42,7 +42,6 @@ - @@ -62,10 +61,7 @@ {% block main_content %} {% flag_enabled 'ADVERTISE_ACTIVITY_UI' as ADVERTISE_ACTIVITY_UI %} - +
-
+ {{ turnstile_form }} diff --git a/concordia/views.py b/concordia/views.py index 1521febf2..a88a932a4 100644 --- a/concordia/views.py +++ b/concordia/views.py @@ -1379,14 +1379,6 @@ def dispatch(self, request, *args, **kwargs): ) return redirect(campaign) - def post(self, request, *args, **kwargs): - if request.method == "POST": - form = TurnstileForm(request.POST) - if form.is_valid(): - return self.form_valid(form) - else: - return self.form_valid(form) - def get_queryset(self): asset_qs = Asset.objects.published().filter( item__project__campaign__slug=self.kwargs["campaign_slug"], @@ -1725,6 +1717,19 @@ def save_transcription(request, *, asset_pk): else: user = request.user + # Turnstile + if request.method == "POST": + form = TurnstileForm(request.POST) + if form.is_valid(): + return TurnstileForm() + else: + return JsonResponse( + { + "error": "Turnstile form invalid...." + "I don't know what to tell you yet...." + } + ) + # Check whether this transcription text contains any URLs # If so, ask the user to correct the transcription by removing the URLs transcription_text = request.POST["text"] From d4e8de528b0d999d512764e1650995c488ee2b9c Mon Sep 17 00:00:00 2001 From: Jen Kuenning Date: Mon, 19 Aug 2024 13:08:33 -0400 Subject: [PATCH 6/8] clean up - still stuck --- concordia/context_processors.py | 2 +- concordia/settings_template.py | 18 +++++++----------- .../templates/transcriptions/asset_detail.html | 1 + .../transcriptions/asset_detail/editor.html | 1 - 4 files changed, 9 insertions(+), 13 deletions(-) diff --git a/concordia/context_processors.py b/concordia/context_processors.py index d6274bb2c..77074e6a3 100644 --- a/concordia/context_processors.py +++ b/concordia/context_processors.py @@ -52,7 +52,7 @@ def turnstile_default_settings(request): "https://challenges.cloudflare.com/turnstile/v0/siteverify", ), "TURN_SITEKEY": getattr( - settings, "TURNSTILE_SITEKEY", "1x00000000000000000000AA" + settings, "TURNSTILE_SITEKEY", "1x00000000000000000000BB" ), "TURN_SECRET": getattr( settings, "TURNSTILE_SECRET", "1x0000000000000000000000000000000AA" diff --git a/concordia/settings_template.py b/concordia/settings_template.py index 5f9781cb4..ecf1fb04b 100644 --- a/concordia/settings_template.py +++ b/concordia/settings_template.py @@ -319,17 +319,13 @@ ] # Turnstile settings -TURN_JS_API_URL = { - "TURNSTILE_JS_API_URL": "https://challenges.cloudflare.com/turnstile/v0/api.js" -} -TURN_VERIFY_URL = { - "TURNSTILE_VERIFY_URL": "https://challenges.cloudflare.com/turnstile/v0/siteverify", -} -TURN_SITEKEY = {"TURNSTILE_SITEKEY": "1x00000000000000000000AA"} -TURN_SECRET = {"TURNSTILE_SECRET": "1x0000000000000000000000000000000AA"} -TURN_TIMEOUT = {"TURNSTILE_TIMEOUT": 5} -TURN_DEFAULT_CONFIG = {"TURNSTILE_DEFAULT_CONFIG": {}} -TURN_PROXIES = {"TURNSTILE_PROXIES": {}} +TURN_JS_API_URL = os.environ.get("TURN_JS_API_URL", "") +TURN_VERIFY_URL = os.environ.get("TURN_VERIFY_URL", "") +TURN_SITEKEY = os.environ.get("TURN_SITEKEY", "") +TURN_SECRET = os.environ.get("TURN_SECRET", "") +TURN_TIMEOUT = os.environ.get("TURN_TIMEOUT", "") +TURN_DEFAULT_CONFIG = os.environ.get("TURN_DEFAULT_CONFIG", {}) +TURN_PROXIES = os.environ.get("TURN_PROXIES", {}) CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.random_char_challenge" #: Anonymous sessions require captcha validation every day by default: diff --git a/concordia/templates/transcriptions/asset_detail.html b/concordia/templates/transcriptions/asset_detail.html index d832a69da..a64f9b686 100644 --- a/concordia/templates/transcriptions/asset_detail.html +++ b/concordia/templates/transcriptions/asset_detail.html @@ -115,6 +115,7 @@ + {{ turnstile_form }} diff --git a/concordia/templates/transcriptions/asset_detail/editor.html b/concordia/templates/transcriptions/asset_detail/editor.html index 2b5fc3d35..de98041c7 100644 --- a/concordia/templates/transcriptions/asset_detail/editor.html +++ b/concordia/templates/transcriptions/asset_detail/editor.html @@ -86,7 +86,6 @@

- {{ turnstile_form }} From 5e963104651b25a59a85e23eb69bf82cfd6cc8b9 Mon Sep 17 00:00:00 2001 From: joshuastegmaier Date: Thu, 22 Aug 2024 15:40:43 -0400 Subject: [PATCH 7/8] Basic implementation of turnstile to transcription form --- concordia/context_processors.py | 16 ++++----- concordia/settings_template.py | 14 ++++---- .../forms/widgets/turnstile_widget.html | 1 - .../transcriptions/asset_detail.html | 2 +- .../transcriptions/asset_detail/editor.html | 1 + concordia/turnstile/context_processor.py | 5 +++ concordia/turnstile/fields.py | 18 ++++------ concordia/turnstile/widgets.py | 7 ++-- concordia/views.py | 33 ++++++++----------- 9 files changed, 45 insertions(+), 52 deletions(-) create mode 100644 concordia/turnstile/context_processor.py diff --git a/concordia/context_processors.py b/concordia/context_processors.py index 77074e6a3..0899dea8d 100644 --- a/concordia/context_processors.py +++ b/concordia/context_processors.py @@ -41,23 +41,23 @@ def turnstile_default_settings(request): """ return { - "TURN_JS_API_URL": getattr( + "TURNSTILE_JS_API_URL": getattr( settings, - "TURNSTILE_JS_API_URL", + "TURN_JS_API_URL", "https://challenges.cloudflare.com/turnstile/v0/api.js", ), - "TURN_VERIFY_URL": getattr( + "TURNSTILE_VERIFY_URL": getattr( settings, "TURNSTILE_VERIFY_URL", "https://challenges.cloudflare.com/turnstile/v0/siteverify", ), - "TURN_SITEKEY": getattr( + "TURNSTILE_SITEKEY": getattr( settings, "TURNSTILE_SITEKEY", "1x00000000000000000000BB" ), - "TURN_SECRET": getattr( + "TURNSTILE_SECRET": getattr( settings, "TURNSTILE_SECRET", "1x0000000000000000000000000000000AA" ), - "TURN_TIMEOUT": getattr(settings, "TURNSTILE_TIMEOUT", 5), - "TURN_DEFAULT_CONFIG": getattr(settings, "TURNSTILE_DEFAULT_CONFIG", {}), - "TURN_PROXIES": getattr(settings, "TURNSTILE_PROXIES", {}), + "TURNSTILE_TIMEOUT": getattr(settings, "TURNSTILE_TIMEOUT", 5), + "TURNSTILE_DEFAULT_CONFIG": getattr(settings, "TURNSTILE_DEFAULT_CONFIG", {}), + "TURNSTILE_PROXIES": getattr(settings, "TURNSTILE_PROXIES", {}), } diff --git a/concordia/settings_template.py b/concordia/settings_template.py index ecf1fb04b..7ee597cb4 100644 --- a/concordia/settings_template.py +++ b/concordia/settings_template.py @@ -319,13 +319,13 @@ ] # Turnstile settings -TURN_JS_API_URL = os.environ.get("TURN_JS_API_URL", "") -TURN_VERIFY_URL = os.environ.get("TURN_VERIFY_URL", "") -TURN_SITEKEY = os.environ.get("TURN_SITEKEY", "") -TURN_SECRET = os.environ.get("TURN_SECRET", "") -TURN_TIMEOUT = os.environ.get("TURN_TIMEOUT", "") -TURN_DEFAULT_CONFIG = os.environ.get("TURN_DEFAULT_CONFIG", {}) -TURN_PROXIES = os.environ.get("TURN_PROXIES", {}) +TURNSTILE_JS_API_URL = os.environ.get("TURNSTILE_JS_API_URL", "") +TURNSTILE_VERIFY_URL = os.environ.get("TURNSTILE_VERIFY_URL", "") +TURNSTILE_SITEKEY = os.environ.get("TURNSTILE_SITEKEY", "") +TURNSTILE_SECRET = os.environ.get("TURNSTILE_SECRET", "") +TURNSTILE_TIMEOUT = os.environ.get("TURNSTILE_TIMEOUT", 5) +TURNSTILE_DEFAULT_CONFIG = os.environ.get("TURNSTILE_DEFAULT_CONFIG", {}) +TURNSTILE_PROXIES = os.environ.get("TURNSTILE_PROXIES", {}) CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.random_char_challenge" #: Anonymous sessions require captcha validation every day by default: diff --git a/concordia/templates/forms/widgets/turnstile_widget.html b/concordia/templates/forms/widgets/turnstile_widget.html index ac634a870..1dcd92df6 100644 --- a/concordia/templates/forms/widgets/turnstile_widget.html +++ b/concordia/templates/forms/widgets/turnstile_widget.html @@ -1,2 +1 @@ -
diff --git a/concordia/templates/transcriptions/asset_detail.html b/concordia/templates/transcriptions/asset_detail.html index a64f9b686..81a786611 100644 --- a/concordia/templates/transcriptions/asset_detail.html +++ b/concordia/templates/transcriptions/asset_detail.html @@ -41,6 +41,7 @@ + @@ -115,7 +116,6 @@ - {{ turnstile_form }} diff --git a/concordia/templates/transcriptions/asset_detail/editor.html b/concordia/templates/transcriptions/asset_detail/editor.html index de98041c7..56ce9c405 100644 --- a/concordia/templates/transcriptions/asset_detail/editor.html +++ b/concordia/templates/transcriptions/asset_detail/editor.html @@ -118,5 +118,6 @@

{% endspaceless %} + {{ turnstile_form.turnstile }}

diff --git a/concordia/turnstile/context_processor.py b/concordia/turnstile/context_processor.py new file mode 100644 index 000000000..ae091a422 --- /dev/null +++ b/concordia/turnstile/context_processor.py @@ -0,0 +1,5 @@ +from django.conf import settings + + +def turnstile_settings(request): + return {"TURN_JS_API_URL": settings.TURN_JS_API_URL} diff --git a/concordia/turnstile/fields.py b/concordia/turnstile/fields.py index b0b6ff3a6..57e57e179 100644 --- a/concordia/turnstile/fields.py +++ b/concordia/turnstile/fields.py @@ -8,15 +8,9 @@ from urllib.request import ProxyHandler, Request, build_opener from django import forms +from django.conf import settings from django.utils.translation import gettext_lazy as _ -from ..settings_template import ( - TURN_DEFAULT_CONFIG, - TURN_PROXIES, - TURN_SECRET, - TURN_TIMEOUT, - TURN_VERIFY_URL, -) from ..turnstile.widgets import TurnstileWidget @@ -31,7 +25,7 @@ class TurnstileField(forms.Field): def __init__(self, **kwargs): superclass_parameters = inspect.signature(super().__init__).parameters superclass_kwargs = {} - widget_settings = TURN_DEFAULT_CONFIG.copy() + widget_settings = settings.TURNSTILE_DEFAULT_CONFIG.copy() for key, value in kwargs.items(): if key in superclass_parameters: superclass_kwargs[key] = value @@ -56,16 +50,16 @@ def widget_attrs(self, widget): def validate(self, value): super().validate(value) - opener = build_opener(ProxyHandler(TURN_PROXIES)) + opener = build_opener(ProxyHandler(settings.TURNSTILE_PROXIES)) post_data = urlencode( { - "secret": TURN_SECRET, + "secret": settings.TURNSTILE_SECRET, "response": value, } ).encode() - request = Request(TURN_VERIFY_URL, post_data) + request = Request(settings.TURNSTILE_VERIFY_URL, post_data) try: - response = opener.open(request, timeout=TURN_TIMEOUT) + response = opener.open(request, timeout=settings.TURNSTILE_TIMEOUT) except HTTPError as exc: raise forms.ValidationError( self.error_messages["error_turnstile"], code="error_turnstile" diff --git a/concordia/turnstile/widgets.py b/concordia/turnstile/widgets.py index f8ceaaac2..80ddfaa2d 100644 --- a/concordia/turnstile/widgets.py +++ b/concordia/turnstile/widgets.py @@ -4,8 +4,7 @@ from urllib.parse import urlencode from django import forms - -from ..settings_template import TURN_JS_API_URL, TURN_SITEKEY +from django.conf import settings class TurnstileWidget(forms.Widget): @@ -20,12 +19,12 @@ def value_from_datadict(self, data, files, name): def build_attrs(self, base_attrs, extra_attrs=None): attrs = super().build_attrs(base_attrs, extra_attrs) - attrs["data-sitekey"] = TURN_SITEKEY + attrs["data-sitekey"] = settings.TURNSTILE_SITEKEY return attrs def get_context(self, name, value, attrs): context = super().get_context(name, value, attrs) - context["api_url"] = TURN_JS_API_URL + context["api_url"] = settings.TURNSTILE_JS_API_URL if self.extra_url: context["api_url"] += "?" + urlencode(self.extra_url) return context diff --git a/concordia/views.py b/concordia/views.py index a88a932a4..282f516fa 100644 --- a/concordia/views.py +++ b/concordia/views.py @@ -1513,7 +1513,7 @@ def get_context_data(self, **kwargs): ctx["undo_available"] = asset.can_rollback()[0] if transcription else False ctx["redo_available"] = asset.can_rollforward()[0] if transcription else False - ctx["turnstile_form"] = TurnstileForm() + ctx["turnstile_form"] = TurnstileForm(auto_id=False) return ctx @@ -1546,14 +1546,14 @@ def ajax_captcha(request): ) -def validate_anonymous_captcha(view): +def validate_anonymous_user(view): @wraps(view) @never_cache def inner(request, *args, **kwargs): if not request.user.is_authenticated: - captcha_last_validated = request.session.get("captcha_validation_time", 0) - age = time() - captcha_last_validated - if age > settings.ANONYMOUS_CAPTCHA_VALIDATION_INTERVAL: + last_validated = request.session.get("anonymous_validation_time", 0) + age = time() - last_validated + if age > settings.ANONYMOUS_VALIDATION_INTERVAL: return ajax_captcha(request) return view(request, *args, **kwargs) @@ -1583,7 +1583,7 @@ def get_transcription_superseded(asset, supersedes_pk): @require_POST -@validate_anonymous_captcha +@validate_anonymous_user @atomic @ratelimit(key="header:cf-connecting-ip", rate="1/m", block=settings.RATELIMIT_BLOCK) def generate_ocr_transcription(request, *, asset_pk): @@ -1630,7 +1630,7 @@ def generate_ocr_transcription(request, *, asset_pk): @require_POST -@validate_anonymous_captcha +@validate_anonymous_user @atomic @ratelimit(key="header:cf-connecting-ip", rate="1/m", block=settings.RATELIMIT_BLOCK) def rollback_transcription(request, *, asset_pk): @@ -1669,7 +1669,7 @@ def rollback_transcription(request, *, asset_pk): @require_POST -@validate_anonymous_captcha +@validate_anonymous_user @atomic @ratelimit(key="header:cf-connecting-ip", rate="1/m", block=settings.RATELIMIT_BLOCK) def rollforward_transcription(request, *, asset_pk): @@ -1706,7 +1706,6 @@ def rollforward_transcription(request, *, asset_pk): @require_POST -@validate_anonymous_captcha @atomic def save_transcription(request, *, asset_pk): asset = get_object_or_404(Asset, pk=asset_pk) @@ -1714,21 +1713,17 @@ def save_transcription(request, *, asset_pk): if request.user.is_anonymous: user = get_anonymous_user() - else: - user = request.user - - # Turnstile - if request.method == "POST": form = TurnstileForm(request.POST) - if form.is_valid(): - return TurnstileForm() - else: + if not form.is_valid(): return JsonResponse( { "error": "Turnstile form invalid...." "I don't know what to tell you yet...." - } + }, + status=400, ) + else: + user = request.user # Check whether this transcription text contains any URLs # If so, ask the user to correct the transcription by removing the URLs @@ -1783,7 +1778,7 @@ def save_transcription(request, *, asset_pk): @require_POST -@validate_anonymous_captcha +@validate_anonymous_user def submit_transcription(request, *, pk): transcription = get_object_or_404(Transcription, pk=pk) asset = transcription.asset From f00a7e02021e7dacefe391ecabc882ec45a08719 Mon Sep 17 00:00:00 2001 From: joshuastegmaier Date: Tue, 27 Aug 2024 10:06:50 -0400 Subject: [PATCH 8/8] Added turnstile to forms that previously used captcha. Removed captcha from codebase. Updated tests to correctly handle Turnstile. Fixed issue with caching and tests that only appeared when running tests multiple times in the same environment more often than once per hour. Updated docs to include Turnstile info --- Pipfile | 1 - Pipfile.lock | 514 +++++++++--------- concordia/settings_local_test.py | 16 + concordia/settings_template.py | 8 - concordia/settings_test.py | 16 + concordia/static/js/src/contribute.js | 70 +-- concordia/templates/registration/login.html | 2 + .../transcriptions/asset_detail.html | 3 - .../asset_detail/captcha_modal.html | 38 -- .../transcriptions/asset_detail/editor.html | 3 +- concordia/tests/test_views.py | 242 +++++---- concordia/turnstile/fields.py | 3 + concordia/urls.py | 2 - concordia/views.py | 88 +-- docs/for-developers.md | 10 + 15 files changed, 459 insertions(+), 557 deletions(-) delete mode 100644 concordia/templates/transcriptions/asset_detail/captcha_modal.html diff --git a/Pipfile b/Pipfile index b1dd05043..ce21ad20d 100644 --- a/Pipfile +++ b/Pipfile @@ -12,7 +12,6 @@ requests = "*" Django = ">=4.2.14, <5.0" bagit = "*" django-registration = "*" -django-simple-captcha = "*" django-tinymce = "*" elasticsearch = "<7.14.0" django-elasticsearch-dsl = "==7.3" diff --git a/Pipfile.lock b/Pipfile.lock index 424fbecdb..5196f10b6 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "8c4e53c0f15830ef951cd629c0c6da48bcc1e898a363aac041b473893333266b" + "sha256": "e8d305a130abab3890cc7e84fb481eba53ab8868dbefb9f27371b7b3050eaae2" }, "pipfile-spec": 6, "requires": { @@ -81,10 +81,11 @@ }, "automat": { "hashes": [ - "sha256:c3164f8742b9dc440f3682482d32aaff7bb53f71740dd018533f9de286b64180", - "sha256:e56beb84edad19dcc11d30e8d9b895f75deeb5ef5e96b84a467066b3b84bb04e" + "sha256:b34227cf63f6325b8ad2399ede780675083e439b20c323d376373d8ee6306d88", + "sha256:bf029a7bc3da1e2c24da2343e7598affaa9f10bf0ab63ff808566ce90551e02a" ], - "version": "==22.10.0" + "markers": "python_version >= '3.8'", + "version": "==24.8.1" }, "axe-selenium-python": { "hashes": [ @@ -155,19 +156,19 @@ }, "boto3": { "hashes": [ - "sha256:7ca22adef4c77ee128e1e1dc7d48bc9512a87cc6fe3d771b3f913d5ecd41c057", - "sha256:864f06528c583dc7b02adf12db395ecfadbf9cb0da90e907e848ffb27128ce19" + "sha256:2cef3aa476181395c260f4b6e6c5565e5a3022a874fb6b579d8e6b169f94e0b3", + "sha256:5724ddeda8e18c7614c20a09c20159ed87ff7439755cf5e250a1a3feaf9afb7e" ], "index": "pypi", - "version": "==1.34.154" + "version": "==1.35.5" }, "botocore": { "hashes": [ - "sha256:4eef4b1bb809b382ba9dc9c88f5fcc4a133f221a1acb693ee6bee4de9f325979", - "sha256:64d9b4c85a504d77cb56dabb2ad717cd8e1717424a88edb458b01d1e5797262a" + "sha256:3a0086c7124cb3b0d9f98563d00ffd14a942c3f9e731d8d1ccf0d3a1ac7ed884", + "sha256:8116b72c7ae845c195146e437e2afd9d17538a37b3f3548dcf67c12c86ba0742" ], "index": "pypi", - "version": "==1.34.154" + "version": "==1.35.5" }, "brotli": { "hashes": [ @@ -527,10 +528,10 @@ }, "cron-descriptor": { "hashes": [ - "sha256:7b1a00d7d25d6ae6896c0da4457e790b98cba778398a3d48e341e5e0d33f0488", - "sha256:a67ba21804983b1427ed7f3e1ec27ee77bf24c652b0430239c268c5ddfbf9dc0" + "sha256:736b3ae9d1a99bc3dbfc5b55b5e6e7c12031e7ba5de716625772f8b02dcd6013", + "sha256:f51ce4ffc1d1f2816939add8524f206c376a42c87a5fca3091ce26725b3b1bca" ], - "version": "==1.4.3" + "version": "==1.4.5" }, "cryptography": { "hashes": [ @@ -614,10 +615,11 @@ }, "django-celery-beat": { "hashes": [ - "sha256:f75b2d129731f1214be8383e18fae6bfeacdb55dffb2116ce849222c0106f9ad" + "sha256:8482034925e09b698c05ad61c36ed2a8dbc436724a3fe119215193a4ca6dc967", + "sha256:851c680d8fbf608ca5fecd5836622beea89fa017bc2b3f94a5b8c648c32d84b1" ], "index": "pypi", - "version": "==2.6.0" + "version": "==2.7.0" }, "django-debug-toolbar": { "hashes": [ @@ -658,12 +660,6 @@ "index": "pypi", "version": "==1.0.0" }, - "django-ranged-response": { - "hashes": [ - "sha256:f71fff352a37316b9bead717fc76e4ddd6c9b99c4680cdf4783b9755af1cf985" - ], - "version": "==0.2.0" - }, "django-ratelimit": { "hashes": [ "sha256:555943b283045b917ad59f196829530d63be2a39adb72788d985b90c81ba808b", @@ -696,14 +692,6 @@ "index": "pypi", "version": "==6.1" }, - "django-simple-captcha": { - "hashes": [ - "sha256:3ae9a7e650cb0cdbcfd4a75aa91fdf25dcc523ef541a7b1f004bd4357798fc03", - "sha256:d188516d326fadd2d5ad076eb89649d55c02cabafe3fdcc2154ac18e9f6d4b97" - ], - "index": "pypi", - "version": "==0.6.0" - }, "django-storages": { "hashes": [ "sha256:69aca94d26e6714d14ad63f33d13619e697508ee33ede184e462ed766dc2a73f", @@ -717,7 +705,7 @@ "sha256:3232e7ecde66ba4464abb6f9e6b8cc739b914efb9b29dc2cf2eee451f7cc2acb", "sha256:aa6f4965838484317b7f08d22c0d91a53d64e7bbbd34264468ae83d4023898a7" ], - "markers": "python_version >= '3.8' and python_version < '4.0'", + "markers": "python_version >= '3.8' and python_version < '4'", "version": "==7.0" }, "django-tinymce": { @@ -1040,11 +1028,11 @@ }, "gunicorn": { "hashes": [ - "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9", - "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63" + "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d", + "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec" ], "index": "pypi", - "version": "==22.0.0" + "version": "==23.0.0" }, "h11": { "hashes": [ @@ -1194,11 +1182,11 @@ }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" + "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", + "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" ], "markers": "python_version >= '3.5'", - "version": "==3.7" + "version": "==3.8" }, "incremental": { "hashes": [ @@ -1250,19 +1238,19 @@ }, "locust": { "hashes": [ - "sha256:20756509939004e95c622ac3042886edab38b736f00534cc03ce2774064e7f71", - "sha256:d26b7333cdef80645f3978d8ff9aabab4d53e41ed82cc8490212aa68e8498fdd" + "sha256:1f056173aefa6ba42501c3bf04bb700df9eddd165e38bb721f7b00643b68b825", + "sha256:566d89b5c4a7b69e3ab6844c1a373909918ee9d04f5d2bab6a8104e43a5721d8" ], "index": "pypi", - "version": "==2.31.1" + "version": "==2.31.4" }, "markdown": { "hashes": [ - "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f", - "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224" + "sha256:2ae2471477cfd02dbbf038d5d9bc226d40def84b4fe2986e49b59b6b472bbed2", + "sha256:7eb6df5690b81a1d7942992c97fad2938e956e79df20cbc6186e9c3a77b1c803" ], "index": "pypi", - "version": "==3.6" + "version": "==3.7" }, "markdown-it-py": { "hashes": [ @@ -1348,11 +1336,11 @@ }, "more-itertools": { "hashes": [ - "sha256:e5d93ef411224fbcef366a6e8ddc4c5781bc6359d43412a65dd5964e46111463", - "sha256:ea6a02e24a9161e51faad17a8782b92a0df82c12c1c8886fec7f0c3fa1a1b320" + "sha256:0f7d9f83a0a8dcfa8a2694a770590d98a67ea943e3d9f5298309a484758c4e27", + "sha256:fe0e63c4ab068eac62410ab05cccca2dc71ec44ba8ef29916a0090df061cf923" ], "index": "pypi", - "version": "==10.3.0" + "version": "==10.4.0" }, "msgpack": { "hashes": [ @@ -1848,11 +1836,11 @@ }, "pytesseract": { "hashes": [ - "sha256:8f22cc98f765bf13517ead0c70effedb46c153540d25783e04014f28b55a5fc6", - "sha256:f1c3a8b0f07fd01a1085d451f5b8315be6eec1d5577a6796d46dc7a62bd4120f" + "sha256:4bf5f880c99406f52a3cfc2633e42d9dc67615e69d8a509d74867d3baddb5db9", + "sha256:7a99c6c2ac598360693d83a416e36e0b33a67638bb9d77fdcac094a3589d4b34" ], "index": "pypi", - "version": "==0.3.10" + "version": "==0.3.13" }, "pytest": { "hashes": [ @@ -1894,118 +1882,118 @@ }, "pyzmq": { "hashes": [ - "sha256:038ae4ffb63e3991f386e7fda85a9baab7d6617fe85b74a8f9cab190d73adb2b", - "sha256:05bacc4f94af468cc82808ae3293390278d5f3375bb20fef21e2034bb9a505b6", - "sha256:0614aed6f87d550b5cecb03d795f4ddbb1544b78d02a4bd5eecf644ec98a39f6", - "sha256:08f74904cb066e1178c1ec706dfdb5c6c680cd7a8ed9efebeac923d84c1f13b1", - "sha256:093a1a3cae2496233f14b57f4b485da01b4ff764582c854c0f42c6dd2be37f3d", - "sha256:0a1f6ea5b1d6cdbb8cfa0536f0d470f12b4b41ad83625012e575f0e3ecfe97f0", - "sha256:0e6cea102ffa16b737d11932c426f1dc14b5938cf7bc12e17269559c458ac334", - "sha256:263cf1e36862310bf5becfbc488e18d5d698941858860c5a8c079d1511b3b18e", - "sha256:28a8b2abb76042f5fd7bd720f7fea48c0fd3e82e9de0a1bf2c0de3812ce44a42", - "sha256:2ae7c57e22ad881af78075e0cea10a4c778e67234adc65c404391b417a4dda83", - "sha256:2cd0f4d314f4a2518e8970b6f299ae18cff7c44d4a1fc06fc713f791c3a9e3ea", - "sha256:2fa76ebcebe555cce90f16246edc3ad83ab65bb7b3d4ce408cf6bc67740c4f88", - "sha256:314d11564c00b77f6224d12eb3ddebe926c301e86b648a1835c5b28176c83eab", - "sha256:347e84fc88cc4cb646597f6d3a7ea0998f887ee8dc31c08587e9c3fd7b5ccef3", - "sha256:359c533bedc62c56415a1f5fcfd8279bc93453afdb0803307375ecf81c962402", - "sha256:393daac1bcf81b2a23e696b7b638eedc965e9e3d2112961a072b6cd8179ad2eb", - "sha256:3b3b8e36fd4c32c0825b4461372949ecd1585d326802b1321f8b6dc1d7e9318c", - "sha256:3c397b1b450f749a7e974d74c06d69bd22dd362142f370ef2bd32a684d6b480c", - "sha256:3d3146b1c3dcc8a1539e7cc094700b2be1e605a76f7c8f0979b6d3bde5ad4072", - "sha256:3ee647d84b83509b7271457bb428cc347037f437ead4b0b6e43b5eba35fec0aa", - "sha256:416ac51cabd54f587995c2b05421324700b22e98d3d0aa2cfaec985524d16f1d", - "sha256:451e16ae8bea3d95649317b463c9f95cd9022641ec884e3d63fc67841ae86dfe", - "sha256:45cb1a70eb00405ce3893041099655265fabcd9c4e1e50c330026e82257892c1", - "sha256:46d6800b45015f96b9d92ece229d92f2aef137d82906577d55fadeb9cf5fcb71", - "sha256:471312a7375571857a089342beccc1a63584315188560c7c0da7e0a23afd8a5c", - "sha256:471880c4c14e5a056a96cd224f5e71211997d40b4bf5e9fdded55dafab1f98f2", - "sha256:5384c527a9a004445c5074f1e20db83086c8ff1682a626676229aafd9cf9f7d1", - "sha256:57bb2acba798dc3740e913ffadd56b1fcef96f111e66f09e2a8db3050f1f12c8", - "sha256:58c33dc0e185dd97a9ac0288b3188d1be12b756eda67490e6ed6a75cf9491d79", - "sha256:59d0acd2976e1064f1b398a00e2c3e77ed0a157529779e23087d4c2fb8aaa416", - "sha256:5a6ed52f0b9bf8dcc64cc82cce0607a3dfed1dbb7e8c6f282adfccc7be9781de", - "sha256:5bc2431167adc50ba42ea3e5e5f5cd70d93e18ab7b2f95e724dd8e1bd2c38120", - "sha256:5cca7b4adb86d7470e0fc96037771981d740f0b4cb99776d5cb59cd0e6684a73", - "sha256:61dfa5ee9d7df297c859ac82b1226d8fefaf9c5113dc25c2c00ecad6feeeb04f", - "sha256:63c1d3a65acb2f9c92dce03c4e1758cc552f1ae5c78d79a44e3bb88d2fa71f3a", - "sha256:65c6e03cc0222eaf6aad57ff4ecc0a070451e23232bb48db4322cc45602cede0", - "sha256:67976d12ebfd61a3bc7d77b71a9589b4d61d0422282596cf58c62c3866916544", - "sha256:68a0a1d83d33d8367ddddb3e6bb4afbb0f92bd1dac2c72cd5e5ddc86bdafd3eb", - "sha256:6c5aeea71f018ebd3b9115c7cb13863dd850e98ca6b9258509de1246461a7e7f", - "sha256:754c99a9840839375ee251b38ac5964c0f369306eddb56804a073b6efdc0cd88", - "sha256:75a95c2358fcfdef3374cb8baf57f1064d73246d55e41683aaffb6cfe6862917", - "sha256:7688653574392d2eaeef75ddcd0b2de5b232d8730af29af56c5adf1df9ef8d6f", - "sha256:77ce6a332c7e362cb59b63f5edf730e83590d0ab4e59c2aa5bd79419a42e3449", - "sha256:7907419d150b19962138ecec81a17d4892ea440c184949dc29b358bc730caf69", - "sha256:79e45a4096ec8388cdeb04a9fa5e9371583bcb826964d55b8b66cbffe7b33c86", - "sha256:7bcbfbab4e1895d58ab7da1b5ce9a327764f0366911ba5b95406c9104bceacb0", - "sha256:80b0c9942430d731c786545da6be96d824a41a51742e3e374fedd9018ea43106", - "sha256:8b88641384e84a258b740801cd4dbc45c75f148ee674bec3149999adda4a8598", - "sha256:8d4dac7d97f15c653a5fedcafa82626bd6cee1450ccdaf84ffed7ea14f2b07a4", - "sha256:8d906d43e1592be4b25a587b7d96527cb67277542a5611e8ea9e996182fae410", - "sha256:8efb782f5a6c450589dbab4cb0f66f3a9026286333fe8f3a084399149af52f29", - "sha256:906e532c814e1d579138177a00ae835cd6becbf104d45ed9093a3aaf658f6a6a", - "sha256:90d4feb2e83dfe9ace6374a847e98ee9d1246ebadcc0cb765482e272c34e5820", - "sha256:911c43a4117915203c4cc8755e0f888e16c4676a82f61caee2f21b0c00e5b894", - "sha256:91d1a20bdaf3b25f3173ff44e54b1cfbc05f94c9e8133314eb2962a89e05d6e3", - "sha256:94c4262626424683feea0f3c34951d39d49d354722db2745c42aa6bb50ecd93b", - "sha256:96d7c1d35ee4a495df56c50c83df7af1c9688cce2e9e0edffdbf50889c167595", - "sha256:9869fa984c8670c8ab899a719eb7b516860a29bc26300a84d24d8c1b71eae3ec", - "sha256:98c03bd7f3339ff47de7ea9ac94a2b34580a8d4df69b50128bb6669e1191a895", - "sha256:995301f6740a421afc863a713fe62c0aaf564708d4aa057dfdf0f0f56525294b", - "sha256:998444debc8816b5d8d15f966e42751032d0f4c55300c48cc337f2b3e4f17d03", - "sha256:9a6847c92d9851b59b9f33f968c68e9e441f9a0f8fc972c5580c5cd7cbc6ee24", - "sha256:9bdfcb74b469b592972ed881bad57d22e2c0acc89f5e8c146782d0d90fb9f4bf", - "sha256:9f136a6e964830230912f75b5a116a21fe8e34128dcfd82285aa0ef07cb2c7bd", - "sha256:a0f0ab9df66eb34d58205913f4540e2ad17a175b05d81b0b7197bc57d000e829", - "sha256:a4b7a989c8f5a72ab1b2bbfa58105578753ae77b71ba33e7383a31ff75a504c4", - "sha256:a7b8aab50e5a288c9724d260feae25eda69582be84e97c012c80e1a5e7e03fb2", - "sha256:ad875277844cfaeca7fe299ddf8c8d8bfe271c3dc1caf14d454faa5cdbf2fa7a", - "sha256:add52c78a12196bc0fda2de087ba6c876ea677cbda2e3eba63546b26e8bf177b", - "sha256:b10163e586cc609f5f85c9b233195554d77b1e9a0801388907441aaeb22841c5", - "sha256:b24079a14c9596846bf7516fe75d1e2188d4a528364494859106a33d8b48be38", - "sha256:b281b5ff5fcc9dcbfe941ac5c7fcd4b6c065adad12d850f95c9d6f23c2652384", - "sha256:b3bb34bebaa1b78e562931a1687ff663d298013f78f972a534f36c523311a84d", - "sha256:b45e6445ac95ecb7d728604bae6538f40ccf4449b132b5428c09918523abc96d", - "sha256:ba0a31d00e8616149a5ab440d058ec2da621e05d744914774c4dde6837e1f545", - "sha256:baba2fd199b098c5544ef2536b2499d2e2155392973ad32687024bd8572a7d1c", - "sha256:bd13f0231f4788db619347b971ca5f319c5b7ebee151afc7c14632068c6261d3", - "sha256:bd3f6329340cef1c7ba9611bd038f2d523cea79f09f9c8f6b0553caba59ec562", - "sha256:bdeb2c61611293f64ac1073f4bf6723b67d291905308a7de9bb2ca87464e3273", - "sha256:bef24d3e4ae2c985034439f449e3f9e06bf579974ce0e53d8a507a1577d5b2ab", - "sha256:c0665d85535192098420428c779361b8823d3d7ec4848c6af3abb93bc5c915bf", - "sha256:c5668dac86a869349828db5fc928ee3f58d450dce2c85607067d581f745e4fb1", - "sha256:c9b9305004d7e4e6a824f4f19b6d8f32b3578aad6f19fc1122aaf320cbe3dc83", - "sha256:ccb42ca0a4a46232d716779421bbebbcad23c08d37c980f02cc3a6bd115ad277", - "sha256:ce6f2b66799971cbae5d6547acefa7231458289e0ad481d0be0740535da38d8b", - "sha256:d36b8fffe8b248a1b961c86fbdfa0129dfce878731d169ede7fa2631447331be", - "sha256:d3dd5523ed258ad58fed7e364c92a9360d1af8a9371e0822bd0146bdf017ef4c", - "sha256:d416f2088ac8f12daacffbc2e8918ef4d6be8568e9d7155c83b7cebed49d2322", - "sha256:d4fafc2eb5d83f4647331267808c7e0c5722c25a729a614dc2b90479cafa78bd", - "sha256:d5c8b17f6e8f29138678834cf8518049e740385eb2dbf736e8f07fc6587ec682", - "sha256:d9270fbf038bf34ffca4855bcda6e082e2c7f906b9eb8d9a8ce82691166060f7", - "sha256:dcc37d9d708784726fafc9c5e1232de655a009dbf97946f117aefa38d5985a0f", - "sha256:ddbb2b386128d8eca92bd9ca74e80f73fe263bcca7aa419f5b4cbc1661e19741", - "sha256:e1e5d0a25aea8b691a00d6b54b28ac514c8cc0d8646d05f7ca6cb64b97358250", - "sha256:e5c88b2f13bcf55fee78ea83567b9fe079ba1a4bef8b35c376043440040f7edb", - "sha256:e7eca8b89e56fb8c6c26dd3e09bd41b24789022acf1cf13358e96f1cafd8cae3", - "sha256:e8746ce968be22a8a1801bf4a23e565f9687088580c3ed07af5846580dd97f76", - "sha256:ec7248673ffc7104b54e4957cee38b2f3075a13442348c8d651777bf41aa45ee", - "sha256:ecb6c88d7946166d783a635efc89f9a1ff11c33d680a20df9657b6902a1d133b", - "sha256:ef3b048822dca6d231d8a8ba21069844ae38f5d83889b9b690bf17d2acc7d099", - "sha256:f133d05aaf623519f45e16ab77526e1e70d4e1308e084c2fb4cedb1a0c764bbb", - "sha256:f3292d384537b9918010769b82ab3e79fca8b23d74f56fc69a679106a3e2c2cf", - "sha256:f774841bb0e8588505002962c02da420bcfb4c5056e87a139c6e45e745c0e2e2", - "sha256:f9499c70c19ff0fbe1007043acb5ad15c1dec7d8e84ab429bca8c87138e8f85c", - "sha256:f99de52b8fbdb2a8f5301ae5fc0f9e6b3ba30d1d5fc0421956967edcc6914242", - "sha256:fa25a620eed2a419acc2cf10135b995f8f0ce78ad00534d729aa761e4adcef8a", - "sha256:fbf558551cf415586e91160d69ca6416f3fce0b86175b64e4293644a7416b81b", - "sha256:fc82269d24860cfa859b676d18850cbb8e312dcd7eada09e7d5b007e2f3d9eb1", - "sha256:ff832cce719edd11266ca32bc74a626b814fff236824aa1aeaad399b69fe6eae" + "sha256:007137c9ac9ad5ea21e6ad97d3489af654381324d5d3ba614c323f60dab8fae6", + "sha256:034da5fc55d9f8da09015d368f519478a52675e558c989bfcb5cf6d4e16a7d2a", + "sha256:05590cdbc6b902101d0e65d6a4780af14dc22914cc6ab995d99b85af45362cc9", + "sha256:070672c258581c8e4f640b5159297580a9974b026043bd4ab0470be9ed324f1f", + "sha256:0aca98bc423eb7d153214b2df397c6421ba6373d3397b26c057af3c904452e37", + "sha256:0bed0e799e6120b9c32756203fb9dfe8ca2fb8467fed830c34c877e25638c3fc", + "sha256:0d987a3ae5a71c6226b203cfd298720e0086c7fe7c74f35fa8edddfbd6597eed", + "sha256:0eaa83fc4c1e271c24eaf8fb083cbccef8fde77ec8cd45f3c35a9a123e6da097", + "sha256:160c7e0a5eb178011e72892f99f918c04a131f36056d10d9c1afb223fc952c2d", + "sha256:17bf5a931c7f6618023cdacc7081f3f266aecb68ca692adac015c383a134ca52", + "sha256:17c412bad2eb9468e876f556eb4ee910e62d721d2c7a53c7fa31e643d35352e6", + "sha256:18c8dc3b7468d8b4bdf60ce9d7141897da103c7a4690157b32b60acb45e333e6", + "sha256:1a534f43bc738181aa7cbbaf48e3eca62c76453a40a746ab95d4b27b1111a7d2", + "sha256:1c17211bc037c7d88e85ed8b7d8f7e52db6dc8eca5590d162717c654550f7282", + "sha256:1f3496d76b89d9429a656293744ceca4d2ac2a10ae59b84c1da9b5165f429ad3", + "sha256:1fcc03fa4997c447dce58264e93b5aa2d57714fbe0f06c07b7785ae131512732", + "sha256:226af7dcb51fdb0109f0016449b357e182ea0ceb6b47dfb5999d569e5db161d5", + "sha256:23f4aad749d13698f3f7b64aad34f5fc02d6f20f05999eebc96b89b01262fb18", + "sha256:25bf2374a2a8433633c65ccb9553350d5e17e60c8eb4de4d92cc6bd60f01d306", + "sha256:28ad5233e9c3b52d76196c696e362508959741e1a005fb8fa03b51aea156088f", + "sha256:28c812d9757fe8acecc910c9ac9dafd2ce968c00f9e619db09e9f8f54c3a68a3", + "sha256:29c6a4635eef69d68a00321e12a7d2559fe2dfccfa8efae3ffb8e91cd0b36a8b", + "sha256:29c7947c594e105cb9e6c466bace8532dc1ca02d498684128b339799f5248277", + "sha256:2a50625acdc7801bc6f74698c5c583a491c61d73c6b7ea4dee3901bb99adb27a", + "sha256:2ae90ff9dad33a1cfe947d2c40cb9cb5e600d759ac4f0fd22616ce6540f72797", + "sha256:2c4a71d5d6e7b28a47a394c0471b7e77a0661e2d651e7ae91e0cab0a587859ca", + "sha256:2ea4ad4e6a12e454de05f2949d4beddb52460f3de7c8b9d5c46fbb7d7222e02c", + "sha256:2eb7735ee73ca1b0d71e0e67c3739c689067f055c764f73aac4cc8ecf958ee3f", + "sha256:31507f7b47cc1ead1f6e86927f8ebb196a0bab043f6345ce070f412a59bf87b5", + "sha256:35cffef589bcdc587d06f9149f8d5e9e8859920a071df5a2671de2213bef592a", + "sha256:367b4f689786fca726ef7a6c5ba606958b145b9340a5e4808132cc65759abd44", + "sha256:39887ac397ff35b7b775db7201095fc6310a35fdbae85bac4523f7eb3b840e20", + "sha256:3a495b30fc91db2db25120df5847d9833af237546fd59170701acd816ccc01c4", + "sha256:3b55a4229ce5da9497dd0452b914556ae58e96a4381bb6f59f1305dfd7e53fc8", + "sha256:402b190912935d3db15b03e8f7485812db350d271b284ded2b80d2e5704be780", + "sha256:43a47408ac52647dfabbc66a25b05b6a61700b5165807e3fbd40063fcaf46386", + "sha256:4661c88db4a9e0f958c8abc2b97472e23061f0bc737f6f6179d7a27024e1faa5", + "sha256:46a446c212e58456b23af260f3d9fb785054f3e3653dbf7279d8f2b5546b21c2", + "sha256:470d4a4f6d48fb34e92d768b4e8a5cc3780db0d69107abf1cd7ff734b9766eb0", + "sha256:49d34ab71db5a9c292a7644ce74190b1dd5a3475612eefb1f8be1d6961441971", + "sha256:4d29ab8592b6ad12ebbf92ac2ed2bedcfd1cec192d8e559e2e099f648570e19b", + "sha256:4d80b1dd99c1942f74ed608ddb38b181b87476c6a966a88a950c7dee118fdf50", + "sha256:4da04c48873a6abdd71811c5e163bd656ee1b957971db7f35140a2d573f6949c", + "sha256:4f78c88905461a9203eac9faac157a2a0dbba84a0fd09fd29315db27be40af9f", + "sha256:4ff9dc6bc1664bb9eec25cd17506ef6672d506115095411e237d571e92a58231", + "sha256:5506f06d7dc6ecf1efacb4a013b1f05071bb24b76350832c96449f4a2d95091c", + "sha256:55cf66647e49d4621a7e20c8d13511ef1fe1efbbccf670811864452487007e08", + "sha256:5a509df7d0a83a4b178d0f937ef14286659225ef4e8812e05580776c70e155d5", + "sha256:5c2b3bfd4b9689919db068ac6c9911f3fcb231c39f7dd30e3138be94896d18e6", + "sha256:6835dd60355593de10350394242b5757fbbd88b25287314316f266e24c61d073", + "sha256:689c5d781014956a4a6de61d74ba97b23547e431e9e7d64f27d4922ba96e9d6e", + "sha256:6a96179a24b14fa6428cbfc08641c779a53f8fcec43644030328f44034c7f1f4", + "sha256:6ace4f71f1900a548f48407fc9be59c6ba9d9aaf658c2eea6cf2779e72f9f317", + "sha256:6b274e0762c33c7471f1a7471d1a2085b1a35eba5cdc48d2ae319f28b6fc4de3", + "sha256:706e794564bec25819d21a41c31d4df2d48e1cc4b061e8d345d7fb4dd3e94072", + "sha256:70fc7fcf0410d16ebdda9b26cbd8bf8d803d220a7f3522e060a69a9c87bf7bad", + "sha256:7133d0a1677aec369d67dd78520d3fa96dd7f3dcec99d66c1762870e5ea1a50a", + "sha256:7445be39143a8aa4faec43b076e06944b8f9d0701b669df4af200531b21e40bb", + "sha256:76589c020680778f06b7e0b193f4b6dd66d470234a16e1df90329f5e14a171cd", + "sha256:76589f2cd6b77b5bdea4fca5992dc1c23389d68b18ccc26a53680ba2dc80ff2f", + "sha256:77eb0968da535cba0470a5165468b2cac7772cfb569977cff92e240f57e31bef", + "sha256:794a4562dcb374f7dbbfb3f51d28fb40123b5a2abadee7b4091f93054909add5", + "sha256:7ad1bc8d1b7a18497dda9600b12dc193c577beb391beae5cd2349184db40f187", + "sha256:7f98f6dfa8b8ccaf39163ce872bddacca38f6a67289116c8937a02e30bbe9711", + "sha256:8423c1877d72c041f2c263b1ec6e34360448decfb323fa8b94e85883043ef988", + "sha256:8685fa9c25ff00f550c1fec650430c4b71e4e48e8d852f7ddcf2e48308038640", + "sha256:878206a45202247781472a2d99df12a176fef806ca175799e1c6ad263510d57c", + "sha256:89289a5ee32ef6c439086184529ae060c741334b8970a6855ec0b6ad3ff28764", + "sha256:8ab5cad923cc95c87bffee098a27856c859bd5d0af31bd346035aa816b081fe1", + "sha256:8b435f2753621cd36e7c1762156815e21c985c72b19135dac43a7f4f31d28dd1", + "sha256:8be4700cd8bb02cc454f630dcdf7cfa99de96788b80c51b60fe2fe1dac480289", + "sha256:8c997098cc65e3208eca09303630e84d42718620e83b733d0fd69543a9cab9cb", + "sha256:8ea039387c10202ce304af74def5021e9adc6297067f3441d348d2b633e8166a", + "sha256:8f7e66c7113c684c2b3f1c83cdd3376103ee0ce4c49ff80a648643e57fb22218", + "sha256:90412f2db8c02a3864cbfc67db0e3dcdbda336acf1c469526d3e869394fe001c", + "sha256:92a78853d7280bffb93df0a4a6a2498cba10ee793cc8076ef797ef2f74d107cf", + "sha256:989d842dc06dc59feea09e58c74ca3e1678c812a4a8a2a419046d711031f69c7", + "sha256:9cb3a6460cdea8fe8194a76de8895707e61ded10ad0be97188cc8463ffa7e3a8", + "sha256:9dd8cd1aeb00775f527ec60022004d030ddc51d783d056e3e23e74e623e33726", + "sha256:9ed69074a610fad1c2fda66180e7b2edd4d31c53f2d1872bc2d1211563904cd9", + "sha256:9edda2df81daa129b25a39b86cb57dfdfe16f7ec15b42b19bfac503360d27a93", + "sha256:a2224fa4a4c2ee872886ed00a571f5e967c85e078e8e8c2530a2fb01b3309b88", + "sha256:a4f96f0d88accc3dbe4a9025f785ba830f968e21e3e2c6321ccdfc9aef755115", + "sha256:aedd5dd8692635813368e558a05266b995d3d020b23e49581ddd5bbe197a8ab6", + "sha256:aee22939bb6075e7afededabad1a56a905da0b3c4e3e0c45e75810ebe3a52672", + "sha256:b1d464cb8d72bfc1a3adc53305a63a8e0cac6bc8c5a07e8ca190ab8d3faa43c2", + "sha256:b8f86dd868d41bea9a5f873ee13bf5551c94cf6bc51baebc6f85075971fe6eea", + "sha256:bc6bee759a6bddea5db78d7dcd609397449cb2d2d6587f48f3ca613b19410cfc", + "sha256:bea2acdd8ea4275e1278350ced63da0b166421928276c7c8e3f9729d7402a57b", + "sha256:bfa832bfa540e5b5c27dcf5de5d82ebc431b82c453a43d141afb1e5d2de025fa", + "sha256:c0e6091b157d48cbe37bd67233318dbb53e1e6327d6fc3bb284afd585d141003", + "sha256:c3789bd5768ab5618ebf09cef6ec2b35fed88709b104351748a63045f0ff9797", + "sha256:c530e1eecd036ecc83c3407f77bb86feb79916d4a33d11394b8234f3bd35b940", + "sha256:c811cfcd6a9bf680236c40c6f617187515269ab2912f3d7e8c0174898e2519db", + "sha256:c92d73464b886931308ccc45b2744e5968cbaade0b1d6aeb40d8ab537765f5bc", + "sha256:cccba051221b916a4f5e538997c45d7d136a5646442b1231b916d0164067ea27", + "sha256:cdeabcff45d1c219636ee2e54d852262e5c2e085d6cb476d938aee8d921356b3", + "sha256:ced65e5a985398827cc9276b93ef6dfabe0273c23de8c7931339d7e141c2818e", + "sha256:d049df610ac811dcffdc147153b414147428567fbbc8be43bb8885f04db39d98", + "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b", + "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629", + "sha256:ded0fc7d90fe93ae0b18059930086c51e640cdd3baebdc783a695c77f123dcd9", + "sha256:e3e0210287329272539eea617830a6a28161fbbd8a3271bf4150ae3e58c5d0e6", + "sha256:e6fa2e3e683f34aea77de8112f6483803c96a44fd726d7358b9888ae5bb394ec", + "sha256:ea0eb6af8a17fa272f7b98d7bebfab7836a0d62738e16ba380f440fceca2d951", + "sha256:ea7f69de383cb47522c9c208aec6dd17697db7875a4674c4af3f8cfdac0bdeae", + "sha256:eac5174677da084abf378739dbf4ad245661635f1600edd1221f150b165343f4", + "sha256:fc4f7a173a5609631bb0c42c23d12c49df3966f89f496a51d3eb0ec81f4519d6", + "sha256:fdb5b3e311d4d4b0eb8b3e8b4d1b0a512713ad7e6a68791d0923d1aec433d919" ], "markers": "python_version >= '3.7'", - "version": "==26.1.0" + "version": "==26.2.0" }, "redis": { "hashes": [ @@ -2025,10 +2013,10 @@ }, "rich": { "hashes": [ - "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222", - "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432" + "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc", + "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4" ], - "version": "==13.7.1" + "version": "==13.8.0" }, "s3transfer": { "hashes": [ @@ -2048,11 +2036,11 @@ }, "sentry-sdk": { "hashes": [ - "sha256:7a8d5163d2ba5c5f4464628c6b68f85e86972f7c636acc78aed45c61b98b7a5e", - "sha256:8763840497b817d44c49b3fe3f5f7388d083f2337ffedf008b2cdb63b5c86dc6" + "sha256:6beede8fc2ab4043da7f69d95534e320944690680dd9a963178a49de71d726c6", + "sha256:8d4a576f7a98eb2fdb40e13106e41f330e5c79d72a68be1316e7852cf4995260" ], "index": "pypi", - "version": "==2.12.0" + "version": "==2.13.0" }, "service-identity": { "hashes": [ @@ -2063,11 +2051,11 @@ }, "setuptools": { "hashes": [ - "sha256:5a03e1860cf56bb6ef48ce186b0e557fdba433237481a9a625176c2831be15d1", - "sha256:8d243eff56d095e5817f796ede6ae32941278f542e0f941867cc05ae52b162ec" + "sha256:b208925fcb9f7af924ed2dc04708ea89791e24bde0d3020b27df0e116088b34e", + "sha256:d59a3e788ab7e012ab2c4baed1b376da6366883ee20d7a5fc426816e3d7b1193" ], "markers": "python_version >= '3.8'", - "version": "==72.1.0" + "version": "==73.0.1" }, "setuptools-scm": { "hashes": [ @@ -2109,11 +2097,11 @@ }, "soupsieve": { "hashes": [ - "sha256:5663d5a7b3bfaeee0bc4372e7fc48f9cff4940b3eec54a6451cc5299f1097690", - "sha256:eaa337ff55a1579b6549dc679565eac1e3d000563bcb1c8ab0d0fefbc0c2cdc7" + "sha256:e2e68417777af359ec65daac1057404a3c8a5455bb8abc36f1a9866ab1a51abb", + "sha256:e72c4ff06e4fb6e4b5a9f0f55fe6e81514581fca1515028625d0f299c602ccc9" ], "markers": "python_version >= '3.8'", - "version": "==2.5" + "version": "==2.6" }, "sqlparse": { "hashes": [ @@ -2148,11 +2136,11 @@ }, "trio": { "hashes": [ - "sha256:6d2fe7ee656146d598ec75128ff4a2386576801b42b691f4a91cc2c18508544a", - "sha256:998bbdc5797621e1976c86820b1bc341cc66b51d2618a31cc8720ddd7df8affe" + "sha256:0346c3852c15e5c7d40ea15972c4805689ef2cb8b5206f794c9c19450119f3a4", + "sha256:c5237e8133eb0a1d72f09a971a55c28ebe69e351c783fc64bc37db8db8bbe1d0" ], "markers": "python_version >= '3.8'", - "version": "==0.26.1" + "version": "==0.26.2" }, "trio-websocket": { "hashes": [ @@ -2165,7 +2153,6 @@ "twisted": { "extras": [ "http2", - "tls", "tls" ], "hashes": [ @@ -2173,7 +2160,6 @@ "sha256:734832ef98108136e222b5230075b1079dad8a3fc5637319615619a7725b0c81" ], "index": "pypi", - "markers": "python_full_version >= '3.8.0'", "version": "==24.7.0" }, "txaio": { @@ -2259,11 +2245,11 @@ }, "werkzeug": { "hashes": [ - "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18", - "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8" + "sha256:02c9eb92b7d6c06f31a782811505d2157837cea66aaede3e217c7c27c039476c", + "sha256:34f2371506b250df4d4f84bfe7b0921e4762525762bbd936614909fe25cd7306" ], "markers": "python_version >= '3.8'", - "version": "==3.0.3" + "version": "==3.0.4" }, "whitenoise": { "hashes": [ @@ -2291,43 +2277,37 @@ }, "zope-interface": { "hashes": [ - "sha256:03bd5c0db82237bbc47833a8b25f1cc090646e212f86b601903d79d7e6b37031", - "sha256:03f1452d5d1f279184d5bdb663a3dc39902d9320eceb63276240791e849054b6", - "sha256:10ebac566dd0cec66f942dc759d46a994a2b3ba7179420f0e2130f88f8a5f400", - "sha256:192b7a792e3145ed880ff6b1a206fdb783697cfdb4915083bfca7065ec845e60", - "sha256:19c829d52e921b9fe0b2c0c6a8f9a2508c49678ee1be598f87d143335b6a35dc", - "sha256:3f3495462bc0438b76536a0e10d765b168ae636092082531b88340dc40dcd118", - "sha256:3f52050c6a10d4a039ec6f2c58e5b3ade5cc570d16cf9d102711e6b8413c90e6", - "sha256:400d06c9ec8dbcc96f56e79376297e7be07a315605c9a2208720da263d44d76f", - "sha256:4ec212037becf6d2f705b7ed4538d56980b1e7bba237df0d8995cbbed29961dc", - "sha256:51d5713e8e38f2d3ec26e0dfdca398ed0c20abda2eb49ffc15a15a23eb8e5f6d", - "sha256:52f5253cca1b35eaeefa51abd366b87f48f8714097c99b131ba61f3fdbbb58e7", - "sha256:5566fd9271c89ad03d81b0831c37d46ae5e2ed211122c998637130159a120cf1", - "sha256:55bbcc74dc0c7ab489c315c28b61d7a1d03cf938cc99cc58092eb065f120c3a5", - "sha256:696c2a381fc7876b3056711717dba5eddd07c2c9e5ccd50da54029a1293b6e43", - "sha256:6ba4b3638d014918b918aa90a9c8370bd74a03abf8fcf9deb353b3a461a59a84", - "sha256:7039e624bcb820f77cc2ff3d1adcce531932990eee16121077eb51d9c76b6c14", - "sha256:88d108d004e0df25224de77ce349a7e73494ea2cb194031f7c9687e68a88ec9b", - "sha256:8c1dff87b30fd150c61367d0e2cdc49bb55f8b9fd2a303560bbc24b951573ae1", - "sha256:9a8195b99e650e6f329ce4e5eb22d448bdfef0406404080812bc96e2a05674cb", - "sha256:af0b33f04677b57843d529b9257a475d2865403300b48c67654c40abac2f9f24", - "sha256:b419f2144e1762ab845f20316f1df36b15431f2622ebae8a6d5f7e8e712b413c", - "sha256:b59deb0ddc7b431e41d720c00f99d68b52cb9bd1d5605a085dc18f502fe9c47f", - "sha256:bc0615351221926a36a0fbcb2520fb52e0b23e8c22a43754d9cb8f21358c33c0", - "sha256:c203d82069ba31e1f3bc7ba530b2461ec86366cd4bfc9b95ec6ce58b1b559c34", - "sha256:ce6cbb852fb8f2f9bb7b9cdca44e2e37bce783b5f4c167ff82cb5f5128163c8f", - "sha256:d33cb526efdc235a2531433fc1287fcb80d807d5b401f9b801b78bf22df560dd", - "sha256:da0cef4d7e3f19c3bd1d71658d6900321af0492fee36ec01b550a10924cffb9c", - "sha256:da21e7eec49252df34d426c2ee9cf0361c923026d37c24728b0fa4cc0599fd03", - "sha256:ea8d51e5eb29e57d34744369cd08267637aa5a0fefc9b5d33775ab7ff2ebf2e3", - "sha256:ec4e87e6fdc511a535254daa122c20e11959ce043b4e3425494b237692a34f1c", - "sha256:f0f5fda7cbf890371a59ab1d06512da4f2c89a6ea194e595808123c863c38eff", - "sha256:f32ca483e6ade23c7caaee9d5ee5d550cf4146e9b68d2fb6c68bac183aa41c37", - "sha256:f749ca804648d00eda62fe1098f229b082dfca930d8bad8386e572a6eafa7525", - "sha256:f89a420cf5a6f2aa7849dd59e1ff0e477f562d97cf8d6a1ee03461e1eec39887" + "sha256:0821efcbdeaf48e12c66b0d19a1f9edec2ed22697ab8885d322c8f82fe5bc892", + "sha256:08d86319fd7542984d4c0ef7865759dab58616154cb237a5a1ce758687255de0", + "sha256:093ab9a2c5105d826755c43a76770b69353dbe95ec27a0b5e88ab4f63d7744b8", + "sha256:13aacff95c59000ecd562d9717a87eca8211f6bc74bea6b8ca68e742d1f8f13d", + "sha256:185ef3a7a01fac1151622579a08995aab66590711c1a4f9b605f88129229dba1", + "sha256:22c93492e5d2f09100a4a23cf709b20f0305cdbbad14f9af2f6e9311742bed8e", + "sha256:28f29dd42819d99682e46a8d3cc2ee60461a77554d4320e0e8a37363f04208e0", + "sha256:3246cccb9e4ce34c9b32ad55a53098043af5e7185623bf5de8e6ec5d8e71415e", + "sha256:356a9c1c8cfece776f54806157057be759d812168395762f47f046b40901e974", + "sha256:3ab142cebe69e0f72bf892da040af97f61fd03c09a23ae2fc7de3ab576c5d4cd", + "sha256:4b671f943d6487d6f1a6bbdce3faffae35e4f74f98ac9b865d2b7370cb6b0bd3", + "sha256:4f5e39373952e1d689476b6e43d779553b165ce332d0fde9c36d9b095f28d052", + "sha256:738de1c72390a2caf543247013f617ed15d272e4c19731a998e81dd5a2379f1c", + "sha256:797510df26b82cf619a894dac4ff4036d11f6340bec0287c89cecb0b1b1c429e", + "sha256:7c47a5068df03f0c9215d3525b166c9d8d4f6d03cbe4e60339818f8c393e3e3e", + "sha256:8eab70e404c2416176b4630914cda275ca95678529e54e66ea45d1a0be422994", + "sha256:91bb6b5e1a158b751e12458d5618c1af42eb3dc8472b87a613d543d9fb7660e0", + "sha256:9da2fb807a20cd4fe381e23e2f906f0a0f4acece6d9abac65d5fc0a1f8383ed8", + "sha256:b76f6048c1a334e26e5d46fdb4f327d9e7e6b348ad607ee9fdce9c7325b5a635", + "sha256:b9d865209cc9795d0f9f4f63b87a86e7a9e032d3cbbb10b1c13bf27343a4fc54", + "sha256:bceaf7ee95735b0d6ac3f5bba0209d056e686999732dc32bd463a53d4488ccdb", + "sha256:bf2746931a6f83370fdc4005dbea4e39e3a3d0333da42897040698c1ff282e9c", + "sha256:d9ab785a7af39c6968385a9d39b712d2263661fa3780bd38efec0cefdbb84036", + "sha256:dd28ba1e2deb0c339881ee7755db649433347bdf3c4f3d885f029fcf10aacdf7", + "sha256:deac72b653817a68b96079c1428ae84860c76f653af03668a02f97b74f8a465b", + "sha256:e299f672cfad3392b097af885a552a51e60d3b44e8572f1401e87f863f8986b4", + "sha256:f1146bb27a411d0d40cc0e88182a6b0e979d68ab526c8e5ae9e27c06506ed017", + "sha256:f77d58cfc3af86d062b8cfa7194db74ca78a615d66bbd23b251bad1b1ecf9818" ], "markers": "python_version >= '3.8'", - "version": "==7.0.1" + "version": "==7.0.2" }, "zope.event": { "hashes": [ @@ -2580,60 +2560,62 @@ }, "pyyaml": { "hashes": [ - "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5", - "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc", - "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df", - "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741", - "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206", - "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27", - "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595", - "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62", - "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98", - "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696", - "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290", - "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9", - "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d", - "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6", - "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867", - "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47", - "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486", - "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6", - "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3", - "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007", - "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938", - "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0", - "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c", - "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735", - "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d", - "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28", - "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4", - "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba", - "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8", - "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef", - "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5", - "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd", - "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3", - "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0", - "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515", - "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c", - "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c", - "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924", - "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34", - "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43", - "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859", - "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673", - "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54", - "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a", - "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b", - "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab", - "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa", - "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c", - "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585", - "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d", - "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f" + "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff", + "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", + "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", + "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e", + "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", + "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", + "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", + "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", + "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", + "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", + "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a", + "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", + "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", + "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8", + "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", + "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19", + "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", + "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a", + "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", + "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", + "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", + "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631", + "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d", + "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", + "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", + "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", + "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", + "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", + "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", + "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706", + "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", + "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", + "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", + "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083", + "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", + "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", + "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", + "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f", + "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725", + "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", + "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", + "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", + "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", + "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", + "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5", + "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d", + "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290", + "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", + "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", + "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", + "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", + "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12", + "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4" ], - "markers": "python_version >= '3.6'", - "version": "==6.0.1" + "markers": "python_version >= '3.8'", + "version": "==6.0.2" }, "sqlparse": { "hashes": [ diff --git a/concordia/settings_local_test.py b/concordia/settings_local_test.py index b683d3c25..3224052dc 100644 --- a/concordia/settings_local_test.py +++ b/concordia/settings_local_test.py @@ -1,3 +1,5 @@ +import os + from .settings_template import * # NOQA ignore=F405 from .settings_template import DATABASES @@ -21,3 +23,17 @@ "CONFIG": {"hosts": [("localhost", 63791)]}, } } + +# Turnstile settings +TURNSTILE_JS_API_URL = os.environ.get( + "TURNSTILE_JS_API_URL", "https://challenges.cloudflare.com/turnstile/v0/api.js" +) +TURNSTILE_VERIFY_URL = os.environ.get( + "TURNSTILE_VERIFY_URL", "https://challenges.cloudflare.com/turnstile/v0/siteverify" +) +TURNSTILE_SITEKEY = os.environ.get( + "TURNSTILE_SITEKEY", "1x00000000000000000000BB" +) # Always pass, invisible +TURNSTILE_SECRET = os.environ.get( + "TURNSTILE_SECRET", "1x0000000000000000000000000000000AA" +) # Always pass diff --git a/concordia/settings_template.py b/concordia/settings_template.py index 7ee597cb4..784bf0c9c 100644 --- a/concordia/settings_template.py +++ b/concordia/settings_template.py @@ -114,7 +114,6 @@ "concordia.apps.ConcordiaAppConfig", "exporter", "importer", - "captcha", "prometheus_metrics.apps.PrometheusMetricsConfig", "robots", "django_celery_beat", @@ -327,13 +326,6 @@ TURNSTILE_DEFAULT_CONFIG = os.environ.get("TURNSTILE_DEFAULT_CONFIG", {}) TURNSTILE_PROXIES = os.environ.get("TURNSTILE_PROXIES", {}) -CAPTCHA_CHALLENGE_FUNCT = "captcha.helpers.random_char_challenge" -#: Anonymous sessions require captcha validation every day by default: -ANONYMOUS_CAPTCHA_VALIDATION_INTERVAL = 86400 - -CAPTCHA_IMAGE_SIZE = [150, 100] -CAPTCHA_FONT_SIZE = 40 - STORAGES = { "default": { "BACKEND": "django.core.files.storage.FileSystemStorage", diff --git a/concordia/settings_test.py b/concordia/settings_test.py index b49eed049..56876b8e9 100644 --- a/concordia/settings_test.py +++ b/concordia/settings_test.py @@ -1,3 +1,5 @@ +import os + from .settings_template import * # NOQA ignore=F405 from .settings_template import DATABASES @@ -21,3 +23,17 @@ "CONFIG": {"hosts": [("localhost", 6379)]}, } } + +# Turnstile settings +TURNSTILE_JS_API_URL = os.environ.get( + "TURNSTILE_JS_API_URL", "https://challenges.cloudflare.com/turnstile/v0/api.js" +) +TURNSTILE_VERIFY_URL = os.environ.get( + "TURNSTILE_VERIFY_URL", "https://challenges.cloudflare.com/turnstile/v0/siteverify" +) +TURNSTILE_SITEKEY = os.environ.get( + "TURNSTILE_SITEKEY", "1x00000000000000000000BB" +) # Always pass, invisible +TURNSTILE_SECRET = os.environ.get( + "TURNSTILE_SECRET", "1x0000000000000000000000000000000AA" +) # Always pass diff --git a/concordia/static/js/src/contribute.js b/concordia/static/js/src/contribute.js index 45f39b6e5..ff8f6d0f1 100644 --- a/concordia/static/js/src/contribute.js +++ b/concordia/static/js/src/contribute.js @@ -60,40 +60,6 @@ $(document).on('keydown', function (event) { }); function setupPage() { - var $captchaModal = $('#captcha-modal'); - var $triggeringCaptchaForm = false; - var $captchaForm = $captchaModal - .find('form') - .on('submit', function (event) { - event.preventDefault(); - - var formData = $captchaForm.serializeArray(); - - $.ajax({ - url: $captchaForm.attr('action'), - method: 'POST', - dataType: 'json', - data: $.param(formData), - }) - .done(function () { - $captchaModal.modal('hide'); - if ($triggeringCaptchaForm) { - $triggeringCaptchaForm.submit(); - } - $triggeringCaptchaForm = false; - }) - .fail(function (jqXHR) { - if (jqXHR.status == 401) { - $captchaModal - .find('[name=key]') - .val(jqXHR.responseJSON.key); - $captchaModal - .find('#captcha-image') - .attr('src', jqXHR.responseJSON.image); - } - }); - }); - $('form.ajax-submission').each(function (index, formElement) { /* Generic AJAX submission logic which takes a form and POSTs its data to the @@ -147,27 +113,16 @@ function setupPage() { } }) .fail(function (jqXHR, textStatus, errorThrown) { - if (jqXHR.status == 401) { - $captchaModal - .find('[name=key]') - .val(jqXHR.responseJSON.key); - $captchaModal - .find('#captcha-image') - .attr('src', jqXHR.responseJSON.image); - $triggeringCaptchaForm = $form; - $captchaModal.modal(); - } else { - $form.trigger('form-submit-failure', { - textStatus: textStatus, - errorThrown: errorThrown, - requestData: formData, - $form: $form, - jqXHR: jqXHR, - }); - unlockControls($form); - if (eventData.lockElement) { - unlockControls($(eventData.lockElement)); - } + $form.trigger('form-submit-failure', { + textStatus: textStatus, + errorThrown: errorThrown, + requestData: formData, + $form: $form, + jqXHR: jqXHR, + }); + unlockControls($form); + if (eventData.lockElement) { + unlockControls($(eventData.lockElement)); } }); @@ -515,6 +470,11 @@ function setupPage() { url: url, method: 'POST', dataType: 'json', + data: { + 'cf-turnstile-response': $transcriptionEditor + .find('input[name="cf-turnstile-response"]') + .val(), + }, }) .done(function (responseData) { displayMessage( diff --git a/concordia/templates/registration/login.html b/concordia/templates/registration/login.html index 26af4419d..19e493958 100644 --- a/concordia/templates/registration/login.html +++ b/concordia/templates/registration/login.html @@ -5,6 +5,7 @@ {% block head_content %} {{ block.super }} + {% endblock head_content %} {% block title %}Login{% endblock title %} @@ -23,6 +24,7 @@

Welcome back!

{% endif %} {% bootstrap_form form %} +
{{ turnstile_form.turnstile }}

By using this system, you agree to comply with the Library's diff --git a/concordia/templates/transcriptions/asset_detail.html b/concordia/templates/transcriptions/asset_detail.html index 81a786611..86cb202f9 100644 --- a/concordia/templates/transcriptions/asset_detail.html +++ b/concordia/templates/transcriptions/asset_detail.html @@ -116,9 +116,6 @@

- diff --git a/concordia/templates/transcriptions/asset_detail/captcha_modal.html b/concordia/templates/transcriptions/asset_detail/captcha_modal.html deleted file mode 100644 index 2ca5c457d..000000000 --- a/concordia/templates/transcriptions/asset_detail/captcha_modal.html +++ /dev/null @@ -1,38 +0,0 @@ - diff --git a/concordia/templates/transcriptions/asset_detail/editor.html b/concordia/templates/transcriptions/asset_detail/editor.html index 56ce9c405..b9e0baa40 100644 --- a/concordia/templates/transcriptions/asset_detail/editor.html +++ b/concordia/templates/transcriptions/asset_detail/editor.html @@ -86,6 +86,8 @@

+
{{ turnstile_form.turnstile }}
+ @@ -118,6 +120,5 @@

{% endspaceless %} - {{ turnstile_form.turnstile }} diff --git a/concordia/tests/test_views.py b/concordia/tests/test_views.py index ea912bef1..8636807a9 100644 --- a/concordia/tests/test_views.py +++ b/concordia/tests/test_views.py @@ -1,9 +1,10 @@ from datetime import date, timedelta from unittest.mock import patch -from captcha.models import CaptchaStore +from django import forms from django.conf import settings from django.contrib.auth.models import User +from django.core.cache import cache from django.http import HttpResponse, JsonResponse from django.test import ( Client, @@ -31,7 +32,6 @@ from concordia.views import ( AccountProfileView, CompletedCampaignListView, - ConcordiaLoginView, ratelimit_view, registration_rate, ) @@ -113,6 +113,12 @@ class ConcordiaViewTests(CreateTestUsers, JSONAssertMixin, TestCase): This class contains the unit tests for the view in the concordia app. """ + def setUp(self): + cache.clear() + + def tearDown(self): + cache.clear() + def test_ratelimit_view(self): c = Client() response = c.get("/error/429/") @@ -477,27 +483,6 @@ def test_campaign_report(self): RATELIMIT_ENABLE=False, SESSION_ENGINE="django.contrib.sessions.backends.cache" ) class TransactionalViewTests(CreateTestUsers, JSONAssertMixin, TransactionTestCase): - def completeCaptcha(self, key=None): - """Submit a CAPTCHA response using the provided challenge key""" - - if key is None: - challenge_data = self.assertValidJSON( - self.client.get(reverse("ajax-captcha")), expected_status=401 - ) - self.assertIn("key", challenge_data) - self.assertIn("image", challenge_data) - key = challenge_data["key"] - - self.assertValidJSON( - self.client.post( - reverse("ajax-captcha"), - data={ - "key": key, - "response": CaptchaStore.objects.get(hashkey=key).response, - }, - ) - ) - def test_asset_reservation(self): """ Test the basic Asset reservation process @@ -749,74 +734,67 @@ def test_asset_reservation_tombstone_expiration(self): self.assertEqual(reservation.reservation_token, data["reservation_token"]) self.assertEqual(reservation.tombstoned, False) - def test_anonymous_transcription_save_captcha(self): - asset = create_asset() - - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} - ) - data = self.assertValidJSON(resp, expected_status=401) - self.assertIn("key", data) - self.assertIn("image", data) - - self.completeCaptcha(data["key"]) - - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} - ) - data = self.assertValidJSON(resp, expected_status=201) - def test_transcription_save(self): asset = create_asset() - # We're not testing the CAPTCHA here so we'll complete it: - self.completeCaptcha() + with patch("concordia.turnstile.fields.TurnstileField.validate") as mock: + mock.side_effect = forms.ValidationError( + "Testing error", code="invalid_turnstile" + ) + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} + ) + data = self.assertValidJSON(resp, expected_status=401) + self.assertIn("error", data) - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} - ) - data = self.assertValidJSON(resp, expected_status=201) - self.assertIn("submissionUrl", data) + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} + ) + data = self.assertValidJSON(resp, expected_status=201) + self.assertIn("submissionUrl", data) - # Test attempts to create a second transcription without marking that it - # supersedes the previous one: - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} - ) - data = self.assertValidJSON(resp, expected_status=409) - self.assertIn("error", data) + # Test attempts to create a second transcription without marking that it + # supersedes the previous one: + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} + ) + data = self.assertValidJSON(resp, expected_status=409) + self.assertIn("error", data) - # This should work with the chain specified: - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), - data={"text": "test", "supersedes": asset.transcription_set.get().pk}, - ) - data = self.assertValidJSON(resp, expected_status=201) - self.assertIn("submissionUrl", data) + # This should work with the chain specified: + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), + data={"text": "test", "supersedes": asset.transcription_set.get().pk}, + ) + data = self.assertValidJSON(resp, expected_status=201) + self.assertIn("submissionUrl", data) - # We should see an error if you attempt to supersede a transcription - # which has already been superseded: - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), - data={ - "text": "test", - "supersedes": asset.transcription_set.order_by("pk").first().pk, - }, - ) - data = self.assertValidJSON(resp, expected_status=409) - self.assertIn("error", data) + # We should see an error if you attempt to supersede a transcription + # which has already been superseded: + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), + data={ + "text": "test", + "supersedes": asset.transcription_set.order_by("pk").first().pk, + }, + ) + data = self.assertValidJSON(resp, expected_status=409) + self.assertIn("error", data) - # A logged in user can take over from an anonymous user: - self.login_user() - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), - data={ - "text": "test", - "supersedes": asset.transcription_set.order_by("pk").last().pk, - }, - ) - data = self.assertValidJSON(resp, expected_status=201) - self.assertIn("submissionUrl", data) + # A logged in user can take over from an anonymous user: + self.login_user() + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), + data={ + "text": "test", + "supersedes": asset.transcription_set.order_by("pk").last().pk, + }, + ) + data = self.assertValidJSON(resp, expected_status=201) + self.assertIn("submissionUrl", data) def test_anonymous_transcription_submission(self): asset = create_asset() @@ -826,36 +804,48 @@ def test_anonymous_transcription_submission(self): transcription.full_clean() transcription.save() - resp = self.client.post( - reverse("submit-transcription", args=(transcription.pk,)) - ) + with patch("concordia.turnstile.fields.TurnstileField.validate") as mock: + mock.side_effect = forms.ValidationError( + "Testing error", code="invalid_turnstile" + ) + resp = self.client.post( + reverse("submit-transcription", args=(transcription.pk,)) + ) data = self.assertValidJSON(resp, expected_status=401) - self.assertIn("key", data) - self.assertIn("image", data) + self.assertIn("error", data) self.assertFalse(Transcription.objects.filter(submitted__isnull=False).exists()) - self.completeCaptcha(data["key"]) - self.client.post(reverse("submit-transcription", args=(transcription.pk,))) - self.assertTrue(Transcription.objects.filter(submitted__isnull=False).exists()) + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + self.client.post( + reverse("submit-transcription", args=(transcription.pk,)), + ) + self.assertTrue( + Transcription.objects.filter(submitted__isnull=False).exists() + ) def test_transcription_submission(self): asset = create_asset() - # We're not testing the CAPTCHA here so we'll complete it: - self.completeCaptcha() - - resp = self.client.post( - reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} - ) + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + resp = self.client.post( + reverse("save-transcription", args=(asset.pk,)), data={"text": "test"} + ) data = self.assertValidJSON(resp, expected_status=201) transcription = Transcription.objects.get() self.assertIsNone(transcription.submitted) - resp = self.client.post( - reverse("submit-transcription", args=(transcription.pk,)) - ) + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + resp = self.client.post( + reverse("submit-transcription", args=(transcription.pk,)) + ) data = self.assertValidJSON(resp, expected_status=200) self.assertIn("id", data) self.assertEqual(data["id"], transcription.pk) @@ -866,9 +856,6 @@ def test_transcription_submission(self): def test_stale_transcription_submission(self): asset = create_asset() - # We're not testing the CAPTCHA here so we'll complete it: - self.completeCaptcha() - anon = get_anonymous_user() t1 = Transcription(asset=asset, user=anon, text="test") @@ -879,9 +866,12 @@ def test_stale_transcription_submission(self): t2.full_clean() t2.save() - resp = self.client.post(reverse("submit-transcription", args=(t1.pk,))) - data = self.assertValidJSON(resp, expected_status=400) - self.assertIn("error", data) + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + resp = self.client.post(reverse("submit-transcription", args=(t1.pk,))) + data = self.assertValidJSON(resp, expected_status=400) + self.assertIn("error", data) def test_transcription_review(self): asset = create_asset() @@ -1538,18 +1528,34 @@ def test_ratelimit_view(self): self.assertNotEqual(response["Retry-After"], 0) -class CaptchaTests(TestCase): +class LoginTests(TestCase, CreateTestUsers): def setUp(self): - self.request_factory = RequestFactory() + self.user = self.create_user("test-user") def test_ConcordiaLoginView(self): - request = self.request_factory.post("/") - request.session = {} - view = setup_view(ConcordiaLoginView(), request) - response = view.post(request) - self.assertNotContains(response, "captcha") - - request.limited = True - view = setup_view(ConcordiaLoginView(), request) - response = view.post(request) - self.assertContains(response, "captcha") + with patch("concordia.turnstile.fields.TurnstileField.validate") as mock: + mock.side_effect = forms.ValidationError( + "Testing error", code="invalid_turnstile" + ) + response = self.client.post( + reverse("registration_login"), + data={"username": self.user.username, "password": self.user._password}, + ) + self.assertIn("user", response.context) + self.assertFalse(response.context["user"].is_authenticated) + + with patch( + "concordia.turnstile.fields.TurnstileField.validate", return_value=True + ): + response = self.client.post( + reverse("registration_login"), + data={"username": self.user.username, "password": self.user._password}, + follow=True, + ) + self.assertRedirects( + response, + expected_url=reverse("homepage"), + target_status_code=200, + ) + self.assertIn("user", response.context) + self.assertTrue(response.context["user"].is_authenticated) diff --git a/concordia/turnstile/fields.py b/concordia/turnstile/fields.py index 57e57e179..24eb5201c 100644 --- a/concordia/turnstile/fields.py +++ b/concordia/turnstile/fields.py @@ -50,6 +50,7 @@ def widget_attrs(self, widget): def validate(self, value): super().validate(value) + opener = build_opener(ProxyHandler(settings.TURNSTILE_PROXIES)) post_data = urlencode( { @@ -57,7 +58,9 @@ def validate(self, value): "response": value, } ).encode() + request = Request(settings.TURNSTILE_VERIFY_URL, post_data) + try: response = opener.open(request, timeout=settings.TURNSTILE_TIMEOUT) except HTTPError as exc: diff --git a/concordia/urls.py b/concordia/urls.py index 10f746a52..941c8a8b7 100644 --- a/concordia/urls.py +++ b/concordia/urls.py @@ -263,8 +263,6 @@ ".well-known/change-password", # https://wicg.github.io/change-password-url/ RedirectView.as_view(pattern_name="password_change"), ), - path("captcha/ajax/", views.ajax_captcha, name="ajax-captcha"), - path("captcha/", include("captcha.urls")), path("admin/", admin.site.urls), # Internal support assists: path("error/500/", server_error), diff --git a/concordia/views.py b/concordia/views.py index 282f516fa..531ec291e 100644 --- a/concordia/views.py +++ b/concordia/views.py @@ -11,9 +11,6 @@ from urllib.parse import urlencode import markdown -from captcha.fields import CaptchaField -from captcha.helpers import captcha_image_url -from captcha.models import CaptchaStore from django.conf import settings from django.contrib import messages from django.contrib.auth import logout @@ -340,33 +337,29 @@ class ConcordiaRegistrationView(RegistrationView): @method_decorator(never_cache, name="dispatch") -@method_decorator( - ratelimit( - group="login", key="post:username", rate="3/15m", method="POST", block=False - ), - name="post", -) class ConcordiaLoginView(LoginView): form_class = UserLoginForm def post(self, request, *args, **kwargs): form = self.get_form() - - # This is set by the ratelimit decorator - # True if the request exceeds the rate limit - blocked = request.limited - recent_captcha = ( - time() - request.session.get("captcha_validation_time", 0) - ) < 86400 - - if blocked and not recent_captcha: - form.fields["captcha"] = CaptchaField() - if form.is_valid(): - return self.form_valid(form) + turnstile_form = TurnstileForm(request.POST) + if turnstile_form.is_valid(): + return self.form_valid(form) + else: + form.add_error(None, "Unable to validate user") + return self.form_invalid(form) + else: return self.form_invalid(form) + def get_context_data(self, **kwargs): + ctx = super().get_context_data(**kwargs) + + ctx["turnstile_form"] = TurnstileForm(auto_id=False) + + return ctx + def ratelimit_view(request, exception=None): status_code = 429 @@ -1518,44 +1511,17 @@ def get_context_data(self, **kwargs): return ctx -@never_cache -def ajax_captcha(request): - if request.method == "POST": - response = request.POST.get("response") - key = request.POST.get("key") - - if response and key: - CaptchaStore.remove_expired() - - # Note that CaptchaStore displays the response in uppercase in the - # image and in the string representation of the object but the - # actual value stored in the database is lowercase! - deleted, _ = CaptchaStore.objects.filter( - response=response.lower(), hashkey=key - ).delete() - - if deleted > 0: - request.session["captcha_validation_time"] = time() - return JsonResponse({"valid": True}) - - key = CaptchaStore.generate_key() - return JsonResponse( - {"key": key, "image": request.build_absolute_uri(captcha_image_url(key))}, - status=401, - content_type="application/json", - ) - - def validate_anonymous_user(view): @wraps(view) @never_cache def inner(request, *args, **kwargs): - if not request.user.is_authenticated: - last_validated = request.session.get("anonymous_validation_time", 0) - age = time() - last_validated - if age > settings.ANONYMOUS_VALIDATION_INTERVAL: - return ajax_captcha(request) - + if not request.user.is_authenticated and request.method == "POST": + form = TurnstileForm(request.POST) + if not form.is_valid(): + return JsonResponse( + {"error": "Unable to validate user"}, + status=401, + ) return view(request, *args, **kwargs) return inner @@ -1583,7 +1549,7 @@ def get_transcription_superseded(asset, supersedes_pk): @require_POST -@validate_anonymous_user +@login_required @atomic @ratelimit(key="header:cf-connecting-ip", rate="1/m", block=settings.RATELIMIT_BLOCK) def generate_ocr_transcription(request, *, asset_pk): @@ -1706,6 +1672,7 @@ def rollforward_transcription(request, *, asset_pk): @require_POST +@validate_anonymous_user @atomic def save_transcription(request, *, asset_pk): asset = get_object_or_404(Asset, pk=asset_pk) @@ -1713,15 +1680,6 @@ def save_transcription(request, *, asset_pk): if request.user.is_anonymous: user = get_anonymous_user() - form = TurnstileForm(request.POST) - if not form.is_valid(): - return JsonResponse( - { - "error": "Turnstile form invalid...." - "I don't know what to tell you yet...." - }, - status=400, - ) else: user = request.user diff --git a/docs/for-developers.md b/docs/for-developers.md index 2b6f438ee..1edbd2d27 100644 --- a/docs/for-developers.md +++ b/docs/for-developers.md @@ -77,6 +77,16 @@ virtualenv environment: 1. Make sure that [redis](https://redis.io/docs/getting-started/) is installed and running. +1. Configure Turnstile in your `.env` file. Unless specifically testing Turnstile, + you'll probably want the following settings: + + ```bash + echo TURNSTILE_SITEKEY=1x00000000000000000000BB >> .env + echo TURNSTILE_SECRET=1x0000000000000000000000000000000AA >> .env + ``` + + Those two settings ensure all Turnstile tests pass. See [Turnstile Testing](https://developers.cloudflare.com/turnstile/troubleshooting/testing/) for other options. + ### Local Development Environment You will likely want to run the Django development server on your localhost