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' 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(), + )