Commit 881e3ba5 by Calen Pennington

Add the ability to dark-launch languages

To mark a language as dark-launched, add it to the DARK_LANGUAGES
django conf setting. To activate a dark-launched language, set he
query parameter `preview-lang` to the language code on any url.

[LMS-2045]
[LMS-2077]
[LMS-2076]
parent ce2c067d
...@@ -12,6 +12,10 @@ Blades: Fix bug when image response in Firefox does not retain input. BLD-711. ...@@ -12,6 +12,10 @@ Blades: Fix bug when image response in Firefox does not retain input. BLD-711.
Blades: Give numerical response tolerance as a range. BLD-25. Blades: Give numerical response tolerance as a range. BLD-25.
Common: Add the ability to dark-launch site translations. These languages
will be unavailable to users except through the use of a specific query
parameter.
Blades: Allow user with BetaTester role correctly use LTI. BLD-641. Blades: Allow user with BetaTester role correctly use LTI. BLD-641.
Blades: Video player persist speed preferences between videos. BLD-237. Blades: Video player persist speed preferences between videos. BLD-237.
......
...@@ -153,6 +153,12 @@ COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", []) ...@@ -153,6 +153,12 @@ COURSES_WITH_UNSAFE_CODE = ENV_TOKENS.get("COURSES_WITH_UNSAFE_CODE", [])
#Timezone overrides #Timezone overrides
TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
# Translation overrides
LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES)
RELEASED_LANGUAGES = ENV_TOKENS.get('RELEASED_LANGUAGES', LANGUAGES)
LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE)
USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N)
ENV_FEATURES = ENV_TOKENS.get('FEATURES', ENV_TOKENS.get('MITX_FEATURES', {})) ENV_FEATURES = ENV_TOKENS.get('FEATURES', ENV_TOKENS.get('MITX_FEATURES', {}))
for feature, value in ENV_FEATURES.items(): for feature, value in ENV_FEATURES.items():
FEATURES[feature] = value FEATURES[feature] = value
......
...@@ -167,6 +167,9 @@ MIDDLEWARE_CLASSES = ( ...@@ -167,6 +167,9 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware', 'track.middleware.TrackMiddleware',
# Allows us to dark-launch particular languages
'dark_lang.middleware.DarkLangMiddleware',
# Detects user-requested locale from 'accept-language' header in http request # Detects user-requested locale from 'accept-language' header in http request
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
...@@ -244,12 +247,16 @@ STATICFILES_DIRS = [ ...@@ -244,12 +247,16 @@ STATICFILES_DIRS = [
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
# We want i18n to be turned off in production, at least until we have full localizations. LANGUAGES = (
# Thus we want the Django translation engine to be disabled. Otherwise even without ('en@pirate', 'Pirate English'),
# localization files, if the user's browser is set to a language other than us-en, ('eo', 'Esperanto'),
# strings like "login" and "password" will be translated and the rest of the page will be )
# in English, which is confusing.
USE_I18N = False # This is the list of language codes for languanges which are released to all users.
# See dark_lang/README.rst for more details.
RELEASED_LANGUAGES = ()
USE_I18N = True
USE_L10N = True USE_L10N = True
# Localization strings (e.g. django.po) are under this directory # Localization strings (e.g. django.po) are under this directory
......
...@@ -9,11 +9,6 @@ from .common import * ...@@ -9,11 +9,6 @@ from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
DEBUG = True DEBUG = True
USE_I18N = True
# For displaying the dummy text, we need to provide a language mapping.
LANGUAGES = (
('eo', 'Esperanto'),
)
TEMPLATE_DEBUG = DEBUG TEMPLATE_DEBUG = DEBUG
LOGGING = get_logger_config(ENV_ROOT / "log", LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev", logging_env="dev",
......
Language Translation Dark Launching
===================================
This app adds the ability to launch language translations that
are only accessible through the use of a specific query parameter
(and are not activated by browser settings).
Installation
------------
Add the ``.DarkLangMiddleware`` to your list of ``MIDDLEWARE_CLASSES``.
It must come after the ``SessionMiddleware``, and before the ``LocaleMiddleware``.
Add the ``RELEASED_LANGUAGES`` setting to your settings file. This
should be a list of all language codes which can be selected via a
user's browser settings.
\ No newline at end of file
"""
Middleware for dark-launching languages. These languages won't be used
when determining which translation to give a user based on their browser
header, but can be selected by setting the ``preview-lang`` query parameter
to the language code.
Adding the query parameter ``clear-lang`` will reset the language stored
in the user's session.
This middleware must be placed before the LocaleMiddleware, but after
the SessionMiddleware.
"""
from django.conf import settings
from django.core.exceptions import MiddlewareNotUsed
from django.utils.translation.trans_real import parse_accept_lang_header
class DarkLangMiddleware(object):
"""
Middleware for dark-launching languages.
This middleware will only be active if the RELEASED_LANGUAGES setting is set.
This setting should contain a list of language codes for languages which
are considered to be dark-launched, and those won't activate based on a
users browser settings.
"""
def __init__(self):
self.released_langs = getattr(settings, 'RELEASED_LANGUAGES', None)
if self.released_langs is None:
raise MiddlewareNotUsed()
def process_request(self, request):
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.startswith(released_lang) for released_lang in self.released_langs)
def _format_accept_value(self, lang, priority=1.0):
"""
Formats lang and priority into a valid accept header fragment.
"""
return "{};q={}".format(lang, priority)
def _clean_accept_headers(self, request):
"""
Remove any language that is not either in ``self.released_langs`` or
a territory of one of those languages.
"""
accept = request.META.get('HTTP_ACCEPT_LANGUAGE', None)
if accept is None or accept == '*':
return
new_accept = ", ".join(
self._format_accept_value(lang, priority)
for lang, priority
in parse_accept_lang_header(accept)
if self._is_released(lang)
)
request.META['HTTP_ACCEPT_LANGUAGE'] = new_accept
def _activate_preview_language(self, request):
"""
If the request has the get parameter ``preview-lang``,
and that language appears doesn't appear in ``self.released_langs``,
then set the session ``django_language`` to that language.
"""
if 'clear-lang' in request.GET:
if 'django_language' in request.session:
del request.session['django_language']
preview_lang = request.GET.get('preview-lang', None)
if not preview_lang:
return
if preview_lang in self.released_langs:
return
request.session['django_language'] = preview_lang
"""
Tests of DarkLangMiddleware
"""
from django.core.exceptions import MiddlewareNotUsed
from django.http import HttpRequest, QueryDict
from django.test import TestCase
from django.test.utils import override_settings
from mock import Mock
from dark_lang.middleware import DarkLangMiddleware
UNSET = object()
def set_if_set(dict, key, value):
"""
Sets ``key`` in ``dict`` to ``value``
unless ``value`` is ``UNSET``
"""
if value is not UNSET:
dict[key] = value
@override_settings(RELEASED_LANGUAGES=('rel'))
class DarkLangMiddlewareTests(TestCase):
"""
Tests of DarkLangMiddleware
"""
def process_request(self, django_language=UNSET, accept=UNSET, preview_lang=UNSET, clear_lang=UNSET):
session = {}
set_if_set(session, 'django_language', django_language)
META = {}
set_if_set(META, 'HTTP_ACCEPT_LANGUAGE', accept)
GET = {}
set_if_set(GET, 'preview-lang', preview_lang)
set_if_set(GET, 'clear-lang', clear_lang)
request = Mock(
spec=HttpRequest,
session=session,
META=META,
GET=GET
)
self.assertIsNone(DarkLangMiddleware().process_request(request))
return request
@override_settings(RELEASED_LANGUAGES=None)
def test_inactive_middleware(self):
with self.assertRaises(MiddlewareNotUsed):
DarkLangMiddleware()
def assertAcceptEquals(self, value, request):
"""
Assert that the HTML_ACCEPT_LANGUAGE header in request
is equal to value
"""
self.assertEquals(
value,
request.META.get('HTTP_ACCEPT_LANGUAGE', UNSET)
)
def test_empty_accept(self):
self.assertAcceptEquals(UNSET, self.process_request())
def test_wildcard_accept(self):
self.assertAcceptEquals('*', self.process_request(accept='*'))
def test_released_accept(self):
self.assertAcceptEquals(
'rel;q=1.0',
self.process_request(accept='rel;q=1.0')
)
def test_unreleased_accept(self):
self.assertAcceptEquals(
'rel;q=1.0',
self.process_request(accept='rel;q=1.0, unrel;q=0.5')
)
@override_settings(RELEASED_LANGUAGES=('rel', 'unrel'))
def test_accept_multiple_released_langs(self):
self.assertAcceptEquals(
'rel;q=1.0, unrel;q=0.5',
self.process_request(accept='rel;q=1.0, unrel;q=0.5')
)
self.assertAcceptEquals(
'rel;q=1.0, unrel;q=0.5',
self.process_request(accept='rel;q=1.0, notrel;q=0.3, unrel;q=0.5')
)
self.assertAcceptEquals(
'rel;q=1.0, unrel;q=0.5',
self.process_request(accept='notrel;q=0.3, rel;q=1.0, unrel;q=0.5')
)
def test_accept_released_territory(self):
self.assertAcceptEquals(
'rel-ter;q=1.0, rel;q=0.5',
self.process_request(accept='rel-ter;q=1.0, rel;q=0.5')
)
def assertSessionLangEquals(self, value, request):
"""
Assert that the 'django_language' set in request.session is equal to value
"""
self.assertEquals(
value,
request.session.get('django_language', UNSET)
)
def test_preview_lang_with_released_language(self):
self.assertSessionLangEquals(
UNSET,
self.process_request(preview_lang='rel')
)
self.assertSessionLangEquals(
'notrel',
self.process_request(preview_lang='rel', django_language='notrel')
)
def test_preview_lang_with_dark_language(self):
self.assertSessionLangEquals(
'unrel',
self.process_request(preview_lang='unrel')
)
self.assertSessionLangEquals(
'unrel',
self.process_request(preview_lang='unrel', django_language='notrel')
)
def test_clear_lang(self):
self.assertSessionLangEquals(
UNSET,
self.process_request(clear_lang=True)
)
self.assertSessionLangEquals(
UNSET,
self.process_request(clear_lang=True, django_language='rel')
)
self.assertSessionLangEquals(
UNSET,
self.process_request(clear_lang=True, django_language='unrel')
)
...@@ -203,6 +203,7 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) ...@@ -203,6 +203,7 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
# Translation overrides # Translation overrides
LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES) LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES)
RELEASED_LANGUAGES = ENV_TOKENS.get('RELEASED_LANGUAGES', LANGUAGES)
LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE) LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE)
USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N) USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N)
......
...@@ -492,14 +492,17 @@ FAVICON_PATH = 'images/favicon.ico' ...@@ -492,14 +492,17 @@ FAVICON_PATH = 'images/favicon.ico'
# Locale/Internationalization # Locale/Internationalization
TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGES = ()
LANGUAGES = (
# We want i18n to be turned off in production, at least until we have full localizations. ('en@pirate', 'Pirate English'),
# Thus we want the Django translation engine to be disabled. Otherwise even without ('eo', 'Esperanto'),
# localization files, if the user's browser is set to a language other than us-en, )
# strings like "login" and "password" will be translated and the rest of the page will be
# in English, which is confusing. # This is the list of language codes for languanges which are released to all users.
USE_I18N = False # See dark_lang/README.rst for more details.
RELEASED_LANGUAGES = ()
USE_I18N = True
USE_L10N = True USE_L10N = True
# Localization strings (e.g. django.po) are under this directory # Localization strings (e.g. django.po) are under this directory
...@@ -639,6 +642,9 @@ MIDDLEWARE_CLASSES = ( ...@@ -639,6 +642,9 @@ MIDDLEWARE_CLASSES = (
'course_wiki.course_nav.Middleware', 'course_wiki.course_nav.Middleware',
# Allows us to dark-launch particular languages
'dark_lang.middleware.DarkLangMiddleware',
# Detects user-requested locale from 'accept-language' header in http request # Detects user-requested locale from 'accept-language' header in http request
'django.middleware.locale.LocaleMiddleware', 'django.middleware.locale.LocaleMiddleware',
......
...@@ -16,11 +16,6 @@ from .common import * ...@@ -16,11 +16,6 @@ from .common import *
from logsettings import get_logger_config from logsettings import get_logger_config
DEBUG = True DEBUG = True
USE_I18N = True
# For displaying the dummy text, we need to provide a language mapping.
LANGUAGES = (
('eo', 'Esperanto'),
)
TEMPLATE_DEBUG = True TEMPLATE_DEBUG = True
FEATURES['DISABLE_START_DATES'] = False FEATURES['DISABLE_START_DATES'] = False
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment