Commit 254a0ce6 by Renzo Lucioni

Allow changing of language preference from profile page

parent c5530b75
...@@ -308,6 +308,8 @@ TIME_ZONE = 'America/New_York' # http://en.wikipedia.org/wiki/List_of_tz_zones_ ...@@ -308,6 +308,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 = lms.envs.common.LANGUAGES LANGUAGES = lms.envs.common.LANGUAGES
LANGUAGE_DICT = dict(LANGUAGES)
USE_I18N = True USE_I18N = True
USE_L10N = True USE_L10N = True
......
# -*- coding: utf-8 -*-
""" Python API for language and translation management. """
from collections import namedtuple
from django.conf import settings
from django.utils.translation import get_language
from dark_lang.models import DarkLangConfig
# Named tuples can be referenced using object-like variable
# deferencing, making the use of tuples more readable by
# eliminating the need to see the context of the tuple packing.
Language = namedtuple('Language', 'code name')
def released_languages():
"""Retrieve the list of released languages.
Constructs a list of Language tuples by intersecting the
list of valid language tuples with the list of released
language codes.
Returns:
list of Language: Languages in which full translations are available.
Example:
>>> print released_languages()
[Language(code='en', name=u'English'), Language(code='fr', name=u'Français')]
"""
released_language_codes = DarkLangConfig.current().released_languages_list
default_language_code = settings.LANGUAGE_CODE
if default_language_code not in released_language_codes:
released_language_codes.append(default_language_code)
released_language_codes.sort()
# Intersect the list of valid language tuples with the list
# of release language codes
released_languages = [
Language(tuple[0], tuple[1])
for tuple in settings.LANGUAGES
if tuple[0] in released_language_codes
]
return released_languages
def preferred_language(preferred_language_code):
"""Retrieve the name of the user's preferred language.
Note:
The preferred_language_code may be None. If this is the case,
the if/else block will handle it by returning either the active
language or the default language.
Args:
preferred_language_code (str): The ISO 639 code corresponding
to the user's preferred language.
Returns:
unicode: The name of the user's preferred language.
"""
active_language_code = get_language()
if preferred_language_code in settings.LANGUAGE_DICT:
# If the user has indicated a preference for a valid
# language, record their preferred language
preferred_language = settings.LANGUAGE_DICT[preferred_language_code]
elif active_language_code in settings.LANGUAGE_DICT:
# Otherwise, set the language used in the current thread
# as the preferred language
preferred_language = settings.LANGUAGE_DICT[active_language_code]
else:
# Otherwise, use the default language
preferred_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE]
return preferred_language
# -*- coding: utf-8 -*-
""" Tests for the language API. """
from django.test import TestCase
import ddt
from lang_pref import api as language_api
@ddt.ddt
class LanguageApiTest(TestCase):
INVALID_LANGUAGE_CODES = ['', 'foo']
def test_released_languages(self):
released_languages = language_api.released_languages()
self.assertGreaterEqual(len(released_languages), 1)
def test_preferred_language(self):
preferred_language = language_api.preferred_language('fr')
self.assertEqual(preferred_language, u'Français')
@ddt.data(*INVALID_LANGUAGE_CODES)
def test_invalid_preferred_language(self, language_code):
preferred_language = language_api.preferred_language(language_code)
self.assertEqual(preferred_language, u'English')
def test_no_preferred_language(self):
preferred_language = language_api.preferred_language(None)
self.assertEqual(preferred_language, u'English')
...@@ -414,4 +414,3 @@ def _validate_email(email): ...@@ -414,4 +414,3 @@ def _validate_email(email):
raise AccountEmailInvalid( raise AccountEmailInvalid(
u"Email '{email}' format is not valid".format(email=email) u"Email '{email}' format is not valid".format(email=email)
) )
...@@ -5,7 +5,8 @@ but does NOT include basic account information such as username, password, and ...@@ -5,7 +5,8 @@ but does NOT include basic account information such as username, password, and
email address. email address.
""" """
from user_api.models import UserProfile
from user_api.models import User, UserProfile, UserPreference
from user_api.helpers import intercept_errors from user_api.helpers import intercept_errors
...@@ -43,13 +44,13 @@ FULL_NAME_MAX_LENGTH = 255 ...@@ -43,13 +44,13 @@ FULL_NAME_MAX_LENGTH = 255
@intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError]) @intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError])
def profile_info(username): def profile_info(username):
"""Retrieve a user's profile information """Retrieve a user's profile information.
Searches either by username or email. Searches either by username or email.
At least one of the keyword args must be provided. At least one of the keyword args must be provided.
Arguments: Args:
username (unicode): The username of the account to retrieve. username (unicode): The username of the account to retrieve.
Returns: Returns:
...@@ -78,7 +79,7 @@ def update_profile(username, full_name=None): ...@@ -78,7 +79,7 @@ def update_profile(username, full_name=None):
Args: Args:
username (unicode): The username associated with the account. username (unicode): The username associated with the account.
Keyword Arguments: Keyword Args:
full_name (unicode): If provided, set the user's full name to this value. full_name (unicode): If provided, set the user's full name to this value.
Returns: Returns:
...@@ -102,31 +103,48 @@ def update_profile(username, full_name=None): ...@@ -102,31 +103,48 @@ def update_profile(username, full_name=None):
@intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError]) @intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError])
def preference_info(username, preference_name): def preference_info(username):
"""Retrieve information about a user's preferences. """Retrieve information about a user's preferences.
Arguments: Args:
username (unicode): The username of the account to retrieve. username (unicode): The username of the account to retrieve.
preference_name (unicode): The name of the preference to retrieve.
Returns: Returns:
The JSON-deserialized value. dict: Empty if there is no user
""" """
pass preferences = UserPreference.objects.filter(user__username=username)
preferences_dict = {}
for preference in preferences:
preferences_dict[preference.key] = preference.value
return preferences_dict
@intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError]) @intercept_errors(ProfileInternalError, ignore_errors=[ProfileRequestError])
def update_preference(username, preference_name, preference_value): def update_preferences(username, **kwargs):
"""Update a user's preference. """Update a user's preferences.
Arguments: Sets the provided preferences for the given user.
Args:
username (unicode): The username of the account to retrieve. username (unicode): The username of the account to retrieve.
preference_name (unicode): The name of the preference to set.
preference_value (JSON-serializable): The new value for the preference. Keyword Args:
**kwargs (unicode): Arbitrary key-value preference pairs
Returns: Returns:
None None
Raises:
ProfileUserNotFound
""" """
pass try:
user = User.objects.get(username=username)
except User.DoesNotExist:
raise ProfileUserNotFound
else:
for key, value in kwargs.iteritems():
UserPreference.set_preference(user, key, value)
...@@ -5,6 +5,7 @@ from django.test import TestCase ...@@ -5,6 +5,7 @@ from django.test import TestCase
import ddt import ddt
from nose.tools import raises from nose.tools import raises
from dateutil.parser import parse as parse_datetime from dateutil.parser import parse as parse_datetime
from user_api.api import account as account_api from user_api.api import account as account_api
from user_api.api import profile as profile_api from user_api.api import profile as profile_api
from user_api.models import UserProfile from user_api.models import UserProfile
...@@ -13,9 +14,9 @@ from user_api.models import UserProfile ...@@ -13,9 +14,9 @@ from user_api.models import UserProfile
@ddt.ddt @ddt.ddt
class ProfileApiTest(TestCase): class ProfileApiTest(TestCase):
USERNAME = u"frank-underwood" USERNAME = u'frank-underwood'
PASSWORD = u"ṕáśśẃőŕd" PASSWORD = u'ṕáśśẃőŕd'
EMAIL = u"frank+underwood@example.com" EMAIL = u'frank+underwood@example.com'
def test_create_profile(self): def test_create_profile(self):
# Create a new account, which should have an empty profile by default. # Create a new account, which should have an empty profile by default.
...@@ -31,9 +32,9 @@ class ProfileApiTest(TestCase): ...@@ -31,9 +32,9 @@ class ProfileApiTest(TestCase):
def test_update_full_name(self): def test_update_full_name(self):
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL) account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
profile_api.update_profile(self.USERNAME, full_name=u"ȻħȺɍłɇs") profile_api.update_profile(self.USERNAME, full_name=u'ȻħȺɍłɇs')
profile = profile_api.profile_info(username=self.USERNAME) profile = profile_api.profile_info(self.USERNAME)
self.assertEqual(profile['full_name'], u"ȻħȺɍłɇs") self.assertEqual(profile['full_name'], u'ȻħȺɍłɇs')
@raises(profile_api.ProfileInvalidField) @raises(profile_api.ProfileInvalidField)
@ddt.data('', 'a' * profile_api.FULL_NAME_MAX_LENGTH + 'a') @ddt.data('', 'a' * profile_api.FULL_NAME_MAX_LENGTH + 'a')
...@@ -43,10 +44,10 @@ class ProfileApiTest(TestCase): ...@@ -43,10 +44,10 @@ class ProfileApiTest(TestCase):
@raises(profile_api.ProfileUserNotFound) @raises(profile_api.ProfileUserNotFound)
def test_update_profile_no_user(self): def test_update_profile_no_user(self):
profile_api.update_profile(self.USERNAME, full_name="test") profile_api.update_profile(self.USERNAME, full_name='test')
def test_retrieve_profile_no_user(self): def test_retrieve_profile_no_user(self):
profile = profile_api.profile_info("does not exist") profile = profile_api.profile_info('does not exist')
self.assertIs(profile, None) self.assertIs(profile, None)
def test_record_name_change_history(self): def test_record_name_change_history(self):
...@@ -55,30 +56,53 @@ class ProfileApiTest(TestCase): ...@@ -55,30 +56,53 @@ class ProfileApiTest(TestCase):
# Change the name once # Change the name once
# Since the original name was an empty string, expect that the list # Since the original name was an empty string, expect that the list
# of old names is empty # of old names is empty
profile_api.update_profile(self.USERNAME, full_name="new name") profile_api.update_profile(self.USERNAME, full_name='new name')
meta = UserProfile.objects.get(user__username=self.USERNAME).get_meta() meta = UserProfile.objects.get(user__username=self.USERNAME).get_meta()
self.assertEqual(meta, {}) self.assertEqual(meta, {})
# Change the name again and expect the new name is stored in the history # Change the name again and expect the new name is stored in the history
profile_api.update_profile(self.USERNAME, full_name="another new name") profile_api.update_profile(self.USERNAME, full_name='another new name')
meta = UserProfile.objects.get(user__username=self.USERNAME).get_meta() meta = UserProfile.objects.get(user__username=self.USERNAME).get_meta()
self.assertEqual(len(meta['old_names']), 1) self.assertEqual(len(meta['old_names']), 1)
name, rationale, timestamp = meta['old_names'][0] name, rationale, timestamp = meta['old_names'][0]
self.assertEqual(name, "new name") self.assertEqual(name, 'new name')
self.assertEqual(rationale, u"") self.assertEqual(rationale, u'')
self._assert_is_datetime(timestamp) self._assert_is_datetime(timestamp)
# Change the name a third time and expect both names are stored in the history # Change the name a third time and expect both names are stored in the history
profile_api.update_profile(self.USERNAME, full_name="yet another new name") profile_api.update_profile(self.USERNAME, full_name='yet another new name')
meta = UserProfile.objects.get(user__username=self.USERNAME).get_meta() meta = UserProfile.objects.get(user__username=self.USERNAME).get_meta()
self.assertEqual(len(meta['old_names']), 2) self.assertEqual(len(meta['old_names']), 2)
name, rationale, timestamp = meta['old_names'][1] name, rationale, timestamp = meta['old_names'][1]
self.assertEqual(name, "another new name") self.assertEqual(name, 'another new name')
self.assertEqual(rationale, u"") self.assertEqual(rationale, u'')
self._assert_is_datetime(timestamp) self._assert_is_datetime(timestamp)
def test_update_and_retrieve_preference_info(self):
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
profile_api.update_preferences(self.USERNAME, preference_key='preference_value')
preferences = profile_api.preference_info(self.USERNAME)
self.assertEqual(preferences['preference_key'], 'preference_value')
@raises(profile_api.ProfileUserNotFound)
def test_retrieve_and_update_preference_info_no_user(self):
preferences = profile_api.preference_info(self.USERNAME)
self.assertEqual(preferences, {})
profile_api.update_preferences(self.USERNAME, preference_key='preference_value')
def test_update_and_retrieve_preference_info_unicode(self):
account_api.create_account(self.USERNAME, self.PASSWORD, self.EMAIL)
profile_api.update_preferences(self.USERNAME, **{u'ⓟⓡⓔⓕⓔⓡⓔⓝⓒⓔ_ⓚⓔⓨ': u'ǝnןɐʌ_ǝɔuǝɹǝɟǝɹd'})
preferences = profile_api.preference_info(self.USERNAME)
self.assertEqual(preferences[u'ⓟⓡⓔⓕⓔⓡⓔⓝⓒⓔ_ⓚⓔⓨ'], u'ǝnןɐʌ_ǝɔuǝɹǝɟǝɹd')
def _assert_is_datetime(self, timestamp): def _assert_is_datetime(self, timestamp):
if not timestamp: if not timestamp:
return False return False
......
...@@ -56,24 +56,6 @@ class StudentAccountViewTest(UrlResetMixin, TestCase): ...@@ -56,24 +56,6 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
result = self.client.login(username=self.USERNAME, password=self.PASSWORD) result = self.client.login(username=self.USERNAME, password=self.PASSWORD)
self.assertTrue(result) self.assertTrue(result)
def _change_email(self, new_email, password):
"""Request to change the user's email. """
data = {}
if new_email is not None:
data['new_email'] = new_email
if password is not None:
# We can't pass a Unicode object to urlencode, so we encode the Unicode object
data['password'] = password.encode('utf-8')
response = self.client.put(
path=reverse('email_change_request'),
data=urlencode(data),
content_type='application/x-www-form-urlencoded'
)
return response
def test_index(self): def test_index(self):
response = self.client.get(reverse('account_index')) response = self.client.get(reverse('account_index'))
self.assertContains(response, "Student Account") self.assertContains(response, "Student Account")
...@@ -153,7 +135,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase): ...@@ -153,7 +135,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
response = self._change_email(invalid_email, self.PASSWORD) response = self._change_email(invalid_email, self.PASSWORD)
self.assertEquals(response.status_code, 400) self.assertEquals(response.status_code, 400)
def test_email_change_confirmation_handler(self): def test_email_change_confirmation(self):
# Get an email change activation key # Get an email change activation key
activation_key = account_api.request_email_change(self.USERNAME, self.NEW_EMAIL, self.PASSWORD) activation_key = account_api.request_email_change(self.USERNAME, self.NEW_EMAIL, self.PASSWORD)
...@@ -242,3 +224,21 @@ class StudentAccountViewTest(UrlResetMixin, TestCase): ...@@ -242,3 +224,21 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
self.assertEqual(email.to, expected_to) self.assertEqual(email.to, expected_to)
self.assertIn(expected_subject, email.subject) self.assertIn(expected_subject, email.subject)
self.assertIn(expected_body, email.body) self.assertIn(expected_body, email.body)
def _change_email(self, new_email, password):
"""Request to change the user's email. """
data = {}
if new_email is not None:
data['new_email'] = new_email
if password is not None:
# We can't pass a Unicode object to urlencode, so we encode the Unicode object
data['password'] = password.encode('utf-8')
response = self.client.put(
path=reverse('email_change_request'),
data=urlencode(data),
content_type='application/x-www-form-urlencoded'
)
return response
...@@ -11,6 +11,7 @@ from django.contrib.auth.decorators import login_required ...@@ -11,6 +11,7 @@ from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from edxmako.shortcuts import render_to_response, render_to_string from edxmako.shortcuts import render_to_response, render_to_string
from microsite_configuration import microsite from microsite_configuration import microsite
from user_api.api import account as account_api from user_api.api import account as account_api
from user_api.api import profile as profile_api from user_api.api import profile as profile_api
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
""" Tests for student profile views. """ """ Tests for student profile views. """
from urllib import urlencode from urllib import urlencode
from collections import namedtuple
from mock import patch from mock import patch
import ddt import ddt
from django.test import TestCase from django.test import TestCase
...@@ -11,16 +13,26 @@ from django.core.urlresolvers import reverse ...@@ -11,16 +13,26 @@ from django.core.urlresolvers import reverse
from util.testing import UrlResetMixin from util.testing import UrlResetMixin
from user_api.api import account as account_api from user_api.api import account as account_api
from user_api.api import profile as profile_api from user_api.api import profile as profile_api
from lang_pref import LANGUAGE_KEY
@ddt.ddt @ddt.ddt
class StudentProfileViewTest(UrlResetMixin, TestCase): class StudentProfileViewTest(UrlResetMixin, TestCase):
""" Tests for the student profile views. """ """ Tests for the student profile views. """
USERNAME = u"heisenberg" USERNAME = u'heisenberg'
PASSWORD = u"ḅḷüëṡḳÿ" PASSWORD = u'ḅḷüëṡḳÿ'
EMAIL = u"walt@savewalterwhite.com" EMAIL = u'walt@savewalterwhite.com'
FULL_NAME = u"𝖂𝖆𝖑𝖙𝖊𝖗 𝖂𝖍𝖎𝖙𝖊" FULL_NAME = u'𝖂𝖆𝖑𝖙𝖊𝖗 𝖂𝖍𝖎𝖙𝖊'
Language = namedtuple('Language', 'code name')
NEW_LANGUAGE = Language('fr', u'Français')
INVALID_LANGUAGE_CODES = [
'',
'foo',
'en@pirate',
]
@patch.dict(settings.FEATURES, {'ENABLE_NEW_DASHBOARD': True}) @patch.dict(settings.FEATURES, {'ENABLE_NEW_DASHBOARD': True})
def setUp(self): def setUp(self):
...@@ -37,38 +49,77 @@ class StudentProfileViewTest(UrlResetMixin, TestCase): ...@@ -37,38 +49,77 @@ class StudentProfileViewTest(UrlResetMixin, TestCase):
def test_index(self): def test_index(self):
response = self.client.get(reverse('profile_index')) response = self.client.get(reverse('profile_index'))
self.assertContains(response, "Student Profile") self.assertContains(response, "Student Profile")
self.assertContains(response, "Change My Name")
self.assertContains(response, "Change Preferred Language")
self.assertContains(response, "Connected Accounts")
def test_name_change_handler(self): def test_name_change(self):
# Verify that the name on the account is blank # Verify that the name on the account is blank
profile_info = profile_api.profile_info(self.USERNAME) profile_info = profile_api.profile_info(self.USERNAME)
self.assertEquals(profile_info['full_name'], '') self.assertEqual(profile_info['full_name'], '')
response = self._change_name(self.FULL_NAME) response = self._change_name(self.FULL_NAME)
self.assertEquals(response.status_code, 204) self.assertEqual(response.status_code, 204)
# Verify that the name on the account has been changed # Verify that the name on the account has been changed
profile_info = profile_api.profile_info(self.USERNAME) profile_info = profile_api.profile_info(self.USERNAME)
self.assertEquals(profile_info['full_name'], self.FULL_NAME) self.assertEqual(profile_info['full_name'], self.FULL_NAME)
def test_name_change_invalid(self): def test_name_change_invalid(self):
# Name cannot be an empty string # Name cannot be an empty string
response = self._change_name('') response = self._change_name('')
self.assertEquals(response.status_code, 400) self.assertEqual(response.status_code, 400)
def test_name_change_missing_params(self): def test_name_change_missing_params(self):
response = self._change_name(None) response = self._change_name(None)
self.assertEquals(response.status_code, 400) self.assertEqual(response.status_code, 400)
@patch('student_profile.views.profile_api.update_profile') @patch('student_profile.views.profile_api.update_profile')
def test_name_change_internal_error(self, mock_call): def test_name_change_internal_error(self, mock_update_profile):
# This can't happen if the user is logged in, but test it anyway # This can't happen if the user is logged in, but test it anyway
mock_call.side_effect = profile_api.ProfileUserNotFound mock_update_profile.side_effect = profile_api.ProfileUserNotFound
response = self._change_name(self.FULL_NAME) response = self._change_name(self.FULL_NAME)
self.assertEqual(response.status_code, 500) self.assertEqual(response.status_code, 500)
@patch('student_profile.views.language_api.released_languages')
def test_language_change(self, mock_released_languages):
mock_released_languages.return_value = [self.NEW_LANGUAGE]
# Set French as the user's preferred language
response = self._change_language(self.NEW_LANGUAGE.code)
self.assertEqual(response.status_code, 204)
# Verify that French is now the user's preferred language
preferences = profile_api.preference_info(self.USERNAME)
self.assertEqual(preferences[LANGUAGE_KEY], self.NEW_LANGUAGE.code)
# Verify that the page reloads in French
response = self.client.get(reverse('profile_index'))
self.assertContains(response, "Merci de choisir la langue")
@ddt.data(*INVALID_LANGUAGE_CODES)
def test_change_to_invalid_or_unreleased_language(self, language_code):
response = self._change_language(language_code)
self.assertEqual(response.status_code, 400)
def test_change_to_missing_language(self):
response = self._change_language(None)
self.assertEqual(response.status_code, 400)
@patch('student_profile.views.profile_api.update_preferences')
@patch('student_profile.views.language_api.released_languages')
def test_language_change_missing_profile(self, mock_released_languages, mock_update_preferences):
# This can't happen if the user is logged in, but test it anyway
mock_released_languages.return_value = [self.NEW_LANGUAGE]
mock_update_preferences.side_effect = profile_api.ProfileUserNotFound
response = self._change_language(self.NEW_LANGUAGE.code)
self.assertEqual(response.status_code, 500)
@ddt.data( @ddt.data(
('get', 'profile_index'), ('get', 'profile_index'),
('put', 'name_change') ('put', 'name_change'),
('put', 'language_change')
) )
@ddt.unpack @ddt.unpack
def test_require_login(self, method, url_name): def test_require_login(self, method, url_name):
...@@ -83,7 +134,8 @@ class StudentProfileViewTest(UrlResetMixin, TestCase): ...@@ -83,7 +134,8 @@ class StudentProfileViewTest(UrlResetMixin, TestCase):
@ddt.data( @ddt.data(
('get', 'profile_index'), ('get', 'profile_index'),
('put', 'name_change') ('put', 'name_change'),
('put', 'language_change')
) )
@ddt.unpack @ddt.unpack
def test_require_http_method(self, correct_method, url_name): def test_require_http_method(self, correct_method, url_name):
...@@ -109,5 +161,22 @@ class StudentProfileViewTest(UrlResetMixin, TestCase): ...@@ -109,5 +161,22 @@ class StudentProfileViewTest(UrlResetMixin, TestCase):
return self.client.put( return self.client.put(
path=reverse('name_change'), path=reverse('name_change'),
data=urlencode(data), data=urlencode(data),
content_type= 'application/x-www-form-urlencoded' content_type='application/x-www-form-urlencoded'
)
def _change_language(self, new_language):
"""Request a language change.
Returns:
HttpResponse
"""
data = {}
if new_language is not None:
data['new_language'] = new_language
return self.client.put(
path=reverse('language_change'),
data=urlencode(data),
content_type='application/x-www-form-urlencoded'
) )
...@@ -4,4 +4,5 @@ urlpatterns = patterns( ...@@ -4,4 +4,5 @@ urlpatterns = patterns(
'student_profile.views', 'student_profile.views',
url(r'^$', 'index', name='profile_index'), url(r'^$', 'index', name='profile_index'),
url(r'^name_change$', 'name_change_handler', name='name_change'), url(r'^name_change$', 'name_change_handler', name='name_change'),
url(r'^language_change$', 'language_change_handler', name='language_change'),
) )
""" Views for a student's profile information. """ """ Views for a student's profile information. """
from django.conf import settings
from django.http import ( from django.http import (
QueryDict, HttpResponse, QueryDict, HttpResponse,
HttpResponseBadRequest, HttpResponseServerError HttpResponseBadRequest, HttpResponseServerError
) )
from django.conf import settings
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from user_api.api import profile as profile_api from user_api.api import profile as profile_api
from lang_pref import LANGUAGE_KEY, api as language_api
from third_party_auth import pipeline from third_party_auth import pipeline
...@@ -26,15 +28,22 @@ def index(request): ...@@ -26,15 +28,22 @@ def index(request):
HttpResponse: 302 if not logged in (redirect to login page) HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 405 if using an unsupported HTTP method HttpResponse: 405 if using an unsupported HTTP method
Example usage: Example:
GET /profile GET /profile
""" """
user = request.user user = request.user
released_languages = language_api.released_languages()
preferred_language_code = profile_api.preference_info(user.username).get(LANGUAGE_KEY)
preferred_language = language_api.preferred_language(preferred_language_code)
context = { context = {
'disable_courseware_js': True 'disable_courseware_js': True,
'released_languages': released_languages,
'preferred_language': preferred_language,
} }
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
...@@ -59,7 +68,7 @@ def name_change_handler(request): ...@@ -59,7 +68,7 @@ def name_change_handler(request):
HttpResponse: 405 if using an unsupported HTTP method HttpResponse: 405 if using an unsupported HTTP method
HttpResponse: 500 if an unexpected error occurs. HttpResponse: 500 if an unexpected error occurs.
Example usage: Example:
PUT /profile/name_change PUT /profile/name_change
...@@ -82,3 +91,51 @@ def name_change_handler(request): ...@@ -82,3 +91,51 @@ def name_change_handler(request):
# A 204 is intended to allow input for actions to take place # A 204 is intended to allow input for actions to take place
# without causing a change to the user agent's active document view. # without causing a change to the user agent's active document view.
return HttpResponse(status=204) return HttpResponse(status=204)
@login_required
@require_http_methods(['PUT'])
@ensure_csrf_cookie
def language_change_handler(request):
"""Change the user's language preference.
Args:
request (HttpRequest)
Returns:
HttpResponse: 204 if successful
HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 400 if no language is provided, or an unreleased
language is provided
HttpResponse: 405 if using an unsupported HTTP method
HttpResponse: 500 if an unexpected error occurs.
Example:
PUT /profile/language_change
"""
put = QueryDict(request.body)
username = request.user.username
new_language = put.get('new_language')
if new_language is None:
return HttpResponseBadRequest("Missing param 'new_language'")
# Check that the provided language code corresponds to a released language
released_languages = language_api.released_languages()
if new_language in [language.code for language in released_languages]:
try:
profile_api.update_preferences(username, **{LANGUAGE_KEY: new_language})
request.session['django_language'] = new_language
except profile_api.ProfileUserNotFound:
return HttpResponseServerError()
else:
return HttpResponseBadRequest(
"Provided language code corresponds to an unreleased language"
)
# A 204 is intended to allow input for actions to take place
# without causing a change to the user agent's active document view.
return HttpResponse(status=204)
...@@ -687,7 +687,7 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html ...@@ -687,7 +687,7 @@ LANGUAGE_CODE = 'en' # http://www.i18nguy.com/unicode/language-identifiers.html
# 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'),
('eo', u'Dummy Language (Esperanto)'), # Dummy languaged used for testing ('eo', u'Dummy Language (Esperanto)'), # Dummy language used for testing
('fake2', u'Fake translations'), # Another dummy language for testing (not pushed to prod) ('fake2', u'Fake translations'), # Another dummy language for testing (not pushed to prod)
('am', u'አማርኛ'), # Amharic ('am', u'አማርኛ'), # Amharic
...@@ -778,6 +778,9 @@ LOCALE_PATHS = (REPO_ROOT + '/conf/locale',) # edx-platform/conf/locale/ ...@@ -778,6 +778,9 @@ LOCALE_PATHS = (REPO_ROOT + '/conf/locale',) # edx-platform/conf/locale/
# Messages # Messages
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
# Guidelines for translators
TRANSLATORS_GUIDE = 'https://github.com/edx/edx-platform/blob/master/docs/en_us/developers/source/i18n_translators_guide.rst'
#################################### GITHUB ####################################### #################################### GITHUB #######################################
# gitreload is used in LMS-workflow to pull content from github # gitreload is used in LMS-workflow to pull content from github
# gitreload requests are only allowed from these IP addresses, which are # gitreload requests are only allowed from these IP addresses, which are
......
...@@ -16,22 +16,42 @@ var edx = edx || {}; ...@@ -16,22 +16,42 @@ var edx = edx || {};
eventHandlers: { eventHandlers: {
init: function() { init: function() {
_fn.eventHandlers.submit(); _fn.eventHandlers.submit();
_fn.eventHandlers.click();
}, },
submit: function() { submit: function() {
$("#name-change-form").submit( _fn.form.submit ); $('#name-change-form').on( 'submit', _fn.update.name );
},
click: function() {
$('#language-change-form .submit-button').on( 'click', _fn.update.language );
}
},
update: {
name: function( event ) {
_fn.form.submit( event, '#new-name', 'new_name', 'name_change' );
},
language: function( event ) {
/**
* The onSuccess argument here means: take `window.location.reload`
* and return a function that will use `window.location` as the
* `this` reference inside `reload()`.
*/
_fn.form.submit( event, '#new-language', 'new_language', 'language_change', window.location.reload.bind(window.location) );
} }
}, },
form: { form: {
submit: function( event ) { submit: function( event, idSelector, key, url, onSuccess ) {
var $newName = $('#new-name'); var $selection = $(idSelector),
var data = { data = {};
new_name: $newName.val()
}; data[key] = $selection.val();
event.preventDefault(); event.preventDefault();
_fn.ajax.put( 'name_change', data ); _fn.ajax.put( url, data, onSuccess );
} }
}, },
...@@ -40,7 +60,7 @@ var edx = edx || {}; ...@@ -40,7 +60,7 @@ var edx = edx || {};
var csrftoken = _fn.cookie.get( 'csrftoken' ); var csrftoken = _fn.cookie.get( 'csrftoken' );
$.ajaxSetup({ $.ajaxSetup({
beforeSend: function(xhr, settings) { beforeSend: function( xhr, settings ) {
if ( settings.type === 'PUT' ) { if ( settings.type === 'PUT' ) {
xhr.setRequestHeader( 'X-CSRFToken', csrftoken ); xhr.setRequestHeader( 'X-CSRFToken', csrftoken );
} }
...@@ -48,11 +68,12 @@ var edx = edx || {}; ...@@ -48,11 +68,12 @@ var edx = edx || {};
}); });
}, },
put: function( url, data ) { put: function( url, data, onSuccess ) {
$.ajax({ $.ajax({
url: url, url: url,
type: 'PUT', type: 'PUT',
data: data data: data,
success: onSuccess ? onSuccess : ''
}); });
} }
}, },
......
...@@ -54,7 +54,12 @@ ...@@ -54,7 +54,12 @@
<ul class="list list-actions actions-supplemental"> <ul class="list list-actions actions-supplemental">
<li class="list-actions-item"> <li class="list-actions-item">
${_("Don't see your preferred language? {link_start}Volunteer to become a translator!{link_end}").format(link_start='<a class=" action action-volunteer" rel="external" target="_blank" href="https://github.com/edx/edx-platform/blob/master/docs/en_us/developers/source/i18n_translators_guide.rst">', link_end="</a>")} ${_("Don't see your preferred language? {link_start}Volunteer to become a translator!{link_end}").format(
link_start='<a class=" action action-volunteer" rel="external" target="_blank" href={translators_guide}>'.format(
translators_guide=settings.TRANSLATORS_GUIDE
),
link_end="</a>"
)}
</li> </li>
</ul> </ul>
</div> </div>
......
...@@ -16,14 +16,46 @@ ...@@ -16,14 +16,46 @@
<form id="name-change-form"> <form id="name-change-form">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}"> <input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<label for="new-name">${_('Full Name')}</label> <label for="new-name">${_("Full Name")}</label>
<input id="new-name" type="text" name="new-name" value="" placeholder="Xsy" /> <input id="new-name" type="text" name="new-name" value="" placeholder="Xsy" />
<div class="submit-button"> <div class="submit-button">
<input type="submit" id="name-change-submit" value="${_('Change My Name')}"> <input type="submit" id="name-change-submit" value="${_("Change My Name")}">
</div> </div>
</form> </form>
<div id="language-change-body">
<form id="language-change-form">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<label for="new-language">${_("Please choose your preferred language")}</label>
<select id="new-language" name="language">
% for language in released_languages:
% if language.name is preferred_language:
<option value="${language.code}" selected="selected">${language.name}</option>
% else:
<option value="${language.code}">${language.name}</option>
% endif
% endfor
</select>
<div class="submit-button">
<input type="submit" id="language-change-submit" value="${_("Change Preferred Language")}" />
</div>
</form>
<ul class="list list-actions actions-supplemental">
<li class="list-actions-item">
${_("Don't see your preferred language? {link_start}Volunteer to become a translator!{link_end}").format(
link_start='<a class=" action action-volunteer" rel="external" target="_blank" href={translators_guide}>'.format(
translators_guide=settings.TRANSLATORS_GUIDE
),
link_end="</a>"
)}
</li>
</ul>
</div>
% if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'): % if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
<%include file="third_party_auth.html" /> <%include file="third_party_auth.html" />
% endif % endif
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