Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions dash/orgs/middleware.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
import traceback
from contextlib import nullcontext

from django.conf import settings
from django.core.exceptions import DisallowedHost
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
82 changes: 82 additions & 0 deletions test_runner/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
Expand Down