Commit 3deb3a3c by Calen Pennington Committed by GitHub

Merge pull request #14977 from cpennington/cale/fix-learner-542

Cale/fix learner 542
parents 09593998 2ca367a1
...@@ -221,6 +221,8 @@ GIT_REPO_EXPORT_DIR = ENV_TOKENS.get('GIT_REPO_EXPORT_DIR', '/edx/var/edxapp/exp ...@@ -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_ ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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
......
...@@ -231,18 +231,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase): ...@@ -231,18 +231,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
# # of sql queries to default, # # of sql queries to default,
# # of mongo queries, # # of mongo queries,
# ) # )
('no_overrides', 1, True, False): (25, 1), ('no_overrides', 1, True, False): (23, 1),
('no_overrides', 2, True, False): (25, 1), ('no_overrides', 2, True, False): (23, 1),
('no_overrides', 3, True, False): (25, 1), ('no_overrides', 3, True, False): (23, 1),
('ccx', 1, True, False): (25, 1), ('ccx', 1, True, False): (23, 1),
('ccx', 2, True, False): (25, 1), ('ccx', 2, True, False): (23, 1),
('ccx', 3, True, False): (25, 1), ('ccx', 3, True, False): (23, 1),
('no_overrides', 1, False, False): (25, 1), ('no_overrides', 1, False, False): (23, 1),
('no_overrides', 2, False, False): (25, 1), ('no_overrides', 2, False, False): (23, 1),
('no_overrides', 3, False, False): (25, 1), ('no_overrides', 3, False, False): (23, 1),
('ccx', 1, False, False): (25, 1), ('ccx', 1, False, False): (23, 1),
('ccx', 2, False, False): (25, 1), ('ccx', 2, False, False): (23, 1),
('ccx', 3, False, False): (25, 1), ('ccx', 3, False, False): (23, 1),
} }
...@@ -254,19 +254,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase): ...@@ -254,19 +254,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
__test__ = True __test__ = True
TEST_DATA = { TEST_DATA = {
('no_overrides', 1, True, False): (25, 3), ('no_overrides', 1, True, False): (23, 3),
('no_overrides', 2, True, False): (25, 3), ('no_overrides', 2, True, False): (23, 3),
('no_overrides', 3, True, False): (25, 3), ('no_overrides', 3, True, False): (23, 3),
('ccx', 1, True, False): (25, 3), ('ccx', 1, True, False): (23, 3),
('ccx', 2, True, False): (25, 3), ('ccx', 2, True, False): (23, 3),
('ccx', 3, True, False): (25, 3), ('ccx', 3, True, False): (23, 3),
('ccx', 1, True, True): (26, 3), ('ccx', 1, True, True): (24, 3),
('ccx', 2, True, True): (26, 3), ('ccx', 2, True, True): (24, 3),
('ccx', 3, True, True): (26, 3), ('ccx', 3, True, True): (24, 3),
('no_overrides', 1, False, False): (25, 3), ('no_overrides', 1, False, False): (23, 3),
('no_overrides', 2, False, False): (25, 3), ('no_overrides', 2, False, False): (23, 3),
('no_overrides', 3, False, False): (25, 3), ('no_overrides', 3, False, False): (23, 3),
('ccx', 1, False, False): (25, 3), ('ccx', 1, False, False): (23, 3),
('ccx', 2, False, False): (25, 3), ('ccx', 2, False, False): (23, 3),
('ccx', 3, False, False): (25, 3), ('ccx', 3, False, False): (23, 3),
} }
...@@ -367,7 +367,7 @@ class SelfPacedCourseInfoTestCase(LoginEnrollmentTestCase, SharedModuleStoreTest ...@@ -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, 18, 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, 18, 4)
...@@ -2,6 +2,7 @@ ...@@ -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 ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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): ...@@ -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)
......
...@@ -206,8 +206,8 @@ class IndexQueryTestCase(ModuleStoreTestCase): ...@@ -206,8 +206,8 @@ class IndexQueryTestCase(ModuleStoreTestCase):
NUM_PROBLEMS = 20 NUM_PROBLEMS = 20
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 10, 147), (ModuleStoreEnum.Type.mongo, 10, 141),
(ModuleStoreEnum.Type.split, 4, 147), (ModuleStoreEnum.Type.split, 4, 141),
) )
@ddt.unpack @ddt.unpack
def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count): def test_index_query_counts(self, store_type, expected_mongo_query_count, expected_mysql_query_count):
...@@ -1408,12 +1408,12 @@ class ProgressPageTests(ModuleStoreTestCase): ...@@ -1408,12 +1408,12 @@ class ProgressPageTests(ModuleStoreTestCase):
"""Test that query counts remain the same for self-paced and instructor-paced courses.""" """Test that query counts remain the same for self-paced and instructor-paced courses."""
SelfPacedConfiguration(enabled=self_paced_enabled).save() SelfPacedConfiguration(enabled=self_paced_enabled).save()
self.setup_course(self_paced=self_paced) self.setup_course(self_paced=self_paced)
with self.assertNumQueries(42), check_mongo_calls(1): with self.assertNumQueries(39), check_mongo_calls(1):
self._get_progress_page() self._get_progress_page()
@ddt.data( @ddt.data(
(False, 42, 28), (False, 39, 25),
(True, 35, 24) (True, 32, 21)
) )
@ddt.unpack @ddt.unpack
def test_progress_queries(self, enable_waffle, initial, subsequent): def test_progress_queries(self, enable_waffle, initial, subsequent):
......
...@@ -377,8 +377,8 @@ class ViewsQueryCountTestCase( ...@@ -377,8 +377,8 @@ class ViewsQueryCountTestCase(
return inner return inner
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 3, 4, 31), (ModuleStoreEnum.Type.mongo, 3, 4, 30),
(ModuleStoreEnum.Type.split, 3, 13, 31), (ModuleStoreEnum.Type.split, 3, 13, 30),
) )
@ddt.unpack @ddt.unpack
@count_queries @count_queries
...@@ -386,8 +386,8 @@ class ViewsQueryCountTestCase( ...@@ -386,8 +386,8 @@ class ViewsQueryCountTestCase(
self.create_thread_helper(mock_request) self.create_thread_helper(mock_request)
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 3, 3, 27), (ModuleStoreEnum.Type.mongo, 3, 3, 26),
(ModuleStoreEnum.Type.split, 3, 10, 27), (ModuleStoreEnum.Type.split, 3, 10, 26),
) )
@ddt.unpack @ddt.unpack
@count_queries @count_queries
......
...@@ -315,6 +315,8 @@ TIME_ZONE = ENV_TOKENS.get('TIME_ZONE', TIME_ZONE) ...@@ -315,6 +315,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 ...@@ -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({ ...@@ -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)
......
...@@ -268,7 +268,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase): ...@@ -268,7 +268,7 @@ class BookmarksListViewTests(BookmarksViewsTestsBase):
self.assertEqual(response.data['developer_message'], u'Parameter usage_id not provided.') self.assertEqual(response.data['developer_message'], u'Parameter usage_id not provided.')
# Send empty data dictionary. # Send empty data dictionary.
with self.assertNumQueries(8): # No queries for bookmark table. with self.assertNumQueries(7): # No queries for bookmark table.
response = self.send_post( response = self.send_post(
client=self.client, client=self.client,
url=reverse('bookmarks'), url=reverse('bookmarks'),
......
...@@ -4,3 +4,7 @@ Useful information for setting the language preference ...@@ -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 @@ ...@@ -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): ...@@ -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
...@@ -9,7 +9,7 @@ from django.http import HttpResponse ...@@ -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): ...@@ -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
...@@ -174,7 +174,7 @@ class TestOwnUsernameAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -174,7 +174,7 @@ class TestOwnUsernameAPI(CacheIsolationTestCase, UserAPITestCase):
Test that a client (logged in) can get her own username. Test that a client (logged in) can get her own username.
""" """
self.client.login(username=self.user.username, password=TEST_PASSWORD) self.client.login(username=self.user.username, password=TEST_PASSWORD)
self._verify_get_own_username(15) self._verify_get_own_username(14)
def test_get_username_inactive(self): def test_get_username_inactive(self):
""" """
...@@ -184,7 +184,7 @@ class TestOwnUsernameAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -184,7 +184,7 @@ class TestOwnUsernameAPI(CacheIsolationTestCase, UserAPITestCase):
self.client.login(username=self.user.username, password=TEST_PASSWORD) self.client.login(username=self.user.username, password=TEST_PASSWORD)
self.user.is_active = False self.user.is_active = False
self.user.save() self.user.save()
self._verify_get_own_username(15) self._verify_get_own_username(14)
def test_get_username_not_logged_in(self): def test_get_username_not_logged_in(self):
""" """
...@@ -305,7 +305,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -305,7 +305,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
""" """
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD) self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
self.create_mock_profile(self.user) self.create_mock_profile(self.user)
with self.assertNumQueries(19): with self.assertNumQueries(18):
response = self.send_get(self.different_client) response = self.send_get(self.different_client)
self._verify_full_shareable_account_response(response, account_privacy=ALL_USERS_VISIBILITY) self._verify_full_shareable_account_response(response, account_privacy=ALL_USERS_VISIBILITY)
...@@ -320,7 +320,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -320,7 +320,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
""" """
self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD) self.different_client.login(username=self.different_user.username, password=TEST_PASSWORD)
self.create_mock_profile(self.user) self.create_mock_profile(self.user)
with self.assertNumQueries(19): with self.assertNumQueries(18):
response = self.send_get(self.different_client) response = self.send_get(self.different_client)
self._verify_private_account_response(response, account_privacy=PRIVATE_VISIBILITY) self._verify_private_account_response(response, account_privacy=PRIVATE_VISIBILITY)
...@@ -395,12 +395,12 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -395,12 +395,12 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
self.assertEqual(False, data["accomplishments_shared"]) self.assertEqual(False, data["accomplishments_shared"])
self.client.login(username=self.user.username, password=TEST_PASSWORD) self.client.login(username=self.user.username, password=TEST_PASSWORD)
verify_get_own_information(17) verify_get_own_information(16)
# Now make sure that the user can get the same information, even if not active # Now make sure that the user can get the same information, even if not active
self.user.is_active = False self.user.is_active = False
self.user.save() self.user.save()
verify_get_own_information(11) verify_get_own_information(10)
def test_get_account_empty_string(self): def test_get_account_empty_string(self):
""" """
...@@ -414,7 +414,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase): ...@@ -414,7 +414,7 @@ class TestAccountsAPI(CacheIsolationTestCase, UserAPITestCase):
legacy_profile.save() legacy_profile.save()
self.client.login(username=self.user.username, password=TEST_PASSWORD) self.client.login(username=self.user.username, password=TEST_PASSWORD)
with self.assertNumQueries(17): with self.assertNumQueries(16):
response = self.send_get(self.client) response = self.send_get(self.client)
for empty_field in ("level_of_education", "gender", "country", "bio"): for empty_field in ("level_of_education", "gender", "country", "bio"):
self.assertIsNone(response.data[empty_field]) self.assertIsNone(response.data[empty_field])
......
...@@ -83,6 +83,7 @@ class PreferenceValidationError(PreferenceRequestError): ...@@ -83,6 +83,7 @@ class PreferenceValidationError(PreferenceRequestError):
""" """
def __init__(self, preference_errors): def __init__(self, preference_errors):
self.preference_errors = preference_errors self.preference_errors = preference_errors
super(PreferenceValidationError, self).__init__(preference_errors)
class PreferenceUpdateError(PreferenceRequestError): class PreferenceUpdateError(PreferenceRequestError):
...@@ -93,6 +94,7 @@ class PreferenceUpdateError(PreferenceRequestError): ...@@ -93,6 +94,7 @@ class PreferenceUpdateError(PreferenceRequestError):
def __init__(self, developer_message, user_message=None): def __init__(self, developer_message, user_message=None):
self.developer_message = developer_message self.developer_message = developer_message
self.user_message = user_message self.user_message = user_message
super(PreferenceUpdateError, self).__init__(developer_message)
class CountryCodeError(ValueError): class CountryCodeError(ValueError):
......
...@@ -6,6 +6,7 @@ from collections import defaultdict ...@@ -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): ...@@ -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)
...@@ -502,3 +506,13 @@ def shim_student_view(view_func, check_logged_in=False): ...@@ -502,3 +506,13 @@ def shim_student_view(view_func, check_logged_in=False):
return response return response
return _inner return _inner
def serializer_is_dirty(preference_serializer):
"""
Return True if saving the supplied (Raw)UserPreferenceSerializer would change the database.
"""
return (
preference_serializer.instance is None or
preference_serializer.instance.value != preference_serializer.validated_data['value']
)
...@@ -20,7 +20,7 @@ from ..errors import ( ...@@ -20,7 +20,7 @@ from ..errors import (
UserAPIInternalError, UserAPIRequestError, UserNotFound, UserNotAuthorized, UserAPIInternalError, UserAPIRequestError, UserNotFound, UserNotAuthorized,
PreferenceValidationError, PreferenceUpdateError, CountryCodeError PreferenceValidationError, PreferenceUpdateError, CountryCodeError
) )
from ..helpers import intercept_errors from ..helpers import intercept_errors, serializer_is_dirty
from ..models import UserOrgTag, UserPreference from ..models import UserOrgTag, UserPreference
from ..serializers import UserSerializer, RawUserPreferenceSerializer from ..serializers import UserSerializer, RawUserPreferenceSerializer
...@@ -142,7 +142,9 @@ def update_user_preferences(requesting_user, update, user=None): ...@@ -142,7 +142,9 @@ def update_user_preferences(requesting_user, update, user=None):
if preference_value is not None: if preference_value is not None:
try: try:
serializer = serializers[preference_key] serializer = serializers[preference_key]
serializer.save()
if serializer_is_dirty(serializer):
serializer.save()
except Exception as error: except Exception as error:
raise _create_preference_update_error(preference_key, preference_value, error) raise _create_preference_update_error(preference_key, preference_value, error)
else: else:
...@@ -177,10 +179,12 @@ def set_user_preference(requesting_user, preference_key, preference_value, usern ...@@ -177,10 +179,12 @@ def set_user_preference(requesting_user, preference_key, preference_value, usern
existing_user = _get_authorized_user(requesting_user, username) existing_user = _get_authorized_user(requesting_user, username)
serializer = create_user_preference_serializer(existing_user, preference_key, preference_value) serializer = create_user_preference_serializer(existing_user, preference_key, preference_value)
validate_user_preference_serializer(serializer, preference_key, preference_value) validate_user_preference_serializer(serializer, preference_key, preference_value)
try:
serializer.save() if serializer_is_dirty(serializer):
except Exception as error: try:
raise _create_preference_update_error(preference_key, preference_value, error) serializer.save()
except Exception as error:
raise _create_preference_update_error(preference_key, preference_value, error)
@intercept_errors(UserAPIInternalError, ignore_errors=[UserAPIRequestError]) @intercept_errors(UserAPIInternalError, ignore_errors=[UserAPIRequestError])
...@@ -310,7 +314,13 @@ def _get_authorized_user(requesting_user, username=None, allow_staff=False): ...@@ -310,7 +314,13 @@ def _get_authorized_user(requesting_user, username=None, allow_staff=False):
If username is not provided, requesting_user.username is assumed. If username is not provided, requesting_user.username is assumed.
""" """
if username is None: if username is None:
username = requesting_user.username # If the user is one that has already been stored to the database, use that
if requesting_user.pk:
return requesting_user
else:
# Otherwise, treat this as a request against a separate user
username = requesting_user.username
try: try:
existing_user = User.objects.get(username=username) existing_user = User.objects.get(username=username)
except ObjectDoesNotExist: except ObjectDoesNotExist:
...@@ -347,13 +357,13 @@ def create_user_preference_serializer(user, preference_key, preference_value): ...@@ -347,13 +357,13 @@ def create_user_preference_serializer(user, preference_key, preference_value):
except ObjectDoesNotExist: except ObjectDoesNotExist:
existing_user_preference = None existing_user_preference = None
new_data = { new_data = {
"user": user.id,
"key": preference_key, "key": preference_key,
"value": preference_value, "value": preference_value,
} }
if existing_user_preference: if existing_user_preference:
serializer = RawUserPreferenceSerializer(existing_user_preference, data=new_data) serializer = RawUserPreferenceSerializer(existing_user_preference, data=new_data, partial=True)
else: else:
new_data['user'] = user.id
serializer = RawUserPreferenceSerializer(data=new_data) serializer = RawUserPreferenceSerializer(data=new_data)
return serializer return serializer
......
...@@ -53,9 +53,8 @@ class TestPreferenceAPI(CacheIsolationTestCase): ...@@ -53,9 +53,8 @@ class TestPreferenceAPI(CacheIsolationTestCase):
super(TestPreferenceAPI, self).setUp() super(TestPreferenceAPI, self).setUp()
self.user = UserFactory.create(password=self.password) self.user = UserFactory.create(password=self.password)
self.different_user = UserFactory.create(password=self.password) self.different_user = UserFactory.create(password=self.password)
self.staff_user = UserFactory(is_staff=True, password=self.password) self.staff_user = UserFactory.create(is_staff=True, password=self.password)
self.no_such_user = UserFactory.create(password=self.password) self.no_such_user = UserFactory.build(password=self.password, username="no_such_user")
self.no_such_user.username = "no_such_user"
self.test_preference_key = "test_key" self.test_preference_key = "test_key"
self.test_preference_value = "test_value" self.test_preference_value = "test_value"
set_user_preference(self.user, self.test_preference_key, self.test_preference_value) set_user_preference(self.user, self.test_preference_key, self.test_preference_value)
......
...@@ -22,12 +22,7 @@ class UserSerializer(serializers.HyperlinkedModelSerializer): ...@@ -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 @@ ...@@ -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
...@@ -55,19 +56,31 @@ class InterceptErrorsTest(TestCase): ...@@ -55,19 +56,31 @@ class InterceptErrorsTest(TestCase):
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):
......
...@@ -1002,7 +1002,7 @@ class UserViewSet(viewsets.ReadOnlyModelViewSet): ...@@ -1002,7 +1002,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"
...@@ -1028,7 +1028,7 @@ class ForumRoleUsersListView(generics.ListAPIView): ...@@ -1028,7 +1028,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
...@@ -1057,7 +1057,7 @@ class PreferenceUsersListView(generics.ListAPIView): ...@@ -1057,7 +1057,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):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment