diff --git a/common/djangoapps/dark_lang/middleware.py b/common/djangoapps/dark_lang/middleware.py index 804d72aa2e..7a7a6b8868 100644 --- a/common/djangoapps/dark_lang/middleware.py +++ b/common/djangoapps/dark_lang/middleware.py @@ -83,11 +83,17 @@ class DarkLangMiddleware(object): self._clean_accept_headers(request) self._activate_preview_language(request) - def _is_released(self, lang_code): - """ - ``True`` iff one of the values in ``self.released_langs`` is a prefix of ``lang_code``. - """ - return any(lang_code.lower().startswith(released_lang.lower()) for released_lang in self.released_langs) + def _fuzzy_match(self, lang_code): + """Returns a fuzzy match for lang_code""" + if lang_code in self.released_langs: + return lang_code + + lang_prefix = lang_code.partition('-')[0] + for released_lang in self.released_langs: + released_prefix = released_lang.partition('-')[0] + if lang_prefix == released_prefix: + return released_lang + return None def _format_accept_value(self, lang, priority=1.0): """ @@ -104,12 +110,13 @@ class DarkLangMiddleware(object): if accept is None or accept == '*': return - new_accept = ", ".join( - self._format_accept_value(lang, priority) - for lang, priority - in dark_parse_accept_lang_header(accept) - if self._is_released(lang) - ) + new_accept = [] + for lang, priority in dark_parse_accept_lang_header(accept): + fuzzy_code = self._fuzzy_match(lang.lower()) + if fuzzy_code: + new_accept.append(self._format_accept_value(fuzzy_code, priority)) + + new_accept = ", ".join(new_accept) request.META['HTTP_ACCEPT_LANGUAGE'] = new_accept diff --git a/common/djangoapps/dark_lang/tests.py b/common/djangoapps/dark_lang/tests.py index 6dd0b41882..b7210088e9 100644 --- a/common/djangoapps/dark_lang/tests.py +++ b/common/djangoapps/dark_lang/tests.py @@ -4,8 +4,10 @@ Tests of DarkLangMiddleware from django.contrib.auth.models import User from django.http import HttpRequest +import ddt from django.test import TestCase from mock import Mock +import unittest from dark_lang.middleware import DarkLangMiddleware from dark_lang.models import DarkLangConfig @@ -23,6 +25,7 @@ def set_if_set(dct, key, value): dct[key] = value +@ddt.ddt class DarkLangMiddlewareTests(TestCase): """ Tests of DarkLangMiddleware @@ -82,6 +85,10 @@ class DarkLangMiddlewareTests(TestCase): def test_wildcard_accept(self): self.assertAcceptEquals('*', self.process_request(accept='*')) + def test_malformed_accept(self): + self.assertAcceptEquals('', self.process_request(accept='xxxxxxxxxxxx')) + self.assertAcceptEquals('', self.process_request(accept='en;q=1.0, es-419:q-0.8')) + def test_released_accept(self): self.assertAcceptEquals( 'rel;q=1.0', @@ -123,14 +130,17 @@ class DarkLangMiddlewareTests(TestCase): ) def test_accept_released_territory(self): + # We will munge 'rel-ter' to be 'rel', so the 'rel-ter' + # user will actually receive the released language 'rel' + # (Otherwise, the user will actually end up getting the server default) self.assertAcceptEquals( - 'rel-ter;q=1.0, rel;q=0.5', + 'rel;q=1.0, rel;q=0.5', self.process_request(accept='rel-ter;q=1.0, rel;q=0.5') ) def test_accept_mixed_case(self): self.assertAcceptEquals( - 'rel-TER;q=1.0, REL;q=0.5', + 'rel;q=1.0, rel;q=0.5', self.process_request(accept='rel-TER;q=1.0, REL;q=0.5') ) @@ -140,11 +150,85 @@ class DarkLangMiddlewareTests(TestCase): enabled=True ).save() + # Since we have only released "rel-ter", the requested code "rel" will + # fuzzy match to "rel-ter", in addition to "rel-ter" exact matching "rel-ter" self.assertAcceptEquals( - 'rel-ter;q=1.0', + 'rel-ter;q=1.0, rel-ter;q=0.5', self.process_request(accept='rel-ter;q=1.0, rel;q=0.5') ) + @ddt.data( + ('es;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 'es' should get 'es-419', not English + ('es-AR;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 'es-AR' should get 'es-419', not English + ) + @ddt.unpack + def test_partial_match_es419(self, accept_header, expected): + # Release es-419 + DarkLangConfig( + released_languages=('es-419, en'), + changed_by=self.user, + enabled=True + ).save() + + self.assertAcceptEquals( + expected, + self.process_request(accept=accept_header) + ) + + def test_partial_match_esar_es(self): + # If I release 'es', 'es-AR' should get 'es', not English + DarkLangConfig( + released_languages=('es, en'), + changed_by=self.user, + enabled=True + ).save() + + self.assertAcceptEquals( + 'es;q=1.0', + self.process_request(accept='es-AR;q=1.0, pt;q=0.5') + ) + + @ddt.data( + # Test condition: If I release 'es-419, es, es-es'... + ('es;q=1.0, pt;q=0.5', 'es;q=1.0'), # 1. es should get es + ('es-419;q=1.0, pt;q=0.5', 'es-419;q=1.0'), # 2. es-419 should get es-419 + ('es-es;q=1.0, pt;q=0.5', 'es-es;q=1.0'), # 3. es-es should get es-es + ) + @ddt.unpack + def test_exact_match_gets_priority(self, accept_header, expected): + # Release 'es-419, es, es-es' + DarkLangConfig( + released_languages=('es-419, es, es-es'), + changed_by=self.user, + enabled=True + ).save() + self.assertAcceptEquals( + expected, + self.process_request(accept=accept_header) + ) + + @unittest.skip("This won't work until fallback is implemented for LA country codes. See LOC-86") + @ddt.data( + 'es-AR', # Argentina + 'es-PY', # Paraguay + ) + def test_partial_match_es_la(self, latin_america_code): + # We need to figure out the best way to implement this. There are a ton of LA country + # codes that ought to fall back to 'es-419' rather than 'es-es'. + # http://unstats.un.org/unsd/methods/m49/m49regin.htm#americas + # If I release 'es, es-419' + # Latin American codes should get es-419 + DarkLangConfig( + released_languages=('es, es-419'), + changed_by=self.user, + enabled=True + ).save() + + self.assertAcceptEquals( + 'es-419;q=1.0', + self.process_request(accept='{};q=1.0, pt;q=0.5'.format(latin_america_code)) + ) + def assertSessionLangEquals(self, value, request): """ Assert that the 'django_language' set in request.session is equal to value @@ -224,6 +308,6 @@ class DarkLangMiddlewareTests(TestCase): ).save() self.assertAcceptEquals( - 'zh-CN;q=1.0, zh-TW;q=0.5, zh-HK;q=0.3', + 'zh-cn;q=1.0, zh-tw;q=0.5, zh-hk;q=0.3', self.process_request(accept='zh-Hans;q=1.0, zh-Hant-TW;q=0.5, zh-HK;q=0.3') )