Merge pull request #14868 from cpennington/learner-542
Store Learner language preferences into a cookie rather than the session
This commit is contained in:
@@ -221,6 +221,8 @@ GIT_REPO_EXPORT_DIR = ENV_TOKENS.get('GIT_REPO_EXPORT_DIR', '/edx/var/edxapp/exp
|
|||||||
# Translation overrides
|
# Translation overrides
|
||||||
LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES)
|
LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES)
|
||||||
LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE)
|
LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE)
|
||||||
|
LANGUAGE_COOKIE = ENV_TOKENS.get('LANGUAGE_COOKIE', LANGUAGE_COOKIE)
|
||||||
|
|
||||||
USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N)
|
USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N)
|
||||||
|
|
||||||
ENV_FEATURES = ENV_TOKENS.get('FEATURES', {})
|
ENV_FEATURES = ENV_TOKENS.get('FEATURES', {})
|
||||||
|
|||||||
@@ -547,6 +547,8 @@ TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_
|
|||||||
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
|
LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
|
||||||
LANGUAGES_BIDI = lms.envs.common.LANGUAGES_BIDI
|
LANGUAGES_BIDI = lms.envs.common.LANGUAGES_BIDI
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE = lms.envs.common.LANGUAGE_COOKIE
|
||||||
|
|
||||||
LANGUAGES = lms.envs.common.LANGUAGES
|
LANGUAGES = lms.envs.common.LANGUAGES
|
||||||
LANGUAGE_DICT = dict(LANGUAGES)
|
LANGUAGE_DICT = dict(LANGUAGES)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ from student.models import CourseEnrollment
|
|||||||
CREATE_LOGON_COOKIE = Signal(providing_args=['user', 'response'])
|
CREATE_LOGON_COOKIE = Signal(providing_args=['user', 'response'])
|
||||||
|
|
||||||
|
|
||||||
def _get_cookie_settings(request):
|
def standard_cookie_settings(request):
|
||||||
""" Returns the common cookie settings (e.g. expiration time). """
|
""" Returns the common cookie settings (e.g. expiration time). """
|
||||||
|
|
||||||
if request.session.get_expire_at_browser_close():
|
if request.session.get_expire_at_browser_close():
|
||||||
@@ -73,7 +73,7 @@ def set_logged_in_cookies(request, response, user):
|
|||||||
HttpResponse
|
HttpResponse
|
||||||
|
|
||||||
"""
|
"""
|
||||||
cookie_settings = _get_cookie_settings(request)
|
cookie_settings = standard_cookie_settings(request)
|
||||||
|
|
||||||
# Backwards compatibility: set the cookie indicating that the user
|
# Backwards compatibility: set the cookie indicating that the user
|
||||||
# is logged in. This is just a boolean value, so it's not very useful.
|
# is logged in. This is just a boolean value, so it's not very useful.
|
||||||
@@ -96,7 +96,7 @@ def set_logged_in_cookies(request, response, user):
|
|||||||
|
|
||||||
def set_user_info_cookie(response, request):
|
def set_user_info_cookie(response, request):
|
||||||
""" Sets the user info cookie on the response. """
|
""" Sets the user info cookie on the response. """
|
||||||
cookie_settings = _get_cookie_settings(request)
|
cookie_settings = standard_cookie_settings(request)
|
||||||
|
|
||||||
# In production, TLS should be enabled so that this cookie is encrypted
|
# In production, TLS should be enabled so that this cookie is encrypted
|
||||||
# when we send it. We also need to set "secure" to True so that the browser
|
# when we send it. We also need to set "secure" to True so that the browser
|
||||||
|
|||||||
@@ -367,7 +367,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest
|
|||||||
self.assertEqual(resp.status_code, 200)
|
self.assertEqual(resp.status_code, 200)
|
||||||
|
|
||||||
def test_num_queries_instructor_paced(self):
|
def test_num_queries_instructor_paced(self):
|
||||||
self.fetch_course_info_with_queries(self.instructor_paced_course, 21, 4)
|
self.fetch_course_info_with_queries(self.instructor_paced_course, 24, 4)
|
||||||
|
|
||||||
def test_num_queries_self_paced(self):
|
def test_num_queries_self_paced(self):
|
||||||
self.fetch_course_info_with_queries(self.self_paced_course, 21, 4)
|
self.fetch_course_info_with_queries(self.self_paced_course, 24, 4)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
Tests i18n in courseware
|
Tests i18n in courseware
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
import re
|
import re
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -14,8 +15,6 @@ from nose.plugins.attrib import attr
|
|||||||
|
|
||||||
from openedx.core.djangoapps.dark_lang.models import DarkLangConfig
|
from openedx.core.djangoapps.dark_lang.models import DarkLangConfig
|
||||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference
|
|
||||||
from student.models import UserProfile
|
|
||||||
from student.tests.factories import UserFactory
|
from student.tests.factories import UserFactory
|
||||||
|
|
||||||
|
|
||||||
@@ -61,8 +60,7 @@ class BaseI18nTestCase(TestCase):
|
|||||||
"""
|
"""
|
||||||
# Create one user and save it to the database
|
# Create one user and save it to the database
|
||||||
email = 'test@edx.org'
|
email = 'test@edx.org'
|
||||||
self.user = UserFactory.build(username='test', email=email, password=self.pwd)
|
self.user = UserFactory.create(username='test', email=email, password=self.pwd)
|
||||||
self.user.save()
|
|
||||||
|
|
||||||
def user_login(self):
|
def user_login(self):
|
||||||
"""
|
"""
|
||||||
@@ -119,7 +117,6 @@ class I18nRegressionTests(BaseI18nTestCase):
|
|||||||
|
|
||||||
def test_unreleased_lang_resolution(self):
|
def test_unreleased_lang_resolution(self):
|
||||||
# Regression test; LOC-85
|
# Regression test; LOC-85
|
||||||
UserProfile.objects.create(user=self.user)
|
|
||||||
self.release_languages('fa')
|
self.release_languages('fa')
|
||||||
self.user_login()
|
self.user_login()
|
||||||
|
|
||||||
@@ -136,7 +133,6 @@ class I18nRegressionTests(BaseI18nTestCase):
|
|||||||
self.assert_tag_has_attr(response.content, "html", "lang", "fa-ir")
|
self.assert_tag_has_attr(response.content, "html", "lang", "fa-ir")
|
||||||
|
|
||||||
def test_preview_lang(self):
|
def test_preview_lang(self):
|
||||||
UserProfile.objects.create(user=self.user)
|
|
||||||
self.user_login()
|
self.user_login()
|
||||||
|
|
||||||
# Regression test; LOC-87
|
# Regression test; LOC-87
|
||||||
@@ -172,9 +168,17 @@ class I18nLangPrefTests(BaseI18nTestCase):
|
|||||||
"""
|
"""
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(I18nLangPrefTests, self).setUp()
|
super(I18nLangPrefTests, self).setUp()
|
||||||
UserProfile.objects.create(user=self.user)
|
|
||||||
self.user_login()
|
self.user_login()
|
||||||
|
|
||||||
|
def set_lang_preference(self, language):
|
||||||
|
"""Sets the user's language preference, allowing the LangPref middleware to operate to set the preference cookie."""
|
||||||
|
response = self.client.patch(
|
||||||
|
reverse('preferences_api', args=[self.user.username]),
|
||||||
|
json.dumps({LANGUAGE_KEY: language}),
|
||||||
|
content_type="application/merge-patch+json"
|
||||||
|
)
|
||||||
|
self.assertEqual(response.status_code, 204)
|
||||||
|
|
||||||
def test_lang_preference(self):
|
def test_lang_preference(self):
|
||||||
# Regression test; LOC-87
|
# Regression test; LOC-87
|
||||||
self.release_languages('ar, es-419')
|
self.release_languages('ar, es-419')
|
||||||
@@ -184,13 +188,13 @@ class I18nLangPrefTests(BaseI18nTestCase):
|
|||||||
self.assert_tag_has_attr(response.content, "html", "lang", self.site_lang)
|
self.assert_tag_has_attr(response.content, "html", "lang", self.site_lang)
|
||||||
|
|
||||||
# Set user language preference
|
# Set user language preference
|
||||||
set_user_preference(self.user, LANGUAGE_KEY, 'ar')
|
self.set_lang_preference('ar')
|
||||||
# and verify we now get an ar response
|
# and verify we now get an ar response
|
||||||
response = self.client.get(self.url)
|
response = self.client.get(self.url)
|
||||||
self.assert_tag_has_attr(response.content, "html", "lang", 'ar')
|
self.assert_tag_has_attr(response.content, "html", "lang", 'ar')
|
||||||
|
|
||||||
# Verify that switching language preference gives the right language
|
# Verify that switching language preference gives the right language
|
||||||
set_user_preference(self.user, LANGUAGE_KEY, 'es-419')
|
self.set_lang_preference('es-419')
|
||||||
response = self.client.get(self.url)
|
response = self.client.get(self.url)
|
||||||
self.assert_tag_has_attr(response.content, "html", "lang", 'es-419')
|
self.assert_tag_has_attr(response.content, "html", "lang", 'es-419')
|
||||||
|
|
||||||
@@ -199,7 +203,7 @@ class I18nLangPrefTests(BaseI18nTestCase):
|
|||||||
self.release_languages('ar, es-419')
|
self.release_languages('ar, es-419')
|
||||||
|
|
||||||
# Set user language preference
|
# Set user language preference
|
||||||
set_user_preference(self.user, LANGUAGE_KEY, 'ar')
|
self.set_lang_preference('ar')
|
||||||
# Verify preview-lang takes precedence
|
# Verify preview-lang takes precedence
|
||||||
self.client.post(self.preview_language_url, {'preview_lang': 'eo', 'set_language': 'set_language'})
|
self.client.post(self.preview_language_url, {'preview_lang': 'eo', 'set_language': 'set_language'})
|
||||||
response = self.client.get(self.url)
|
response = self.client.get(self.url)
|
||||||
|
|||||||
@@ -314,6 +314,8 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE)
|
|||||||
LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES)
|
LANGUAGES = ENV_TOKENS.get('LANGUAGES', LANGUAGES)
|
||||||
LANGUAGE_DICT = dict(LANGUAGES)
|
LANGUAGE_DICT = dict(LANGUAGES)
|
||||||
LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE)
|
LANGUAGE_CODE = ENV_TOKENS.get('LANGUAGE_CODE', LANGUAGE_CODE)
|
||||||
|
LANGUAGE_COOKIE = ENV_TOKENS.get('LANGUAGE_COOKIE', LANGUAGE_COOKIE)
|
||||||
|
|
||||||
USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N)
|
USE_I18N = ENV_TOKENS.get('USE_I18N', USE_I18N)
|
||||||
|
|
||||||
# Additional installed apps
|
# Additional installed apps
|
||||||
|
|||||||
@@ -863,6 +863,8 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
|
|||||||
# these languages display right to left
|
# these languages display right to left
|
||||||
LANGUAGES_BIDI = ("he", "ar", "fa", "ur", "fa-ir", "rtl")
|
LANGUAGES_BIDI = ("he", "ar", "fa", "ur", "fa-ir", "rtl")
|
||||||
|
|
||||||
|
LANGUAGE_COOKIE = "openedx-language-preference"
|
||||||
|
|
||||||
# Sourced from http://www.localeplanet.com/icu/ and wikipedia
|
# Sourced from http://www.localeplanet.com/icu/ and wikipedia
|
||||||
LANGUAGES = (
|
LANGUAGES = (
|
||||||
('en', u'English'),
|
('en', u'English'),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ FEATURES.update({
|
|||||||
'ENABLE_COURSEWARE_SEARCH': False,
|
'ENABLE_COURSEWARE_SEARCH': False,
|
||||||
'ENABLE_COURSE_DISCOVERY': False,
|
'ENABLE_COURSE_DISCOVERY': False,
|
||||||
'ENABLE_DASHBOARD_SEARCH': False,
|
'ENABLE_DASHBOARD_SEARCH': False,
|
||||||
|
'SHOW_LANGUAGE_SELECTOR': True
|
||||||
})
|
})
|
||||||
|
|
||||||
ENABLE_MKTG_SITE = os.environ.get('ENABLE_MARKETING_SITE', False)
|
ENABLE_MKTG_SITE = os.environ.get('ENABLE_MARKETING_SITE', False)
|
||||||
|
|||||||
@@ -4,3 +4,7 @@ Useful information for setting the language preference
|
|||||||
|
|
||||||
# this is the UserPreference key for the user's preferred language
|
# this is the UserPreference key for the user's preferred language
|
||||||
LANGUAGE_KEY = 'pref-lang'
|
LANGUAGE_KEY = 'pref-lang'
|
||||||
|
|
||||||
|
LANGUAGE_HEADER = 'HTTP_ACCEPT_LANGUAGE'
|
||||||
|
|
||||||
|
COOKIE_DURATION = 14 * 24 * 60 * 60 # 14 days in seconds
|
||||||
|
|||||||
@@ -2,12 +2,17 @@
|
|||||||
Middleware for Language Preferences
|
Middleware for Language Preferences
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.utils.translation import LANGUAGE_SESSION_KEY
|
from django.utils.translation import LANGUAGE_SESSION_KEY
|
||||||
from django.utils.translation.trans_real import parse_accept_lang_header
|
from django.utils.translation.trans_real import parse_accept_lang_header
|
||||||
|
|
||||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
from openedx.core.djangoapps.lang_pref import (
|
||||||
from openedx.core.djangoapps.lang_pref.api import released_languages
|
LANGUAGE_KEY, LANGUAGE_HEADER, COOKIE_DURATION
|
||||||
from openedx.core.djangoapps.user_api.preferences.api import get_user_preference, delete_user_preference
|
)
|
||||||
|
from openedx.core.djangoapps.user_api.preferences.api import (
|
||||||
|
get_user_preference, delete_user_preference, set_user_preference
|
||||||
|
)
|
||||||
|
from openedx.core.djangoapps.user_api.errors import UserAPIInternalError, UserAPIRequestError
|
||||||
|
|
||||||
|
|
||||||
class LanguagePreferenceMiddleware(object):
|
class LanguagePreferenceMiddleware(object):
|
||||||
@@ -21,17 +26,46 @@ class LanguagePreferenceMiddleware(object):
|
|||||||
def process_request(self, request):
|
def process_request(self, request):
|
||||||
"""
|
"""
|
||||||
If a user's UserPreference contains a language preference, use the user's preference.
|
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.
|
||||||
"""
|
"""
|
||||||
languages = released_languages()
|
cookie_lang = request.COOKIES.get(settings.LANGUAGE_COOKIE, None)
|
||||||
system_released_languages = [seq[0] for seq in languages]
|
if cookie_lang:
|
||||||
|
if request.user.is_authenticated():
|
||||||
|
set_user_preference(request.user, LANGUAGE_KEY, cookie_lang)
|
||||||
|
|
||||||
|
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("{};q={}".format(lang, qvalue) for (lang, qvalue) in current_langs)
|
||||||
|
else:
|
||||||
|
accept_header = cookie_lang
|
||||||
|
request.META[LANGUAGE_HEADER] = accept_header
|
||||||
|
|
||||||
|
def process_response(self, request, response):
|
||||||
# If the user is logged in, check for their language preference
|
# If the user is logged in, check for their language preference
|
||||||
if request.user.is_authenticated():
|
if getattr(request, 'user', None) and request.user.is_authenticated():
|
||||||
# Get the user's language preference
|
# Get the user's language preference
|
||||||
user_pref = get_user_preference(request.user, LANGUAGE_KEY)
|
try:
|
||||||
# Set it to the LANGUAGE_SESSION_KEY (Django-specific session setting governing language pref)
|
user_pref = get_user_preference(request.user, LANGUAGE_KEY)
|
||||||
if user_pref:
|
except (UserAPIRequestError, UserAPIInternalError):
|
||||||
if user_pref in system_released_languages:
|
# If we can't find the user preferences, then don't modify the cookie
|
||||||
request.session[LANGUAGE_SESSION_KEY] = user_pref
|
pass
|
||||||
|
else:
|
||||||
|
# Set it in the LANGUAGE_COOKIE
|
||||||
|
if user_pref:
|
||||||
|
response.set_cookie(
|
||||||
|
settings.LANGUAGE_COOKIE,
|
||||||
|
value=user_pref,
|
||||||
|
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||||
|
max_age=COOKIE_DURATION,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
delete_user_preference(request.user, LANGUAGE_KEY)
|
response.delete_cookie(
|
||||||
|
settings.LANGUAGE_COOKIE,
|
||||||
|
domain=settings.SESSION_COOKIE_DOMAIN
|
||||||
|
)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|||||||
@@ -2,17 +2,22 @@
|
|||||||
Tests for lang_pref middleware.
|
Tests for lang_pref middleware.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import itertools
|
||||||
import mock
|
import mock
|
||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
|
from django.conf import settings
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
|
from django.core.urlresolvers import reverse
|
||||||
from django.test.client import RequestFactory
|
from django.test.client import RequestFactory
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.contrib.sessions.middleware import SessionMiddleware
|
from django.contrib.sessions.middleware import SessionMiddleware
|
||||||
from django.utils.translation import LANGUAGE_SESSION_KEY
|
from django.utils.translation import LANGUAGE_SESSION_KEY
|
||||||
|
from django.utils.translation.trans_real import parse_accept_lang_header
|
||||||
|
|
||||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY, COOKIE_DURATION
|
||||||
from openedx.core.djangoapps.lang_pref.middleware import LanguagePreferenceMiddleware
|
from openedx.core.djangoapps.lang_pref.middleware import LanguagePreferenceMiddleware
|
||||||
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference, get_user_preference
|
from openedx.core.djangoapps.user_api.preferences.api import set_user_preference, get_user_preference, delete_user_preference
|
||||||
from student.tests.factories import UserFactory
|
from student.tests.factories import UserFactory
|
||||||
from student.tests.factories import AnonymousUserFactory
|
from student.tests.factories import AnonymousUserFactory
|
||||||
|
|
||||||
@@ -34,73 +39,134 @@ class TestUserPreferenceMiddleware(TestCase):
|
|||||||
self.request.META['HTTP_ACCEPT_LANGUAGE'] = 'ar;q=1.0' # pylint: disable=no-member
|
self.request.META['HTTP_ACCEPT_LANGUAGE'] = 'ar;q=1.0' # pylint: disable=no-member
|
||||||
self.session_middleware.process_request(self.request)
|
self.session_middleware.process_request(self.request)
|
||||||
|
|
||||||
def test_no_language_set_in_session_or_prefs(self):
|
def test_logout_shouldnt_remove_cookie(self):
|
||||||
# nothing set in the session or the prefs
|
|
||||||
self.middleware.process_request(self.request)
|
|
||||||
self.assertNotIn(LANGUAGE_SESSION_KEY, self.request.session) # pylint: disable=no-member
|
|
||||||
|
|
||||||
@mock.patch(
|
|
||||||
'openedx.core.djangoapps.lang_pref.middleware.released_languages',
|
|
||||||
mock.Mock(return_value=[('eo', 'esperanto')])
|
|
||||||
)
|
|
||||||
def test_language_in_user_prefs(self):
|
|
||||||
# language set in the user preferences and not the session
|
|
||||||
set_user_preference(self.user, LANGUAGE_KEY, 'eo')
|
|
||||||
self.middleware.process_request(self.request)
|
|
||||||
self.assertEquals(self.request.session[LANGUAGE_SESSION_KEY], 'eo') # pylint: disable=no-member
|
|
||||||
|
|
||||||
@mock.patch(
|
|
||||||
'openedx.core.djangoapps.lang_pref.middleware.released_languages',
|
|
||||||
mock.Mock(return_value=[('en', 'english'), ('eo', 'esperanto')])
|
|
||||||
)
|
|
||||||
def test_language_in_session(self):
|
|
||||||
# language set in both the user preferences and session,
|
|
||||||
# preference should get precedence. The session will hold the last value,
|
|
||||||
# which is probably the user's last preference. Look up the updated preference.
|
|
||||||
|
|
||||||
# Dark lang middleware should run after this middleware, so it can
|
|
||||||
# set a session language as an override of the user's preference.
|
|
||||||
self.request.session[LANGUAGE_SESSION_KEY] = 'en' # pylint: disable=no-member
|
|
||||||
set_user_preference(self.user, LANGUAGE_KEY, 'eo')
|
|
||||||
self.middleware.process_request(self.request)
|
self.middleware.process_request(self.request)
|
||||||
|
|
||||||
self.assertEquals(self.request.session[LANGUAGE_SESSION_KEY], 'eo') # pylint: disable=no-member
|
|
||||||
|
|
||||||
@mock.patch(
|
|
||||||
'openedx.core.djangoapps.lang_pref.middleware.released_languages',
|
|
||||||
mock.Mock(return_value=[('eo', 'dummy Esperanto'), ('ar', 'arabic'), ('eu-es', 'euskara (Espainia)')])
|
|
||||||
)
|
|
||||||
@ddt.data('ar;q=1.0', 'eu;q=1.0', 'es-419;q=1.0')
|
|
||||||
def test_browser_language_in_session_for_unauthenticated_user(self, accept_language):
|
|
||||||
"""
|
|
||||||
test: browser language should not be set in user session for unauthenticated user.
|
|
||||||
"""
|
|
||||||
self.request.META['HTTP_ACCEPT_LANGUAGE'] = accept_language # pylint: disable=no-member
|
|
||||||
self.request.user = self.anonymous_user
|
self.request.user = self.anonymous_user
|
||||||
self.middleware.process_request(self.request)
|
|
||||||
self.assertNotIn(LANGUAGE_SESSION_KEY, self.request.session) # pylint: disable=no-member
|
|
||||||
|
|
||||||
@mock.patch(
|
response = mock.Mock(spec=HttpResponse)
|
||||||
'openedx.core.djangoapps.lang_pref.middleware.released_languages',
|
self.middleware.process_response(self.request, response)
|
||||||
mock.Mock(return_value=[('en', 'english')])
|
|
||||||
)
|
|
||||||
def test_browser_language_not_be_in_session(self):
|
|
||||||
"""
|
|
||||||
test: browser language should not be set in user session if it is not supported by system.
|
|
||||||
"""
|
|
||||||
self.request.user = self.anonymous_user
|
|
||||||
self.middleware.process_request(self.request)
|
|
||||||
self.assertNotEqual(self.request.session.get(LANGUAGE_SESSION_KEY), 'ar') # pylint: disable=no-member
|
|
||||||
|
|
||||||
@mock.patch(
|
response.delete_cookie.assert_not_called()
|
||||||
'openedx.core.djangoapps.lang_pref.middleware.released_languages',
|
|
||||||
mock.Mock(return_value=[('en', 'english'), ('ar', 'arabic')])
|
@ddt.data(None, 'es', 'en')
|
||||||
)
|
def test_preference_setting_changes_cookie(self, lang_pref_out):
|
||||||
def test_delete_user_lang_preference_not_supported_by_system(self):
|
|
||||||
"""
|
"""
|
||||||
test: user preferred language has been removed from user preferences model if it is not supported by system
|
Test that the LANGUAGE_COOKIE is always set to the user's current language preferences
|
||||||
for authenticated users.
|
at the end of the request, with an expiry that's the same as the users current session cookie.
|
||||||
"""
|
"""
|
||||||
set_user_preference(self.user, LANGUAGE_KEY, 'eo')
|
if lang_pref_out:
|
||||||
|
set_user_preference(self.user, LANGUAGE_KEY, lang_pref_out)
|
||||||
|
else:
|
||||||
|
delete_user_preference(self.user, LANGUAGE_KEY)
|
||||||
|
|
||||||
|
response = mock.Mock(spec=HttpResponse)
|
||||||
|
self.middleware.process_response(self.request, response)
|
||||||
|
|
||||||
|
if lang_pref_out:
|
||||||
|
response.set_cookie.assert_called_with(
|
||||||
|
settings.LANGUAGE_COOKIE,
|
||||||
|
value=lang_pref_out,
|
||||||
|
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||||
|
max_age=COOKIE_DURATION,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
response.delete_cookie.assert_called_with(
|
||||||
|
settings.LANGUAGE_COOKIE,
|
||||||
|
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertNotIn(LANGUAGE_SESSION_KEY, self.request.session)
|
||||||
|
|
||||||
|
@ddt.data(*itertools.product(
|
||||||
|
(None, 'eo', 'es'), # LANGUAGE_COOKIE
|
||||||
|
(None, 'es', 'en'), # Language Preference In
|
||||||
|
))
|
||||||
|
@ddt.unpack
|
||||||
|
@mock.patch('openedx.core.djangoapps.lang_pref.middleware.set_user_preference')
|
||||||
|
def test_preference_cookie_changes_setting(self, lang_cookie, lang_pref_in, mock_set_user_preference):
|
||||||
|
self.request.COOKIES[settings.LANGUAGE_COOKIE] = lang_cookie
|
||||||
|
|
||||||
|
if lang_pref_in:
|
||||||
|
set_user_preference(self.user, LANGUAGE_KEY, lang_pref_in)
|
||||||
|
else:
|
||||||
|
delete_user_preference(self.user, LANGUAGE_KEY)
|
||||||
|
|
||||||
self.middleware.process_request(self.request)
|
self.middleware.process_request(self.request)
|
||||||
self.assertEqual(get_user_preference(self.request.user, LANGUAGE_KEY), None)
|
|
||||||
|
if lang_cookie is None:
|
||||||
|
self.assertEqual(mock_set_user_preference.mock_calls, [])
|
||||||
|
else:
|
||||||
|
mock_set_user_preference.assert_called_with(self.user, LANGUAGE_KEY, lang_cookie)
|
||||||
|
|
||||||
|
@ddt.data(*(
|
||||||
|
(logged_in, ) + test_def
|
||||||
|
for logged_in in (True, False)
|
||||||
|
for test_def in [
|
||||||
|
# (LANGUAGE_COOKIE, LANGUAGE_SESSION_KEY, Accept-Language In, Accept-Language Out)
|
||||||
|
(None, None, None, None),
|
||||||
|
(None, 'eo', None, None),
|
||||||
|
(None, 'eo', 'en', 'en'),
|
||||||
|
(None, None, 'en', 'en'),
|
||||||
|
('en', None, None, 'en'),
|
||||||
|
('en', None, 'eo', 'en;q=1.0,eo'),
|
||||||
|
('en', None, 'en', 'en'),
|
||||||
|
('en', 'eo', 'en', 'en'),
|
||||||
|
('en', 'eo', 'eo', 'en;q=1.0,eo')
|
||||||
|
]
|
||||||
|
))
|
||||||
|
@ddt.unpack
|
||||||
|
def test_preference_cookie_overrides_browser(self, logged_in, lang_cookie, lang_session, accept_lang_in, accept_lang_out):
|
||||||
|
if not logged_in:
|
||||||
|
self.request.user = self.anonymous_user
|
||||||
|
if lang_cookie:
|
||||||
|
self.request.COOKIES[settings.LANGUAGE_COOKIE] = lang_cookie
|
||||||
|
if lang_session:
|
||||||
|
self.request.session[LANGUAGE_SESSION_KEY] = lang_session
|
||||||
|
if accept_lang_in:
|
||||||
|
self.request.META['HTTP_ACCEPT_LANGUAGE'] = accept_lang_in
|
||||||
|
else:
|
||||||
|
del self.request.META['HTTP_ACCEPT_LANGUAGE']
|
||||||
|
|
||||||
|
self.middleware.process_request(self.request)
|
||||||
|
|
||||||
|
accept_lang_result = self.request.META.get('HTTP_ACCEPT_LANGUAGE')
|
||||||
|
if accept_lang_result:
|
||||||
|
accept_lang_result = parse_accept_lang_header(accept_lang_result)
|
||||||
|
|
||||||
|
if accept_lang_out:
|
||||||
|
accept_lang_out = parse_accept_lang_header(accept_lang_out)
|
||||||
|
|
||||||
|
if accept_lang_out and accept_lang_result:
|
||||||
|
self.assertItemsEqual(accept_lang_result, accept_lang_out)
|
||||||
|
else:
|
||||||
|
self.assertEqual(accept_lang_result, accept_lang_out)
|
||||||
|
|
||||||
|
self.assertEquals(self.request.session.get(LANGUAGE_SESSION_KEY), lang_session)
|
||||||
|
|
||||||
|
@ddt.data(None, 'es', 'en')
|
||||||
|
def test_logout_preserves_cookie(self, lang_cookie):
|
||||||
|
if lang_cookie:
|
||||||
|
self.client.cookies[settings.LANGUAGE_COOKIE] = lang_cookie
|
||||||
|
elif settings.LANGUAGE_COOKIE in self.client.cookies:
|
||||||
|
del self.client.cookies[settings.LANGUAGE_COOKIE]
|
||||||
|
# Use an actual call to the logout endpoint, because the logout function
|
||||||
|
# explicitly clears all cookies
|
||||||
|
self.client.get(reverse('logout'))
|
||||||
|
if lang_cookie:
|
||||||
|
self.assertEqual(
|
||||||
|
self.client.cookies[settings.LANGUAGE_COOKIE].value,
|
||||||
|
lang_cookie
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.assertNotIn(settings.LANGUAGE_COOKIE, self.client.cookies)
|
||||||
|
|
||||||
|
def test_process_response_no_user_noop(self):
|
||||||
|
del self.request.user
|
||||||
|
response = mock.Mock(spec=HttpResponse)
|
||||||
|
|
||||||
|
result = self.middleware.process_response(self.request, response)
|
||||||
|
|
||||||
|
self.assertIs(result, response)
|
||||||
|
self.assertEqual(response.mock_calls, [])
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from django.http import HttpResponse
|
|||||||
from django.views.decorators.csrf import ensure_csrf_cookie
|
from django.views.decorators.csrf import ensure_csrf_cookie
|
||||||
from django.utils.translation import LANGUAGE_SESSION_KEY
|
from django.utils.translation import LANGUAGE_SESSION_KEY
|
||||||
|
|
||||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY, COOKIE_DURATION
|
||||||
|
|
||||||
|
|
||||||
@ensure_csrf_cookie
|
@ensure_csrf_cookie
|
||||||
@@ -17,9 +17,16 @@ def update_session_language(request):
|
|||||||
"""
|
"""
|
||||||
Update the language session key.
|
Update the language session key.
|
||||||
"""
|
"""
|
||||||
|
response = HttpResponse(200)
|
||||||
if request.method == 'PATCH':
|
if request.method == 'PATCH':
|
||||||
data = json.loads(request.body)
|
data = json.loads(request.body)
|
||||||
language = data.get(LANGUAGE_KEY, settings.LANGUAGE_CODE)
|
language = data.get(LANGUAGE_KEY, settings.LANGUAGE_CODE)
|
||||||
if request.session.get(LANGUAGE_SESSION_KEY, None) != language:
|
if request.session.get(LANGUAGE_SESSION_KEY, None) != language:
|
||||||
request.session[LANGUAGE_SESSION_KEY] = unicode(language)
|
request.session[LANGUAGE_SESSION_KEY] = unicode(language)
|
||||||
return HttpResponse(200)
|
response.set_cookie(
|
||||||
|
settings.LANGUAGE_COOKIE,
|
||||||
|
language,
|
||||||
|
domain=settings.SESSION_COOKIE_DOMAIN,
|
||||||
|
max_age=COOKIE_DURATION
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from collections import defaultdict
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
|
import traceback
|
||||||
|
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.core.serializers.json import DjangoJSONEncoder
|
from django.core.serializers.json import DjangoJSONEncoder
|
||||||
@@ -65,16 +66,19 @@ def intercept_errors(api_error, ignore_errors=None):
|
|||||||
LOGGER.warning(msg)
|
LOGGER.warning(msg)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
caller = traceback.format_stack(limit=2)[0]
|
||||||
|
|
||||||
# Otherwise, log the error and raise the API-specific error
|
# Otherwise, log the error and raise the API-specific error
|
||||||
msg = (
|
msg = (
|
||||||
u"An unexpected error occurred when calling '{func_name}' "
|
u"An unexpected error occurred when calling '{func_name}' "
|
||||||
u"with arguments '{args}' and keyword arguments '{kwargs}': "
|
u"with arguments '{args}' and keyword arguments '{kwargs}' from {caller}: "
|
||||||
u"{exception}"
|
u"{exception}"
|
||||||
).format(
|
).format(
|
||||||
func_name=func.func_name,
|
func_name=func.func_name,
|
||||||
args=args,
|
args=args,
|
||||||
kwargs=kwargs,
|
kwargs=kwargs,
|
||||||
exception=ex.developer_message if hasattr(ex, 'developer_message') else repr(ex)
|
exception=ex.developer_message if hasattr(ex, 'developer_message') else repr(ex),
|
||||||
|
caller=caller.strip()
|
||||||
)
|
)
|
||||||
LOGGER.exception(msg)
|
LOGGER.exception(msg)
|
||||||
raise api_error(msg)
|
raise api_error(msg)
|
||||||
|
|||||||
@@ -22,12 +22,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer):
|
|||||||
"""
|
"""
|
||||||
Return the name attribute from the user profile object if profile exists else none
|
Return the name attribute from the user profile object if profile exists else none
|
||||||
"""
|
"""
|
||||||
try:
|
return user.profile.name
|
||||||
profile = UserProfile.objects.get(user=user)
|
|
||||||
except UserProfile.DoesNotExist:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return profile.name
|
|
||||||
|
|
||||||
def get_preferences(self, user):
|
def get_preferences(self, user):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
Tests for helper functions.
|
Tests for helper functions.
|
||||||
"""
|
"""
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
import mock
|
import mock
|
||||||
import ddt
|
import ddt
|
||||||
from django import forms
|
from django import forms
|
||||||
@@ -52,22 +53,35 @@ class InterceptErrorsTest(TestCase):
|
|||||||
|
|
||||||
@mock.patch('openedx.core.djangoapps.user_api.helpers.LOGGER')
|
@mock.patch('openedx.core.djangoapps.user_api.helpers.LOGGER')
|
||||||
def test_logs_errors(self, mock_logger):
|
def test_logs_errors(self, mock_logger):
|
||||||
|
self.maxDiff = None
|
||||||
exception = 'openedx.core.djangoapps.user_api.tests.test_helpers.FakeInputException'
|
exception = 'openedx.core.djangoapps.user_api.tests.test_helpers.FakeInputException'
|
||||||
expected_log_msg = (
|
expected_log_msg = (
|
||||||
u"An unexpected error occurred when calling 'intercepted_function' with arguments '()' and "
|
u"An unexpected error occurred when calling 'intercepted_function' with arguments '()' and "
|
||||||
u"keyword arguments '{'raise_error': <class '" + exception + u"'>}': FakeInputException()"
|
u"keyword arguments '{{'raise_error': <class '{}'>}}' "
|
||||||
)
|
u"from File \"{}\", line XXX, in test_logs_errors\n"
|
||||||
|
u" intercepted_function(raise_error=FakeInputException): FakeInputException()"
|
||||||
|
).format(exception, __file__)
|
||||||
|
|
||||||
# Verify that the raised exception has the error message
|
# Verify that the raised exception has the error message
|
||||||
try:
|
try:
|
||||||
intercepted_function(raise_error=FakeInputException)
|
intercepted_function(raise_error=FakeInputException)
|
||||||
except FakeOutputException as ex:
|
except FakeOutputException as ex:
|
||||||
self.assertEqual(ex.message, expected_log_msg)
|
actual_message = re.sub(r'line \d+', 'line XXX', ex.message, flags=re.MULTILINE)
|
||||||
|
self.assertEqual(actual_message, expected_log_msg)
|
||||||
|
|
||||||
# Verify that the error logger is called
|
# Verify that the error logger is called
|
||||||
# This will include the stack trace for the original exception
|
# This will include the stack trace for the original exception
|
||||||
# because it's called with log level "ERROR"
|
# because it's called with log level "ERROR"
|
||||||
mock_logger.exception.assert_called_once_with(expected_log_msg)
|
calls = mock_logger.exception.mock_calls
|
||||||
|
self.assertEqual(len(calls), 1)
|
||||||
|
name, args, kwargs = calls[0]
|
||||||
|
|
||||||
|
self.assertEqual(name, '')
|
||||||
|
self.assertEqual(len(args), 1)
|
||||||
|
self.assertEqual(kwargs, {})
|
||||||
|
|
||||||
|
actual_message = re.sub(r'line \d+', 'line XXX', args[0], flags=re.MULTILINE)
|
||||||
|
self.assertEqual(actual_message, expected_log_msg)
|
||||||
|
|
||||||
|
|
||||||
class FormDescriptionTest(TestCase):
|
class FormDescriptionTest(TestCase):
|
||||||
|
|||||||
@@ -956,7 +956,7 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet):
|
|||||||
"""
|
"""
|
||||||
authentication_classes = (authentication.SessionAuthentication,)
|
authentication_classes = (authentication.SessionAuthentication,)
|
||||||
permission_classes = (ApiKeyHeaderPermission,)
|
permission_classes = (ApiKeyHeaderPermission,)
|
||||||
queryset = User.objects.all().prefetch_related("preferences")
|
queryset = User.objects.all().prefetch_related("preferences").select_related("profile")
|
||||||
serializer_class = UserSerializer
|
serializer_class = UserSerializer
|
||||||
paginate_by = 10
|
paginate_by = 10
|
||||||
paginate_by_param = "page_size"
|
paginate_by_param = "page_size"
|
||||||
@@ -982,7 +982,7 @@ class ForumRoleUsersListView(generics.ListAPIView):
|
|||||||
raise ParseError('course_id must be specified')
|
raise ParseError('course_id must be specified')
|
||||||
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id_string)
|
course_id = SlashSeparatedCourseKey.from_deprecated_string(course_id_string)
|
||||||
role = Role.objects.get_or_create(course_id=course_id, name=name)[0]
|
role = Role.objects.get_or_create(course_id=course_id, name=name)[0]
|
||||||
users = role.users.all()
|
users = role.users.prefetch_related("preferences").select_related("profile").all()
|
||||||
return users
|
return users
|
||||||
|
|
||||||
|
|
||||||
@@ -1011,7 +1011,7 @@ class PreferenceUsersListView(generics.ListAPIView):
|
|||||||
paginate_by_param = "page_size"
|
paginate_by_param = "page_size"
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return User.objects.filter(preferences__key=self.kwargs["pref_key"]).prefetch_related("preferences")
|
return User.objects.filter(preferences__key=self.kwargs["pref_key"]).prefetch_related("preferences").select_related("profile")
|
||||||
|
|
||||||
|
|
||||||
class UpdateEmailOptInPreference(APIView):
|
class UpdateEmailOptInPreference(APIView):
|
||||||
|
|||||||
Reference in New Issue
Block a user