From d1a81d08b3f301c6868e35ab9b8ff3c1a114f9b1 Mon Sep 17 00:00:00 2001 From: Maxim Beder Date: Wed, 27 Oct 2021 20:03:00 +0200 Subject: [PATCH 1/2] fix: fix side-wide language for legacy UI When side-wide language is set via LANGUAGE_CODE site configuration, some elements of legacy UI still use the language set in user preferences (or None, which fallsback to 'en' if nothing is set in preferences). The expectation is that everything should be translated to the language which is set in the site configuration. In legacy UI (i.e. templates) the code of the current language sometimes is needed, and in these cases `user_language` variable from the context is used. The value for that variable is inserted via context processor, which takes the value from the user preferences. The solution modifies the context processor to check if there is a language set in site configuration, and if there is, send that value, instead of whatever value there was in user peferences. --- lms/djangoapps/courseware/context_processor.py | 6 ++++++ .../courseware/tests/test_context_processor.py | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/lms/djangoapps/courseware/context_processor.py b/lms/djangoapps/courseware/context_processor.py index 8444401db806..6b72c764f6c9 100644 --- a/lms/djangoapps/courseware/context_processor.py +++ b/lms/djangoapps/courseware/context_processor.py @@ -7,6 +7,7 @@ """ +from openedx.core.djangoapps.site_configuration.helpers import get_value from openedx.core.djangoapps.user_api.errors import UserAPIInternalError, UserNotFound from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences from openedx.core.lib.cache_utils import get_cache @@ -22,6 +23,8 @@ def user_timezone_locale_prefs(request): """ Checks if request has an authenticated user. If so, sends set (or none if unset) time_zone and language prefs. + If site-wide language is set, that language is used over the language set + in user preferences. This interacts with the DateUtils to either display preferred or attempt to determine system/browser set time_zones and languages @@ -43,6 +46,9 @@ def user_timezone_locale_prefs(request): key: user_preferences.get(pref_name, None) for key, pref_name in RETRIEVABLE_PREFERENCES.items() } + site_wide_language = get_value('LANGUAGE_CODE', None) + if site_wide_language: + user_prefs['user_language'] = site_wide_language cached_value.update(user_prefs) return cached_value diff --git a/lms/djangoapps/courseware/tests/test_context_processor.py b/lms/djangoapps/courseware/tests/test_context_processor.py index 07c9001ab43e..e77b7873f7aa 100644 --- a/lms/djangoapps/courseware/tests/test_context_processor.py +++ b/lms/djangoapps/courseware/tests/test_context_processor.py @@ -3,7 +3,7 @@ """ -from unittest.mock import Mock +from unittest.mock import Mock, patch from django.contrib.auth.models import AnonymousUser from lms.djangoapps.courseware.context_processor import user_timezone_locale_prefs @@ -43,3 +43,10 @@ def test_no_language_preference(self): assert context['user_language'] is None assert context['user_timezone'] is not None assert context['user_timezone'] == 'Asia/Tokyo' + + @patch("lms.djangoapps.courseware.context_processor.get_value") + def test_site_wide_language_set(self, mock_get_value): + mock_get_value.return_value = 'ar' + set_user_preference(self.user, 'pref-lang', 'en') + context = user_timezone_locale_prefs(self.request) + assert context['user_language'] == 'ar' From 1a93925374d15b84b32eefb2dcf92db21b10154f Mon Sep 17 00:00:00 2001 From: Maxim Beder Date: Wed, 27 Oct 2021 21:16:25 +0200 Subject: [PATCH 2/2] fix: fix site-wide language for MFEs When a site-wide language is set in site configurations, the desired result is that MFEs use that language over the one which is set in user preferences (or if None is set, it would fallback to 'en'). Since the MFEs use language value from cookies to determine which language to use, and the existing solution was only modifying the language in the session, and not overwrite the value in the cookies, the MFEs would use language set in users' preferences (or 'en' if nothing was set). This commit removes the existing solution and implements a new one in the middleware that sets language values in cookies based on the values set in users' preferences. The modified middleware is going to check if there is a site-wide language set in site configurations, and if it is, will use that value over the one which is set in users' preferences. Additionally, if site-wide language is set, the middleware won't save the language which is passed in cookies as the user preference, to not overwrite the user preference for other sites, where this site configuration doesn't apply. --- .../core/djangoapps/dark_lang/middleware.py | 10 -- openedx/core/djangoapps/dark_lang/tests.py | 10 -- .../core/djangoapps/lang_pref/middleware.py | 135 +++++++++++------- .../lang_pref/tests/test_middleware.py | 78 ++++++++++ 4 files changed, 159 insertions(+), 74 deletions(-) diff --git a/openedx/core/djangoapps/dark_lang/middleware.py b/openedx/core/djangoapps/dark_lang/middleware.py index 07181a27df60..dd2e8051660f 100644 --- a/openedx/core/djangoapps/dark_lang/middleware.py +++ b/openedx/core/djangoapps/dark_lang/middleware.py @@ -16,7 +16,6 @@ from openedx.core.djangoapps.dark_lang import DARK_LANGUAGE_KEY from openedx.core.djangoapps.dark_lang.models import DarkLangConfig -from openedx.core.djangoapps.site_configuration.helpers import get_value from openedx.core.djangoapps.user_api.preferences.api import get_user_preference # If django 1.7 or higher is used, the right-side can be updated with new-style codes. @@ -91,17 +90,8 @@ def process_request(self, request): return self._clean_accept_headers(request) - self._set_site_or_microsite_language(request) self._activate_preview_language(request) - def _set_site_or_microsite_language(self, request): - """ - Apply language specified in site configuration. - """ - language = get_value('LANGUAGE_CODE', None) - if language: - request.session[LANGUAGE_SESSION_KEY] = language - def _fuzzy_match(self, lang_code): """Returns a fuzzy match for lang_code""" match = None diff --git a/openedx/core/djangoapps/dark_lang/tests.py b/openedx/core/djangoapps/dark_lang/tests.py index cfa578011082..a4ee279e84e3 100644 --- a/openedx/core/djangoapps/dark_lang/tests.py +++ b/openedx/core/djangoapps/dark_lang/tests.py @@ -257,16 +257,6 @@ def _set_client_session_language(self, session_language): session[LANGUAGE_SESSION_KEY] = session_language session.save() - @with_site_configuration(configuration={'LANGUAGE_CODE': 'rel'}) - def test_site_configuration_language(self): - # `LANGUAGE_CODE` in site configuration should override session lang - self._set_client_session_language('notrel') - self.client.get('/home') - self.assert_session_lang_equals( - 'rel', - self.client.session - ) - def test_preview_lang_with_released_language(self): # Preview lang should always override selection self._post_set_preview_lang('rel') diff --git a/openedx/core/djangoapps/lang_pref/middleware.py b/openedx/core/djangoapps/lang_pref/middleware.py index e00dca0e0df3..c2524212f1fd 100644 --- a/openedx/core/djangoapps/lang_pref/middleware.py +++ b/openedx/core/djangoapps/lang_pref/middleware.py @@ -9,6 +9,7 @@ from django.utils.translation.trans_real import parse_accept_lang_header from openedx.core.djangoapps.lang_pref import COOKIE_DURATION, LANGUAGE_HEADER, LANGUAGE_KEY +from openedx.core.djangoapps.site_configuration.helpers import get_value from openedx.core.djangoapps.user_api.errors import UserAPIInternalError, UserAPIRequestError from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, set_user_preference from openedx.core.lib.mobile_utils import is_request_from_mobile_app @@ -26,63 +27,89 @@ def process_request(self, request): """ If a user's UserPreference contains a language preference, use the user's preference. Save the current language preference cookie as the user's preferred language. + + If site-wide language is set, don't use the language from user's + preferences and don't set the value from the cookies as the user's + preffered language. + Instead use the value set as a site-wide language. """ + site_wide_language = get_value('LANGUAGE_CODE', None) + if site_wide_language: + request.session[LANGUAGE_SESSION_KEY] = site_wide_language + self.update_accept_language(request, site_wide_language) + return + cookie_lang = request.COOKIES.get(settings.LANGUAGE_COOKIE, None) - if cookie_lang: - if request.user.is_authenticated: - set_user_preference(request.user, LANGUAGE_KEY, cookie_lang) - else: - request._anonymous_user_cookie_lang = cookie_lang # lint-amnesty, pylint: disable=protected-access - - accept_header = request.META.get(LANGUAGE_HEADER, None) - if accept_header: - current_langs = parse_accept_lang_header(accept_header) - # Promote the cookie_lang over any language currently in the accept header - current_langs = [(lang, qvalue) for (lang, qvalue) in current_langs if lang != cookie_lang] - current_langs.insert(0, (cookie_lang, 1)) - accept_header = ",".join(f"{lang};q={qvalue}" for (lang, qvalue) in current_langs) - else: - accept_header = cookie_lang - request.META[LANGUAGE_HEADER] = accept_header - - # Allow the new cookie setting to update the language in the user's session - if LANGUAGE_SESSION_KEY in request.session and request.session[LANGUAGE_SESSION_KEY] != cookie_lang: - del request.session[LANGUAGE_SESSION_KEY] + if not cookie_lang: + return + + if request.user.is_authenticated: + set_user_preference(request.user, LANGUAGE_KEY, cookie_lang) + else: + request._anonymous_user_cookie_lang = cookie_lang # lint-amnesty, pylint: disable=protected-access + + self.update_accept_language(request, cookie_lang) + + # Allow the new cookie setting to update the language in the user's session + if LANGUAGE_SESSION_KEY in request.session and request.session[LANGUAGE_SESSION_KEY] != cookie_lang: + del request.session[LANGUAGE_SESSION_KEY] def process_response(self, request, response): # lint-amnesty, pylint: disable=missing-function-docstring - # If the user is logged in, check for their language preference. Also check for real user - # if current user is a masquerading user, - user_pref = None - current_user = None - if hasattr(request, 'user'): - current_user = getattr(request.user, 'real_user', request.user) - - if current_user and current_user.is_authenticated: - anonymous_cookie_lang = getattr(request, '_anonymous_user_cookie_lang', None) - if anonymous_cookie_lang: - user_pref = anonymous_cookie_lang - set_user_preference(current_user, LANGUAGE_KEY, anonymous_cookie_lang) - else: - # Get the user's language preference - try: - user_pref = get_user_preference(current_user, LANGUAGE_KEY) - except (UserAPIRequestError, UserAPIInternalError): - # If we can't find the user preferences, then don't modify the cookie - pass - - # If set, set the user_pref in the LANGUAGE_COOKIE - if user_pref and not is_request_from_mobile_app(request): - response.set_cookie( - settings.LANGUAGE_COOKIE, - value=user_pref, - domain=settings.SESSION_COOKIE_DOMAIN, - max_age=COOKIE_DURATION, - secure=request.is_secure() - ) - else: - response.delete_cookie( - settings.LANGUAGE_COOKIE, - domain=settings.SESSION_COOKIE_DOMAIN - ) + site_wide_language = get_value('LANGUAGE_CODE', None) + if site_wide_language: + response.set_cookie( + settings.LANGUAGE_COOKIE, + value=site_wide_language, + domain=settings.SESSION_COOKIE_DOMAIN, + max_age=COOKIE_DURATION, + secure=request.is_secure() + ) + else: + # If the user is logged in, check for their language preference. Also check for real user + # if current user is a masquerading user, + user_pref = None + current_user = None + if hasattr(request, 'user'): + current_user = getattr(request.user, 'real_user', request.user) + + if current_user and current_user.is_authenticated: + anonymous_cookie_lang = getattr(request, '_anonymous_user_cookie_lang', None) + if anonymous_cookie_lang: + user_pref = anonymous_cookie_lang + set_user_preference(current_user, LANGUAGE_KEY, anonymous_cookie_lang) + else: + # Get the user's language preference + try: + user_pref = get_user_preference(current_user, LANGUAGE_KEY) + except (UserAPIRequestError, UserAPIInternalError): + # If we can't find the user preferences, then don't modify the cookie + pass + + # If set, set the user_pref in the LANGUAGE_COOKIE + if user_pref and not is_request_from_mobile_app(request): + response.set_cookie( + settings.LANGUAGE_COOKIE, + value=user_pref, + domain=settings.SESSION_COOKIE_DOMAIN, + max_age=COOKIE_DURATION, + secure=request.is_secure() + ) + else: + response.delete_cookie( + settings.LANGUAGE_COOKIE, + domain=settings.SESSION_COOKIE_DOMAIN + ) return response + + def update_accept_language(self, request, new_lang): + accept_header = request.META.get(LANGUAGE_HEADER, None) + if accept_header: + current_langs = parse_accept_lang_header(accept_header) + # Promote the new_lang over any language currently in the accept header + current_langs = [(lang, qvalue) for (lang, qvalue) in current_langs if lang != new_lang] + current_langs.insert(0, (new_lang, 1)) + accept_header = ",".join(f"{lang};q={qvalue}" for (lang, qvalue) in current_langs) + else: + accept_header = new_lang + request.META[LANGUAGE_HEADER] = accept_header diff --git a/openedx/core/djangoapps/lang_pref/tests/test_middleware.py b/openedx/core/djangoapps/lang_pref/tests/test_middleware.py index b53dda198321..e4d7030c8836 100644 --- a/openedx/core/djangoapps/lang_pref/tests/test_middleware.py +++ b/openedx/core/djangoapps/lang_pref/tests/test_middleware.py @@ -266,3 +266,81 @@ def test_remove_lang_cookie_for_mobile_app(self, mock_get_user_preference, mock_ mock_is_mobile_request.return_value = True response = self.middleware.process_response(self.request, response) response.delete_cookie.assert_called() + + @mock.patch('openedx.core.djangoapps.lang_pref.middleware.get_value') + def test_process_request_uses_site_wide_language_over_user_preferences( + self, + mock_get_value, + ): + """ + Test that process_request method uses site-wide language when set over + the language set in user preferences. + """ + mock_get_value.return_value = 'ar' + + self.middleware.process_request(self.request) + + assert self.request.session.get(LANGUAGE_SESSION_KEY, None) == 'ar' + + @mock.patch('openedx.core.djangoapps.lang_pref.middleware.set_user_preference') + @mock.patch('openedx.core.djangoapps.lang_pref.middleware.get_value') + def test_process_request_does_not_save_lang_from_cookies_when_site_wide_language_is_set( + self, + mock_get_value, + mock_set_user_preference, + ): + """ + Test that process_request method doesn't save language from cookies as + user preference when site-wide language is set. + """ + mock_get_value.return_value = 'ar' + self.request.COOKIES[settings.LANGUAGE_COOKIE] = 'en' + + self.middleware.process_request(self.request) + + mock_set_user_preference.assert_not_called() + + @mock.patch('openedx.core.djangoapps.lang_pref.middleware.LanguagePreferenceMiddleware.update_accept_language') + @mock.patch('openedx.core.djangoapps.lang_pref.middleware.set_user_preference') + @mock.patch('openedx.core.djangoapps.lang_pref.middleware.get_value') + def test_process_request_updates_accept_language( + self, + mock_get_value, + mock_set_user_preference, + mock_update_accept_language, + ): + """ + Test that process_request method updates accept language meta request + header when site-wide language is set. + """ + mock_get_value.return_value = 'ar' + self.request.COOKIES[settings.LANGUAGE_COOKIE] = 'en' + + self.middleware.process_request(self.request) + + mock_update_accept_language.assert_called_with(self.request, 'ar') + + @mock.patch('openedx.core.djangoapps.lang_pref.middleware.get_user_preference') + @mock.patch('openedx.core.djangoapps.lang_pref.middleware.get_value') + def test_process_response_uses_site_wide_language_over_user_preferences( + self, + mock_get_value, + mock_get_user_preference, + ): + """ + Test that process_response method uses site-wide language when set over + the language set in user preferences. + """ + mock_get_user_preference.return_value = 'en' + mock_get_value.return_value = 'ar' + response = mock.Mock(spec=HttpResponse) + + self.middleware.process_response(self.request, response) + + response.set_cookie.assert_called_with( + settings.LANGUAGE_COOKIE, + value='ar', + domain=settings.SESSION_COOKIE_DOMAIN, + max_age=COOKIE_DURATION, + secure=self.request.is_secure(), + )