From b42a7a1cfb0ecb871f1fc9987d32a2e6f396369c Mon Sep 17 00:00:00 2001 From: "Albert St. Aubin" Date: Mon, 11 Jul 2016 09:53:06 -0400 Subject: [PATCH] Removed the Darklang preview-lang and clear-lang parameters and added the new DarkLang settings form at /update_lang Remove the DarkLang middleware from the LMS Created and basic routing to the update_lang page for the GET Request TNL-4742 Basic form functionality Working example in LMS of the form to set the language Login now required to change the preview language, and corrected some minor bugs Updates to move the template code to lms and to correct minor defects TNL-4742 Added template for preview_lang.html to cms TNL-4742 Changed filename of darklang.py to api.py to match convention TNL-4742 Updated and refactored the Darklang tests TNL-4742 Updated comments in tests TNL-4742 Formating updates TNL-4742 Updated comments and formatting TNL-4742 Corrected i18n tests and corrected PEP8 format issues TNL-4742 Code Lint/PEP-8 corrections and upates TNL-4742 Removed constant that was not needed (to be squashed) TNL-4742 Added init method to clear up PEP8 Warnings (will squash) TNL-4742 PEP-8/Lint issue resolved (squash) Updated for i18n TNL-4742 Refactored the preview_lang.html page to use a common included template Refactoring and changes from PR comments TNL-4742 Correction for safecommit violation (Squash) TNL-4742 PR changes and refactoring (Squash) Updates to reduce changes made in the urls used TNL-4742 Removed unneeded aria-described by and bug in MAKO Template (squash) TNK-4742 Updated docstring comments Clarified form response text Minor PR request (Squash) Refactoring of how the responses are generated within the DarkLang views file A series of refactors in response to PR comments Method name change for clarity in reponse to PR comments (Squash) Updates to tests per PR requests (Squash) Minor comment updates for clarity and PR requests (Squash) Updated per PR comments and added a test for empty preview_language Layout and code style updates (Squash) Updated test to contain method in the request. Removed the Darklang preview-lang and clear-lang parameters and added the new DarkLang settings form at /update_lang Refactored tests and added some tests for coverage, corrected defect with empty input codes Removed unused and obsolete code Corrected test errors, resolved PR comments, and updated comments to clarify testing TNL-4742 Updated tests to deal with Pylint quality issue (Squash) Updated tests to better reflect test case and PR updates (Squash) --- cms/templates/darklang/preview_lang.html | 15 ++ cms/urls.py | 3 + common/djangoapps/dark_lang/middleware.py | 105 ++++------- common/djangoapps/dark_lang/tests.py | 177 +++++++++++------- common/djangoapps/dark_lang/urls.py | 12 ++ common/djangoapps/dark_lang/views.py | 163 ++++++++++++++++ .../darklang/preview_lang_include.html | 36 ++++ lms/djangoapps/courseware/tests/test_i18n.py | 87 +++++---- lms/templates/darklang/preview_lang.html | 15 ++ lms/urls.py | 1 + 10 files changed, 440 insertions(+), 174 deletions(-) create mode 100644 cms/templates/darklang/preview_lang.html create mode 100644 common/djangoapps/dark_lang/urls.py create mode 100644 common/djangoapps/dark_lang/views.py create mode 100644 common/templates/darklang/preview_lang_include.html create mode 100644 lms/templates/darklang/preview_lang.html diff --git a/cms/templates/darklang/preview_lang.html b/cms/templates/darklang/preview_lang.html new file mode 100644 index 0000000000..3b84513591 --- /dev/null +++ b/cms/templates/darklang/preview_lang.html @@ -0,0 +1,15 @@ +## Override the default styles_version to the Pattern Library version (version 2) +<%! main_css = "style-main-v2" %> +<%page expression_filter="h"/> +<%! +from django.utils.translation import ugettext as _ +%> + +<%inherit file="../base.html" /> +<%block name="title">${_("Preview Language Setting")} +<%block name="bodyclass">is-signedin pattern-library + +<%block name="content"> + <%include file="/darklang/preview_lang_include.html" /> + + diff --git a/cms/urls.py b/cms/urls.py index d539456efd..d4e2c08dfa 100644 --- a/cms/urls.py +++ b/cms/urls.py @@ -55,6 +55,9 @@ urlpatterns = patterns( # Update session view url(r'^lang_pref/session_language', 'lang_pref.views.update_session_language', name='session_language'), + + # Darklang View to change the preview language (or dark language) + url(r'^update_lang/', include('dark_lang.urls', namespace='darklang')), ) # User creation and updating views diff --git a/common/djangoapps/dark_lang/middleware.py b/common/djangoapps/dark_lang/middleware.py index d1062c7e40..4e4e97f4c7 100644 --- a/common/djangoapps/dark_lang/middleware.py +++ b/common/djangoapps/dark_lang/middleware.py @@ -1,11 +1,8 @@ """ 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. +header, but can be selected by setting the Preview Languages on the Dark +Language setting page. This middleware must be placed before the LocaleMiddleware, but after the SessionMiddleware. @@ -15,16 +12,29 @@ from django.conf import settings from dark_lang import DARK_LANGUAGE_KEY from dark_lang.models import DarkLangConfig from openedx.core.djangoapps.user_api.preferences.api import ( - delete_user_preference, get_user_preference, set_user_preference + get_user_preference ) -from lang_pref import LANGUAGE_KEY - from django.utils.translation.trans_real import parse_accept_lang_header from django.utils.translation import LANGUAGE_SESSION_KEY +# If django 1.7 or higher is used, the right-side can be updated with new-style codes. +CHINESE_LANGUAGE_CODE_MAP = { + # The following are the new-style language codes for chinese language + 'zh-hans': 'zh-CN', # Chinese (Simplified), + 'zh-hans-cn': 'zh-CN', # Chinese (Simplified, China) + 'zh-hans-sg': 'zh-CN', # Chinese (Simplified, Singapore) + 'zh-hant': 'zh-TW', # Chinese (Traditional) + 'zh-hant-hk': 'zh-HK', # Chinese (Traditional, Hongkong) + 'zh-hant-mo': 'zh-TW', # Chinese (Traditional, Macau) + 'zh-hant-tw': 'zh-TW', # Chinese (Traditional, Taiwan) + # The following are the old-style language codes that django does not recognize + 'zh-mo': 'zh-TW', # Chinese (Traditional, Macau) + 'zh-sg': 'zh-CN', # Chinese (Simplified, Singapore) +} -def dark_parse_accept_lang_header(accept): - ''' + +def _dark_parse_accept_lang_header(accept): + """ The use of 'zh-cn' for 'Simplified Chinese' and 'zh-tw' for 'Traditional Chinese' are now deprecated, as discussed here: https://code.djangoproject.com/ticket/18419. The new language codes 'zh-hans' and 'zh-hant' are now used since django 1.7. @@ -34,7 +44,7 @@ def dark_parse_accept_lang_header(accept): This function can keep compatibility between the old and new language codes. If one day edX uses django 1.7 or higher, this function can be modified to support the old language codes until there are no browsers use them. - ''' + """ browser_langs = parse_accept_lang_header(accept) django_langs = [] for lang, priority in browser_langs: @@ -43,21 +53,6 @@ def dark_parse_accept_lang_header(accept): return django_langs -# If django 1.7 or higher is used, the right-side can be updated with new-style codes. -CHINESE_LANGUAGE_CODE_MAP = { - # The following are the new-style language codes for chinese language - 'zh-hans': 'zh-CN', # Chinese (Simplified), - 'zh-hans-cn': 'zh-CN', # Chinese (Simplified, China) - 'zh-hans-sg': 'zh-CN', # Chinese (Simplified, Singapore) - 'zh-hant': 'zh-TW', # Chinese (Traditional) - 'zh-hant-hk': 'zh-HK', # Chinese (Traditional, Hongkong) - 'zh-hant-mo': 'zh-TW', # Chinese (Traditional, Macau) - 'zh-hant-tw': 'zh-TW', # Chinese (Traditional, Taiwan) - # The following are the old-style language codes that django does not recognize - 'zh-mo': 'zh-TW', # Chinese (Traditional, Macau) - 'zh-sg': 'zh-CN', # Chinese (Simplified, Singapore) -} - class DarkLangMiddleware(object): """ @@ -66,7 +61,6 @@ class DarkLangMiddleware(object): This is configured by creating ``DarkLangConfig`` rows in the database, using the django admin site. """ - @property def released_langs(self): """ @@ -89,21 +83,16 @@ class DarkLangMiddleware(object): def _fuzzy_match(self, lang_code): """Returns a fuzzy match for lang_code""" + match = None 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): - """ - Formats lang and priority into a valid accept header fragment. - """ - return "{};q={}".format(lang, priority) + match = lang_code + else: + lang_prefix = lang_code.partition('-')[0] + for released_lang in self.released_langs: + released_prefix = released_lang.partition('-')[0] + if lang_prefix == released_prefix: + match = released_lang + return match def _clean_accept_headers(self, request): """ @@ -115,10 +104,11 @@ class DarkLangMiddleware(object): return new_accept = [] - for lang, priority in dark_parse_accept_lang_header(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)) + # Formats lang and priority into a valid accept header fragment. + new_accept.append("{};q={}".format(fuzzy_code, priority)) new_accept = ", ".join(new_accept) @@ -126,29 +116,11 @@ class DarkLangMiddleware(object): def _activate_preview_language(self, request): """ - If the request has the get parameter ``preview-lang``, - and that language doesn't appear in ``self.released_langs``, - then set the session LANGUAGE_SESSION_KEY to that language. + Check the user's dark language setting in the session and apply it """ auth_user = request.user.is_authenticated() - if 'clear-lang' in request.GET: - # delete the session language key (if one is set) - if LANGUAGE_SESSION_KEY in request.session: - del request.session[LANGUAGE_SESSION_KEY] - - if auth_user: - # Reset user's dark lang preference to null - delete_user_preference(request.user, DARK_LANGUAGE_KEY) - # Get & set user's preferred language - user_pref = get_user_preference(request.user, LANGUAGE_KEY) - if user_pref: - request.session[LANGUAGE_SESSION_KEY] = user_pref - return - - # Get the user's preview lang - this is either going to be set from a query - # param `?preview-lang=xx`, or we may have one already set as a dark lang preference. - preview_lang = request.GET.get('preview-lang', None) - if not preview_lang and auth_user: + preview_lang = None + if auth_user: # Get the request user's dark lang preference preview_lang = get_user_preference(request.user, DARK_LANGUAGE_KEY) @@ -158,8 +130,3 @@ class DarkLangMiddleware(object): # Set the session key to the requested preview lang request.session[LANGUAGE_SESSION_KEY] = preview_lang - - # Make sure that we set the requested preview lang as the dark lang preference for the - # user, so that the lang_pref middleware doesn't clobber away the dark lang preview. - if auth_user: - set_user_preference(request.user, DARK_LANGUAGE_KEY, preview_lang) diff --git a/common/djangoapps/dark_lang/tests.py b/common/djangoapps/dark_lang/tests.py index db529bdb8e..bc3fda776c 100644 --- a/common/djangoapps/dark_lang/tests.py +++ b/common/djangoapps/dark_lang/tests.py @@ -1,20 +1,18 @@ """ Tests of DarkLangMiddleware """ -from django.contrib.auth.models import User -from django.http import HttpRequest +import unittest import ddt +from django.http import HttpRequest from django.test import TestCase +from django.test.client import Client +from django.utils.translation import LANGUAGE_SESSION_KEY from mock import Mock -import unittest from dark_lang.middleware import DarkLangMiddleware from dark_lang.models import DarkLangConfig -from django.utils.translation import LANGUAGE_SESSION_KEY from student.tests.factories import UserFactory - - UNSET = object() @@ -34,23 +32,23 @@ class DarkLangMiddlewareTests(TestCase): """ def setUp(self): super(DarkLangMiddlewareTests, self).setUp() - self.user = User() + self.user = UserFactory.build(username='test', email='test@edx.org', password='test_password') self.user.save() + self.client = Client() + self.client.login(username=self.user.username, password='test_password') DarkLangConfig( released_languages='rel', changed_by=self.user, enabled=True ).save() - def process_request(self, language_session_key=UNSET, accept=UNSET, preview_lang=UNSET, clear_lang=UNSET): + def process_middleware_request(self, language_session_key=UNSET, accept=UNSET): """ Build a request and then process it using the ``DarkLangMiddleware``. Args: language_session_key (str): The language code to set in request.session[LANUGAGE_SESSION_KEY] accept (str): The accept header to set in request.META['HTTP_ACCEPT_LANGUAGE'] - preview_lang (str): The value to set in request.GET['preview_lang'] - clear_lang (str): The value to set in request.GET['clear_lang'] """ session = {} set_if_set(session, LANGUAGE_SESSION_KEY, language_session_key) @@ -58,17 +56,16 @@ class DarkLangMiddlewareTests(TestCase): 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, - user=UserFactory() + GET={}, + method='GET', + user=self.user ) + + # Process it through the Middleware to ensure the language is available as expected. self.assertIsNone(DarkLangMiddleware().process_request(request)) return request @@ -83,31 +80,31 @@ class DarkLangMiddlewareTests(TestCase): ) def test_empty_accept(self): - self.assertAcceptEquals(UNSET, self.process_request()) + self.assertAcceptEquals(UNSET, self.process_middleware_request()) def test_wildcard_accept(self): - self.assertAcceptEquals('*', self.process_request(accept='*')) + self.assertAcceptEquals('*', self.process_middleware_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')) + self.assertAcceptEquals('', self.process_middleware_request(accept='xxxxxxxxxxxx')) + self.assertAcceptEquals('', self.process_middleware_request(accept='en;q=1.0, es-419:q-0.8')) def test_released_accept(self): self.assertAcceptEquals( 'rel;q=1.0', - self.process_request(accept='rel;q=1.0') + self.process_middleware_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') + self.process_middleware_request(accept='rel;q=1.0, unrel;q=0.5') ) def test_accept_with_syslang(self): self.assertAcceptEquals( 'en;q=1.0, rel;q=0.8', - self.process_request(accept='en;q=1.0, rel;q=0.8, unrel;q=0.5') + self.process_middleware_request(accept='en;q=1.0, rel;q=0.8, unrel;q=0.5') ) def test_accept_multiple_released_langs(self): @@ -119,17 +116,17 @@ class DarkLangMiddlewareTests(TestCase): self.assertAcceptEquals( 'rel;q=1.0, unrel;q=0.5', - self.process_request(accept='rel;q=1.0, unrel;q=0.5') + self.process_middleware_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.process_middleware_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') + self.process_middleware_request(accept='notrel;q=0.3, rel;q=1.0, unrel;q=0.5') ) def test_accept_released_territory(self): @@ -138,17 +135,17 @@ class DarkLangMiddlewareTests(TestCase): # (Otherwise, the user will actually end up getting the server default) self.assertAcceptEquals( 'rel;q=1.0, rel;q=0.5', - self.process_request(accept='rel-ter;q=1.0, rel;q=0.5') + self.process_middleware_request(accept='rel-ter;q=1.0, rel;q=0.5') ) def test_accept_mixed_case(self): self.assertAcceptEquals( 'rel;q=1.0, rel;q=0.5', - self.process_request(accept='rel-TER;q=1.0, REL;q=0.5') + self.process_middleware_request(accept='rel-TER;q=1.0, REL;q=0.5') ) DarkLangConfig( - released_languages=('REL-TER'), + released_languages='REL-TER', changed_by=self.user, enabled=True ).save() @@ -157,7 +154,7 @@ class DarkLangMiddlewareTests(TestCase): # fuzzy match to "rel-ter", in addition to "rel-ter" exact matching "rel-ter" self.assertAcceptEquals( 'rel-ter;q=1.0, rel-ter;q=0.5', - self.process_request(accept='rel-ter;q=1.0, rel;q=0.5') + self.process_middleware_request(accept='rel-ter;q=1.0, rel;q=0.5') ) @ddt.data( @@ -175,7 +172,7 @@ class DarkLangMiddlewareTests(TestCase): self.assertAcceptEquals( expected, - self.process_request(accept=accept_header) + self.process_middleware_request(accept=accept_header) ) def test_partial_match_esar_es(self): @@ -188,7 +185,7 @@ class DarkLangMiddlewareTests(TestCase): self.assertAcceptEquals( 'es;q=1.0', - self.process_request(accept='es-AR;q=1.0, pt;q=0.5') + self.process_middleware_request(accept='es-AR;q=1.0, pt;q=0.5') ) @ddt.data( @@ -207,7 +204,7 @@ class DarkLangMiddlewareTests(TestCase): ).save() self.assertAcceptEquals( expected, - self.process_request(accept=accept_header) + self.process_middleware_request(accept=accept_header) ) @unittest.skip("This won't work until fallback is implemented for LA country codes. See LOC-86") @@ -229,55 +226,100 @@ class DarkLangMiddlewareTests(TestCase): self.assertAcceptEquals( 'es-419;q=1.0', - self.process_request(accept='{};q=1.0, pt;q=0.5'.format(latin_america_code)) + self.process_middleware_request(accept='{};q=1.0, pt;q=0.5'.format(latin_america_code)) ) - def assertSessionLangEquals(self, value, request): + def assert_session_lang_equals(self, value, session): """ - Assert that the LANGUAGE_SESSION_KEY set in request.session is equal to value + Assert that the LANGUAGE_SESSION_KEY set in session is equal to value """ self.assertEquals( value, - request.session.get(LANGUAGE_SESSION_KEY, UNSET) + session.get(LANGUAGE_SESSION_KEY, UNSET) ) + def _post_set_preview_lang(self, preview_language): + """ + Sends a post request to set the preview language + """ + return self.client.post('/update_lang/', {'preview_lang': preview_language, 'set_language': 'set_language'}) + + def _post_clear_preview_lang(self): + """ + Sends a post request to Clear the preview language + """ + return self.client.post('/update_lang/', {'reset': 'reset'}) + + def _set_client_session_language(self, session_language): + """ + Set the session language in the Client + """ + session = self.client.session + session[LANGUAGE_SESSION_KEY] = session_language + session.save() + def test_preview_lang_with_released_language(self): - # Preview lang should always override selection. - self.assertSessionLangEquals( + # Preview lang should always override selection + self._post_set_preview_lang('rel') + # Refresh the page with a get request to confirm the preview language was set + self.client.get('/home') + self.assert_session_lang_equals( 'rel', - self.process_request(preview_lang='rel') + self.client.session ) - self.assertSessionLangEquals( + # Set the session language and ensure that the preview language overrides + self._set_client_session_language('notrel') + self._post_set_preview_lang('rel') + self.client.get('/home') + self.assert_session_lang_equals( 'rel', - self.process_request(preview_lang='rel', language_session_key='notrel') + self.client.session ) def test_preview_lang_with_dark_language(self): - self.assertSessionLangEquals( + self._post_set_preview_lang('unrel') + self.client.get('/home') + self.assert_session_lang_equals( 'unrel', - self.process_request(preview_lang='unrel') + self.client.session ) - self.assertSessionLangEquals( + # Test a clear and then a set of the preview language + self._post_clear_preview_lang() + self._post_set_preview_lang('unrel') + self.client.get('/home') + self.assert_session_lang_equals( 'unrel', - self.process_request(preview_lang='unrel', language_session_key='notrel') + self.client.session + ) + + def test_empty_preview_language(self): + # When posting an empty preview_language the currently set language should not change + self._set_client_session_language('rel') + self._post_set_preview_lang(' ') + self.client.get('/home') + self.assert_session_lang_equals( + 'rel', + self.client.session ) def test_clear_lang(self): - self.assertSessionLangEquals( + # Clear a language when no language was set + self._post_clear_preview_lang() + self.client.get('/home') + self.assert_session_lang_equals( UNSET, - self.process_request(clear_lang=True) + self.client.session ) - self.assertSessionLangEquals( + # Set a language and clear it to ensure the clear is working as expected + self._post_set_preview_lang('notclear') + self._post_clear_preview_lang() + self.client.get('/home') + self.assert_session_lang_equals( UNSET, - self.process_request(clear_lang=True, language_session_key='rel') - ) - - self.assertSessionLangEquals( - UNSET, - self.process_request(clear_lang=True, language_session_key='unrel') + self.client.session ) def test_disabled(self): @@ -285,22 +327,25 @@ class DarkLangMiddlewareTests(TestCase): self.assertAcceptEquals( 'notrel;q=0.3, rel;q=1.0, unrel;q=0.5', - self.process_request(accept='notrel;q=0.3, rel;q=1.0, unrel;q=0.5') + self.process_middleware_request(accept='notrel;q=0.3, rel;q=1.0, unrel;q=0.5') ) - self.assertSessionLangEquals( + # With DarkLang disabled the clear should not change the session language + self._set_client_session_language('rel') + self._post_clear_preview_lang() + self.client.get('/home') + self.assert_session_lang_equals( 'rel', - self.process_request(clear_lang=True, language_session_key='rel') + self.client.session ) - self.assertSessionLangEquals( + # Test that setting the preview language with DarkLang disabled does nothing + self._set_client_session_language('unrel') + self._post_set_preview_lang('rel') + self.client.get('/home') + self.assert_session_lang_equals( 'unrel', - self.process_request(clear_lang=True, language_session_key='unrel') - ) - - self.assertSessionLangEquals( - 'rel', - self.process_request(preview_lang='unrel', language_session_key='rel') + self.client.session ) def test_accept_chinese_language_codes(self): @@ -312,5 +357,5 @@ class DarkLangMiddlewareTests(TestCase): self.assertAcceptEquals( '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') + self.process_middleware_request(accept='zh-Hans;q=1.0, zh-Hant-TW;q=0.5, zh-HK;q=0.3') ) diff --git a/common/djangoapps/dark_lang/urls.py b/common/djangoapps/dark_lang/urls.py new file mode 100644 index 0000000000..be3dafc3e4 --- /dev/null +++ b/common/djangoapps/dark_lang/urls.py @@ -0,0 +1,12 @@ +""" +Contains all the URLs for the Dark Language Support App +""" + +from django.conf.urls import patterns, url + +from dark_lang import views + +urlpatterns = patterns( + '', + url(r'^$', views.DarkLangView.as_view(), name='preview_lang'), +) diff --git a/common/djangoapps/dark_lang/views.py b/common/djangoapps/dark_lang/views.py new file mode 100644 index 0000000000..2d7d69aa43 --- /dev/null +++ b/common/djangoapps/dark_lang/views.py @@ -0,0 +1,163 @@ +""" +Views file for the Darklang Django App +""" +from django.contrib.auth.decorators import login_required +from django.utils.decorators import method_decorator +from django.utils.translation import LANGUAGE_SESSION_KEY +from django.utils.translation import ugettext as _ +from django.views.generic.base import View +from openedx.core.djangoapps.user_api.preferences.api import ( + delete_user_preference, get_user_preference, set_user_preference +) +from openedx.core.lib.api.view_utils import view_auth_classes + +from dark_lang import DARK_LANGUAGE_KEY +from dark_lang.models import DarkLangConfig +from edxmako.shortcuts import render_to_response +from lang_pref import LANGUAGE_KEY + +LANGUAGE_INPUT_FIELD = 'preview_lang' + + +@view_auth_classes() +class DarkLangView(View): + """ + View used when a user is attempting to change the preview language using Darklang. + + Expected Behavior: + GET - returns a form for setting/resetting the user's dark language + POST - updates or clears the setting to the given dark language + """ + template_name = 'darklang/preview_lang.html' + + @method_decorator(login_required) + def get(self, request): + """ + Returns the Form for setting/resetting a User's dark language setting + + Arguments: + request (Request): The Django Request Object + + Returns: + HttpResponse: View containing the form for setting the preview lang + """ + context = { + 'disable_courseware_js': True, + 'uses_pattern_library': True + } + return render_to_response(self.template_name, context) + + @method_decorator(login_required) + def post(self, request): + """ + Sets or clears the DarkLang depending on the incoming post data. + + Arguments: + request (Request): The Django Request Object + + Returns: + HttpResponse: View containing the form for setting the preview lang with the status + included in the context + """ + return self.process_darklang_request(request) + + def process_darklang_request(self, request): + """ + Proccess the request to Set or clear the DarkLang depending on the incoming request. + + Arguments: + request (Request): The Django Request Object + + Returns: + HttpResponse: View containing the form for setting the preview lang with the status + included in the context + """ + context = { + 'disable_courseware_js': True, + 'uses_pattern_library': True + } + response = None + if not DarkLangConfig.current().enabled: + message = _('Preview Language is currently disabled') + context.update({'form_submit_message': message}) + context.update({'success': False}) + response = render_to_response(self.template_name, context, request=request) + + elif 'set_language' in request.POST: + # Set the Preview Language + response = self._set_preview_language(request, context) + elif 'reset' in request.POST: + # Reset and clear the language preference + response = self._clear_preview_language(request, context) + return response + + def _set_preview_language(self, request, context): + """ + Set the Preview language + + Arguments: + request (Request): The incoming Django Request + context dict: The basic context for the Response + + Returns: + HttpResponse: View containing the form for setting the preview lang with the status + included in the context + """ + message = None + show_refresh_message = False + + preview_lang = request.POST.get(LANGUAGE_INPUT_FIELD, '') + if not preview_lang.strip(): + message = _('Language code not provided') + else: + # Set the session key to the requested preview lang + request.session[LANGUAGE_SESSION_KEY] = preview_lang + + # Make sure that we set the requested preview lang as the dark lang preference for the + # user, so that the lang_pref middleware doesn't clobber away the dark lang preview. + auth_user = request.user + if auth_user: + set_user_preference(request.user, DARK_LANGUAGE_KEY, preview_lang) + + message = _('Language set to language code: {preview_language_code}').format( + preview_language_code=preview_lang + ) + show_refresh_message = True + context.update({'form_submit_message': message}) + context.update({'success': show_refresh_message}) + response = render_to_response(self.template_name, context) + return response + + def _clear_preview_language(self, request, context): + """ + Clears the dark language preview + + Arguments: + request (Request): The incoming Django Request + context dict: The basic context for the Response + Returns: + HttpResponse: View containing the form for setting the preview lang with the status + included in the context + """ + # delete the session language key (if one is set) + if LANGUAGE_SESSION_KEY in request.session: + del request.session[LANGUAGE_SESSION_KEY] + + user_pref = '' + auth_user = request.user + if auth_user: + # Reset user's dark lang preference to null + delete_user_preference(auth_user, DARK_LANGUAGE_KEY) + # Get & set user's preferred language + user_pref = get_user_preference(auth_user, LANGUAGE_KEY) + if user_pref: + request.session[LANGUAGE_SESSION_KEY] = user_pref + if user_pref is None: + message = _('Language reset to the default language code') + else: + message = _("Language reset to user's preference: {preview_language_code}").format( + preview_language_code=user_pref + ) + context.update({'form_submit_message': message}) + context.update({'success': True}) + return render_to_response(self.template_name, context) diff --git a/common/templates/darklang/preview_lang_include.html b/common/templates/darklang/preview_lang_include.html new file mode 100644 index 0000000000..1fc2ccb020 --- /dev/null +++ b/common/templates/darklang/preview_lang_include.html @@ -0,0 +1,36 @@ +<%! +from django.utils.translation import ugettext as _ +%> +<%page expression_filter="h"/> + +

+ ${_("Preview Language Setting")} +

+
+
+
+
+ +
+
+
+ + +
+ + +
+ +
+ % if not form_submit_message is UNDEFINED: +

${form_submit_message}

+ % if success: +

+ ${_("Please refresh the page to see the changes applied.")} +

+ % endif + % endif +
diff --git a/lms/djangoapps/courseware/tests/test_i18n.py b/lms/djangoapps/courseware/tests/test_i18n.py index 4f8599eafd..263b6ef1f8 100644 --- a/lms/djangoapps/courseware/tests/test_i18n.py +++ b/lms/djangoapps/courseware/tests/test_i18n.py @@ -2,29 +2,34 @@ Tests i18n in courseware """ import re -from nose.plugins.attrib import attr - from django.conf import settings from django.contrib.auth.models import User -from django.core.urlresolvers import reverse, NoReverseMatch +from django.core.urlresolvers import reverse from django.test import TestCase from django.test.client import Client from django.utils import translation +from nose.plugins.attrib import attr +from openedx.core.djangoapps.user_api.preferences.api import set_user_preference from dark_lang.models import DarkLangConfig from lang_pref import LANGUAGE_KEY -from openedx.core.djangoapps.user_api.preferences.api import set_user_preference -from student.tests.factories import UserFactory, RegistrationFactory, UserProfileFactory +from student.tests.factories import UserFactory class BaseI18nTestCase(TestCase): """ Base utilities for i18n test classes to derive from """ + preview_language_url = '/update_lang/' + url = reverse('dashboard') + site_lang = settings.LANGUAGE_CODE + pwd = 'test_password' def setUp(self): super(BaseI18nTestCase, self).setUp() self.addCleanup(translation.deactivate) + self.client = Client() + self.create_user() def assert_tag_has_attr(self, content, tag, attname, value): """Assert that a tag in `content` has a certain value in a certain attribute.""" @@ -47,6 +52,22 @@ class BaseI18nTestCase(TestCase): enabled=True ).save() + def create_user(self): + """ + Creates the user log in + """ + # Create one user and save it to the database + email = 'test@edx.org' + self.user = UserFactory.build(username='test', email=email, password=self.pwd) + self.user.save() + + def user_login(self): + """ + Log the user in + """ + # Get the login url & log in our user + self.client.login(username=self.user.username, password=self.pwd) + @attr('shard_1') class I18nTestCase(BaseI18nTestCase): @@ -96,35 +117,44 @@ class I18nRegressionTests(BaseI18nTestCase): def test_unreleased_lang_resolution(self): # Regression test; LOC-85 self.release_languages('fa') + self.user_login() # We've released 'fa', AND we have language files for 'fa-ir' but # we want to keep 'fa-ir' as a dark language. Requesting 'fa-ir' # in the http request (NOT with the ?preview-lang query param) should # receive files for 'fa' - response = self.client.get('/', HTTP_ACCEPT_LANGUAGE='fa-ir') + response = self.client.get(self.url, HTTP_ACCEPT_LANGUAGE='fa-ir') self.assert_tag_has_attr(response.content, "html", "lang", "fa") # Now try to access with dark lang - response = self.client.get('/?preview-lang=fa-ir') + self.client.post(self.preview_language_url, {'preview_lang': 'fa-ir', 'set_language': 'set_language'}) + response = self.client.get(self.url) self.assert_tag_has_attr(response.content, "html", "lang", "fa-ir") def test_preview_lang(self): + self.user_login() + # Regression test; LOC-87 self.release_languages('es-419') site_lang = settings.LANGUAGE_CODE # Visit the front page; verify we see site default lang - response = self.client.get('/') + response = self.client.get(self.url) self.assert_tag_has_attr(response.content, "html", "lang", site_lang) # Verify we can switch language using the preview-lang query param - response = self.client.get('/?preview-lang=eo') + # Set the language + self.client.post(self.preview_language_url, {'preview_lang': 'eo', 'set_language': 'set_language'}) + + response = self.client.get(self.url) self.assert_tag_has_attr(response.content, "html", "lang", "eo") # We should be able to see released languages using preview-lang, too - response = self.client.get('/?preview-lang=es-419') + self.client.post(self.preview_language_url, {'preview_lang': 'es-419', 'set_language': 'set_language'}) + response = self.client.get(self.url) self.assert_tag_has_attr(response.content, "html", "lang", "es-419") # Clearing the language should go back to site default - response = self.client.get('/?clear-lang') + self.client.post(self.preview_language_url, {'reset': 'reset'}) + response = self.client.get(self.url) self.assert_tag_has_attr(response.content, "html", "lang", site_lang) @@ -137,32 +167,7 @@ class I18nLangPrefTests(BaseI18nTestCase): """ def setUp(self): super(I18nLangPrefTests, self).setUp() - # Create one user and save it to the database - email = 'test@edx.org' - pwd = 'test_password' - self.user = UserFactory.build(username='test', email=email) - self.user.set_password(pwd) - self.user.save() - - # Create a registration for the user - RegistrationFactory(user=self.user) - - # Create a profile for the user - UserProfileFactory(user=self.user) - - # Create the test client - self.client = Client() - - # Get the login url & log in our user - try: - login_url = reverse('login_post') - except NoReverseMatch: - login_url = reverse('login') - self.client.post(login_url, {'email': email, 'password': pwd}) - - # Url and site lang vars for tests to use - self.url = reverse('dashboard') - self.site_lang = settings.LANGUAGE_CODE + self.user_login() def test_lang_preference(self): # Regression test; LOC-87 @@ -190,12 +195,16 @@ class I18nLangPrefTests(BaseI18nTestCase): # Set user language preference set_user_preference(self.user, LANGUAGE_KEY, 'ar') # Verify preview-lang takes precedence - response = self.client.get('{}?preview-lang=eo'.format(self.url)) + self.client.post(self.preview_language_url, {'preview_lang': 'eo', 'set_language': 'set_language'}) + response = self.client.get(self.url) + self.assert_tag_has_attr(response.content, "html", "lang", 'eo') # Hitting another page should keep the dark language set. response = self.client.get(reverse('courses')) self.assert_tag_has_attr(response.content, "html", "lang", "eo") # Clearing language must set language back to preference language - response = self.client.get('{}?clear-lang'.format(self.url)) + self.client.post(self.preview_language_url, {'reset': 'reset'}) + response = self.client.get(self.url) + self.assert_tag_has_attr(response.content, "html", "lang", 'ar') diff --git a/lms/templates/darklang/preview_lang.html b/lms/templates/darklang/preview_lang.html new file mode 100644 index 0000000000..7b781b4743 --- /dev/null +++ b/lms/templates/darklang/preview_lang.html @@ -0,0 +1,15 @@ +## Override the default styles_version to the Pattern Library version (version 2) +<%! main_css = "style-main-v2" %> +<%page expression_filter="h"/> +<%! +from django.utils.translation import ugettext as _ +%> + +<%inherit file="/main.html" /> +<%block name="pagetitle">${_("Preview Language Setting")} +<%block name="nav_skip"> +<%block name="bodyclass">pattern-library + +<%block name="content"> + <%include file="/darklang/preview_lang_include.html" /> + diff --git a/lms/urls.py b/lms/urls.py index 6077e169d8..43f49df378 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -114,6 +114,7 @@ urlpatterns = ( url(r'^course_modes/', include('course_modes.urls')), url(r'^verify_student/', include('verify_student.urls')), + url(r'^update_lang/', include('dark_lang.urls', namespace='darklang')), # URLs for API access management url(r'^api-admin/', include('openedx.core.djangoapps.api_admin.urls', namespace='api_admin')), )