diff --git a/dash/orgs/middleware.py b/dash/orgs/middleware.py index d53c3a1..ce13e2c 100644 --- a/dash/orgs/middleware.py +++ b/dash/orgs/middleware.py @@ -1,5 +1,6 @@ import re import traceback +from contextlib import nullcontext from django.conf import settings from django.core.exceptions import DisallowedHost @@ -47,9 +48,27 @@ class SetOrgMiddleware(MiddlewareMixin): Sets the org on the request, based on the subdomain """ + async_capable = False + def __init__(self, get_response=None): super(SetOrgMiddleware, self).__init__(get_response) + def __call__(self, request): + response = self.process_request(request) + if response: + return response + + org = request.org + + # activate the org's language and timezone for the duration of the request only, restoring whatever was + # active before so that state never leaks into subsequent requests on the same thread, and org-less + # requests keep e.g. the language negotiated by LocaleMiddleware + lang_override = translation.override(org.language or settings.DEFAULT_LANGUAGE) if org else nullcontext() + tz_override = timezone.override(org.timezone) if org and org.timezone else nullcontext() + + with lang_override, tz_override: + return self.get_response(request) + def process_request(self, request): # try looking the domain level @@ -83,20 +102,6 @@ def process_request(self, request): request.org = org - self.set_language(request, org) - self.set_timezone(request, org) - - def set_language(self, request, org): - """Set the current language from the org configuration.""" - if org: - lang = org.language or settings.DEFAULT_LANGUAGE - translation.activate(lang) - - def set_timezone(self, request, org): - """Set the current timezone from the org configuration.""" - if org and org.timezone: - timezone.activate(org.timezone) - def process_view(self, request, view_func, view_args, view_kwargs): if not request.org: # serve static files diff --git a/test_runner/tests.py b/test_runner/tests.py index d9cc2f4..c798e19 100644 --- a/test_runner/tests.py +++ b/test_runner/tests.py @@ -15,6 +15,7 @@ from django.test import override_settings from django.test.utils import CaptureQueriesContext from django.urls import ResolverMatch, reverse +from django.utils import timezone, translation from django.utils.encoding import force_str from dash.categories.fields import CategoryChoiceField @@ -351,6 +352,87 @@ def test_process(self): self.assertEqual(self.request.org, empty_subdomain_org) self.assertEqual(self.request.user.get_org(), empty_subdomain_org) + def simulate_call(self, host, url_name, path="/"): + """ + Simulates a full middleware call, capturing the language and timezone active while the view runs + """ + self.request = Mock(spec=HttpRequest) + self.request.get_host.return_value = host + self.request.user = self.admin + self.request.path = path + self.request.META = dict(HTTP_HOST=None) + + def get_response(request): + request.resolver_match = ResolverMatch(self.mock_view, [], {}, url_name) + response = self.middleware.process_view(request, self.mock_view, [], {}) + if response: + return response + + self.view_language = translation.get_language() + self.view_timezone = timezone.get_current_timezone_name() + return HttpResponse() + + return SetOrgMiddleware(get_response)(self.request) + + def test_language_and_timezone_restored(self): + self.addCleanup(translation.deactivate) + self.addCleanup(timezone.deactivate) + + # start from the default language and timezone + translation.deactivate() + timezone.deactivate() + + ug_org = self.create_org("uganda", self.admin) + ug_org.language = "fr" + ug_org.timezone = zoneinfo.ZoneInfo("Africa/Kigali") + ug_org.save() + + # a request with an org activates its language and timezone for the duration of the view + response = self.simulate_call("uganda.ureport.io", "dash.test_test") + self.assertEqual(response.status_code, 200) + self.assertEqual(self.request.org, ug_org) + self.assertEqual(self.view_language, "fr") + self.assertEqual(self.view_timezone, "Africa/Kigali") + + # but restores the previous state once the response is returned + self.assertEqual(translation.get_language(), settings.LANGUAGE_CODE) + self.assertEqual(timezone.get_current_timezone_name(), settings.TIME_ZONE) + + # so a subsequent org-less request on the same thread sees the defaults + response = self.simulate_call("ureport.io", "orgs.org_create") + self.assertEqual(response.status_code, 200) + self.assertIsNone(self.request.org) + self.assertEqual(self.view_language, settings.LANGUAGE_CODE) + self.assertEqual(self.view_timezone, settings.TIME_ZONE) + + def test_org_less_request_keeps_negotiated_language(self): + self.addCleanup(translation.deactivate) + self.addCleanup(timezone.deactivate) + + # simulate LocaleMiddleware having negotiated a language before this middleware runs + translation.activate("es") + + response = self.simulate_call("ureport.io", "orgs.org_create") + self.assertEqual(response.status_code, 200) + self.assertIsNone(self.request.org) + + # an org-less request doesn't touch the active language + self.assertEqual(self.view_language, "es") + self.assertEqual(translation.get_language(), "es") + + def test_org_without_language_gets_default(self): + self.addCleanup(translation.deactivate) + self.addCleanup(timezone.deactivate) + + ug_org = self.create_org("uganda", self.admin) + ug_org.language = None + ug_org.save() + + response = self.simulate_call("uganda.ureport.io", "dash.test_test") + self.assertEqual(response.status_code, 200) + self.assertEqual(self.request.org, ug_org) + self.assertEqual(self.view_language, settings.DEFAULT_LANGUAGE) + class OrgContextProcessorTestcase(DashTest): def test_group_perms_wrapper(self):