Commit 37625e8e by Renzo Lucioni

Merge pull request #5515 from edx/renzo/language-backbone

Use Backbone for student account and profile JS.
parents 7802c1ed 00d976b8
......@@ -69,13 +69,15 @@ def preferred_language(preferred_language_code):
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]
pass
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]
preferred_language_code = active_language_code
else:
# Otherwise, use the default language
preferred_language = settings.LANGUAGE_DICT[settings.LANGUAGE_CODE]
preferred_language_code = settings.LANGUAGE_CODE
return preferred_language
preferred_language = settings.LANGUAGE_DICT[preferred_language_code]
return Language(preferred_language_code, preferred_language)
......@@ -18,13 +18,13 @@ class LanguageApiTest(TestCase):
def test_preferred_language(self):
preferred_language = language_api.preferred_language('fr')
self.assertEqual(preferred_language, u'Français')
self.assertEqual(preferred_language, language_api.Language('fr', 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')
self.assertEqual(preferred_language, language_api.Language('en', u'English'))
def test_no_preferred_language(self):
preferred_language = language_api.preferred_language(None)
self.assertEqual(preferred_language, u'English')
self.assertEqual(preferred_language, language_api.Language('en', u'English'))
......@@ -6,6 +6,7 @@ email address.
"""
from user_api.models import User, UserProfile, UserPreference
from user_api.helpers import intercept_errors
......
......@@ -36,6 +36,8 @@ class AccountApiTest(TestCase):
"@",
"@domain.com",
"test@no_extension",
u"fŕáńḱ@example.com",
u"frank@éxáḿṕĺé.ćőḿ",
# Long email -- subtract the length of the @domain
# except for one character (so we exceed the max length limit)
......
......@@ -26,8 +26,8 @@ msgid ""
msgstr ""
"Project-Id-Version: 0.1a\n"
"Report-Msgid-Bugs-To: openedx-translation@googlegroups.com\n"
"POT-Creation-Date: 2014-09-24 14:15-0400\n"
"PO-Revision-Date: 2014-09-24 18:16:48.345620\n"
"POT-Creation-Date: 2014-10-01 13:57+0000\n"
"PO-Revision-Date: 2014-10-01 13:57:56.490708\n"
"Last-Translator: \n"
"Language-Team: openedx-translation <openedx-translation@googlegroups.com>\n"
"MIME-Version: 1.0\n"
......@@ -1538,6 +1538,36 @@ msgstr ""
"Ýöür ßröwsér döésn't süppört dïréçt äççéss tö thé çlïpßöärd. Pléäsé üsé thé "
"Çtrl+X/Ç/V kéýßöärd shörtçüts ïnstéäd. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢#"
#: common/lib/xmodule/xmodule/js/src/lti/lti.js
msgid ""
"Click OK to have your username and e-mail address sent to a 3rd party application.\n"
"\n"
"Click Cancel to return to this page without sending your information."
msgstr ""
"Çlïçk ÖK tö hävé ýöür üsérnämé änd é-mäïl äddréss sént tö ä 3rd pärtý äpplïçätïön.\n"
"\n"
"Çlïçk Çänçél tö rétürn tö thïs pägé wïthöüt séndïng ýöür ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ιριѕι#"
#: common/lib/xmodule/xmodule/js/src/lti/lti.js
msgid ""
"Click OK to have your username sent to a 3rd party application.\n"
"\n"
"Click Cancel to return to this page without sending your information."
msgstr ""
"Çlïçk ÖK tö hävé ýöür üsérnämé sént tö ä 3rd pärtý äpplïçätïön.\n"
"\n"
"Çlïçk Çänçél tö rétürn tö thïs pägé wïthöüt séndïng ýöür ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α#"
#: common/lib/xmodule/xmodule/js/src/lti/lti.js
msgid ""
"Click OK to have your e-mail address sent to a 3rd party application.\n"
"\n"
"Click Cancel to return to this page without sending your information."
msgstr ""
"Çlïçk ÖK tö hävé ýöür é-mäïl äddréss sént tö ä 3rd pärtý äpplïçätïön.\n"
"\n"
"Çlïçk Çänçél tö rétürn tö thïs pägé wïthöüt séndïng ýöür ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕє¢тєтυя α∂ι#"
#: common/lib/xmodule/xmodule/js/src/sequence/display.js
msgid ""
"Sequence error! Cannot navigate to tab %(tab_name)s in the current "
......@@ -1747,9 +1777,7 @@ msgstr ""
#: common/static/coffee/src/discussion/utils.js
#: common/static/coffee/src/discussion/views/discussion_thread_list_view.js
#: common/static/coffee/src/discussion/views/discussion_thread_list_view.js
#: common/static/coffee/src/discussion/views/new_post_view.js
#: common/static/coffee/src/discussion/views/new_post_view.js
#: common/static/coffee/src/discussion/views/new_post_view.js
#: common/static/coffee/src/discussion/views/discussion_topic_menu_view.js
msgid "…"
msgstr "… #"
......@@ -2373,33 +2401,6 @@ msgstr "Çlösé Çälçülätör Ⱡ'σ#"
msgid "Post body"
msgstr "Pöst ßödý #"
#. Translators: "Distribution" refers to a grade distribution. This error
#. message appears when there is an error getting the data on grade
#. distribution.;
#: lms/static/coffee/src/instructor_dashboard/analytics.js
msgid "Error fetching distribution."
msgstr "Érrör fétçhïng dïstrïßütïön. Ⱡ'σяєм #"
#: lms/static/coffee/src/instructor_dashboard/analytics.js
#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js
msgid "Unavailable metric display."
msgstr "Ûnäväïläßlé métrïç dïspläý. Ⱡ'σяєм#"
#: lms/static/coffee/src/instructor_dashboard/analytics.js
#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js
msgid "Error fetching grade distributions."
msgstr "Érrör fétçhïng grädé dïstrïßütïöns. Ⱡ'σяєм ιρ#"
#: lms/static/coffee/src/instructor_dashboard/analytics.js
#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js
msgid "Last Updated: <%= timestamp %>"
msgstr "Läst Ûpdätéd: <%= timestamp %> Ⱡ'σ#"
#: lms/static/coffee/src/instructor_dashboard/analytics.js
#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js
msgid "<%= num_students %> students scored."
msgstr "<%= num_students %> stüdénts sçöréd. Ⱡ'σя#"
#: lms/static/coffee/src/instructor_dashboard/data_download.js
#: cms/templates/js/mock/mock-group-configuration-page.underscore
msgid "Loading..."
......@@ -2429,6 +2430,22 @@ msgstr ""
"Lïnks äré générätéd ön démänd änd éxpïré wïthïn 5 mïnütés düé tö thé "
"sénsïtïvé nätüré öf stüdént ïnförmätïön. Ⱡ'σяєм ιρѕυм ∂σłσя ѕιт αмєт, ¢σηѕ#"
#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js
msgid "Unavailable metric display."
msgstr "Ûnäväïläßlé métrïç dïspläý. Ⱡ'σяєм#"
#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js
msgid "Error fetching grade distributions."
msgstr "Érrör fétçhïng grädé dïstrïßütïöns. Ⱡ'σяєм ιρ#"
#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js
msgid "Last Updated: <%= timestamp %>"
msgstr "Läst Ûpdätéd: <%= timestamp %> Ⱡ'σ#"
#: lms/static/coffee/src/instructor_dashboard/instructor_analytics.js
msgid "<%= num_students %> students scored."
msgstr "<%= num_students %> stüdénts sçöréd. Ⱡ'σя#"
#: lms/static/coffee/src/instructor_dashboard/membership.js
msgid "Username"
msgstr "Ûsérnämé #"
......@@ -3122,6 +3139,31 @@ msgstr "Süççéssfüllý résçöréd prößlém för üsér {user} Ⱡ'σяє
msgid "Failed to rescore problem."
msgstr "Fäïléd tö résçöré prößlém. Ⱡ'σяєм#"
#: lms/static/js/student_account/account.js
#: lms/static/js/student_profile/profile.js
msgid "The data could not be saved."
msgstr "Thé dätä çöüld nöt ßé sävéd. Ⱡ'σяєм #"
#: lms/static/js/student_account/account.js
msgid "Please enter a valid email address"
msgstr "Pléäsé éntér ä välïd émäïl äddréss Ⱡ'σяєм ιρ#"
#: lms/static/js/student_account/account.js
msgid "Please enter a valid password"
msgstr "Pléäsé éntér ä välïd pässwörd Ⱡ'σяєм #"
#: lms/static/js/student_account/account.js
msgid "Please check your email to confirm the change"
msgstr "Pléäsé çhéçk ýöür émäïl tö çönfïrm thé çhängé Ⱡ'σяєм ιρѕυм#"
#: lms/static/js/student_profile/profile.js
msgid "Full name cannot be blank"
msgstr "Füll nämé çännöt ßé ßlänk Ⱡ'σяєм#"
#: lms/static/js/student_profile/profile.js
msgid "Saved"
msgstr "Sävéd Ⱡ'σяєм ι#"
#: lms/templates/class_dashboard/all_section_metrics.js
#: lms/templates/class_dashboard/all_section_metrics.js
msgid "Unable to retrieve data, please try again later."
......
......@@ -62,7 +62,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
def test_change_email(self):
response = self._change_email(self.NEW_EMAIL, self.PASSWORD)
self.assertEquals(response.status_code, 204)
self.assertEquals(response.status_code, 200)
# Verify that the email associated with the account remains unchanged
profile_info = profile_api.profile_info(self.USERNAME)
......@@ -79,7 +79,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
# Retrieve the activation key from the email
email_body = mail.outbox[0].body
result = re.search('/email_change_confirm/([^ \n]+)', email_body)
result = re.search('/email/confirmation/([^ \n]+)', email_body)
self.assertIsNot(result, None)
activation_key = result.group(1)
......@@ -127,7 +127,7 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
# Request to change the original user's email to the email used by the inactive user
response = self._change_email(self.NEW_EMAIL, self.PASSWORD)
self.assertEquals(response.status_code, 204)
self.assertEquals(response.status_code, 200)
@ddt.data(*INVALID_EMAILS)
def test_email_change_request_email_invalid(self, invalid_email):
......@@ -192,14 +192,15 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
self.assertEqual(response.status_code, 400)
@ddt.data(
('get', 'account_index'),
('put', 'email_change_request')
('get', 'account_index', []),
('post', 'email_change_request', []),
('get', 'email_change_confirm', [123])
)
@ddt.unpack
def test_require_login(self, method, url_name):
def test_require_login(self, method, url_name, args):
# Access the page while logged out
self.client.logout()
url = reverse(url_name)
url = reverse(url_name, args=args)
response = getattr(self.client, method)(url, follow=True)
# Should have been redirected to the login page
......@@ -207,13 +208,14 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
self.assertIn('accounts/login?next=', response.redirect_chain[0][0])
@ddt.data(
('get', 'account_index'),
('put', 'email_change_request')
('get', 'account_index', []),
('post', 'email_change_request', []),
('get', 'email_change_confirm', [123])
)
@ddt.unpack
def test_require_http_method(self, correct_method, url_name):
def test_require_http_method(self, correct_method, url_name, args):
wrong_methods = {'get', 'put', 'post', 'head', 'options', 'delete'} - {correct_method}
url = reverse(url_name)
url = reverse(url_name, args=args)
for method in wrong_methods:
response = getattr(self.client, method)(url)
......@@ -230,15 +232,9 @@ class StudentAccountViewTest(UrlResetMixin, TestCase):
data = {}
if new_email is not None:
data['new_email'] = new_email
data['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
return self.client.post(path=reverse('email_change_request'), data=data)
......@@ -3,6 +3,6 @@ from django.conf.urls import patterns, url
urlpatterns = patterns(
'student_account.views',
url(r'^$', 'index', name='account_index'),
url(r'^email_change_request$', 'email_change_request_handler', name='email_change_request'),
url(r'^email_change_confirm/(?P<key>[^/]*)$', 'email_change_confirmation_handler', name='email_change_confirm'),
url(r'^email$', 'email_change_request_handler', name='email_change_request'),
url(r'^email/confirmation/(?P<key>[^/]*)$', 'email_change_confirmation_handler', name='email_change_confirm'),
)
......@@ -42,7 +42,7 @@ def index(request):
@login_required
@require_http_methods(['PUT'])
@require_http_methods(['POST'])
@ensure_csrf_cookie
def email_change_request_handler(request):
"""Handle a request to change the user's email address.
......@@ -51,7 +51,7 @@ def email_change_request_handler(request):
request (HttpRequest)
Returns:
HttpResponse: 204 if the confirmation email was sent successfully
HttpResponse: 200 if the confirmation email was sent successfully
HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 400 if the format of the new email is incorrect
HttpResponse: 401 if the provided password (in the form) is incorrect
......@@ -62,22 +62,20 @@ def email_change_request_handler(request):
Example usage:
PUT /account/email_change_request
POST /account/email
"""
put = QueryDict(request.body)
user = request.user
password = put.get('password')
username = user.username
old_email = profile_api.profile_info(username)['email']
new_email = put.get('new_email')
username = request.user.username
password = request.POST.get('password')
new_email = request.POST.get('email')
if new_email is None:
return HttpResponseBadRequest("Missing param 'new_email'")
return HttpResponseBadRequest("Missing param 'email'")
if password is None:
return HttpResponseBadRequest("Missing param 'password'")
old_email = profile_api.profile_info(username)['email']
try:
key = account_api.request_email_change(username, new_email, password)
except account_api.AccountUserNotFound:
......@@ -104,12 +102,11 @@ def email_change_request_handler(request):
settings.DEFAULT_FROM_EMAIL
)
# Email new address
# Send a confirmation email to the new address containing the activation key
send_mail(subject, message, from_address, [new_email])
# 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)
# Send a 200 response code to the client to indicate that the email was sent successfully.
return HttpResponse(status=200)
@login_required
......
......@@ -2,7 +2,7 @@
""" Tests for student profile views. """
from urllib import urlencode
from collections import namedtuple
import json
from mock import patch
import ddt
......@@ -13,7 +13,7 @@ from django.core.urlresolvers import reverse
from util.testing import UrlResetMixin
from user_api.api import account as account_api
from user_api.api import profile as profile_api
from lang_pref import LANGUAGE_KEY
from lang_pref import LANGUAGE_KEY, api as language_api
@ddt.ddt
......@@ -25,8 +25,7 @@ class StudentProfileViewTest(UrlResetMixin, TestCase):
EMAIL = u'walt@savewalterwhite.com'
FULL_NAME = u'𝖂𝖆𝖑𝖙𝖊𝖗 𝖂𝖍𝖎𝖙𝖊'
Language = namedtuple('Language', 'code name')
NEW_LANGUAGE = Language('fr', u'Français')
TEST_LANGUAGE = language_api.Language('eo', u'Dummy language')
INVALID_LANGUAGE_CODES = [
'',
......@@ -49,8 +48,6 @@ class StudentProfileViewTest(UrlResetMixin, TestCase):
def test_index(self):
response = self.client.get(reverse('profile_index'))
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(self):
......@@ -81,45 +78,61 @@ class StudentProfileViewTest(UrlResetMixin, TestCase):
response = self._change_name(self.FULL_NAME)
self.assertEqual(response.status_code, 500)
@patch('student_profile.views.language_api.preferred_language')
@patch('student_profile.views.language_api.released_languages')
def test_get_released_languages(self, mock_released_languages, mock_preferred_language):
mock_released_languages.return_value = [self.TEST_LANGUAGE]
mock_preferred_language.return_value = self.TEST_LANGUAGE
response = self.client.get(reverse('language_info'))
self.assertEqual(
json.loads(response.content),
{
'preferredLanguage': {'code': self.TEST_LANGUAGE.code, 'name': self.TEST_LANGUAGE.name},
'languages': [{'code': self.TEST_LANGUAGE.code, 'name': self.TEST_LANGUAGE.name}]
}
)
@patch('student_profile.views.language_api.released_languages')
def test_language_change(self, mock_released_languages):
mock_released_languages.return_value = [self.NEW_LANGUAGE]
mock_released_languages.return_value = [self.TEST_LANGUAGE]
# Set French as the user's preferred language
response = self._change_language(self.NEW_LANGUAGE.code)
# Set the dummy language as the user's preferred language
response = self._change_preferences(language=self.TEST_LANGUAGE.code)
self.assertEqual(response.status_code, 204)
# Verify that French is now the user's preferred language
# Verify that the dummy language is now the user's preferred language
preferences = profile_api.preference_info(self.USERNAME)
self.assertEqual(preferences[LANGUAGE_KEY], self.NEW_LANGUAGE.code)
self.assertEqual(preferences[LANGUAGE_KEY], self.TEST_LANGUAGE.code)
# Verify that the page reloads in French
# Verify that the page reloads in the dummy language
response = self.client.get(reverse('profile_index'))
self.assertContains(response, "Merci de choisir la langue")
self.assertContains(response, u"Stüdént Pröfïlé")
@ddt.data(*INVALID_LANGUAGE_CODES)
def test_change_to_invalid_or_unreleased_language(self, language_code):
response = self._change_language(language_code)
response = self._change_preferences(language=language_code)
self.assertEqual(response.status_code, 400)
def test_change_to_missing_language(self):
response = self._change_language(None)
response = self._change_preferences(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_released_languages.return_value = [self.TEST_LANGUAGE]
mock_update_preferences.side_effect = profile_api.ProfileUserNotFound
response = self._change_language(self.NEW_LANGUAGE.code)
response = self._change_preferences(language=self.TEST_LANGUAGE.code)
self.assertEqual(response.status_code, 500)
@ddt.data(
('get', 'profile_index'),
('put', 'name_change'),
('put', 'language_change')
('put', 'profile_index'),
('put', 'preference_handler'),
('get', 'language_info'),
)
@ddt.unpack
def test_require_login(self, method, url_name):
......@@ -133,13 +146,13 @@ class StudentProfileViewTest(UrlResetMixin, TestCase):
self.assertIn('accounts/login?next=', response.redirect_chain[0][0])
@ddt.data(
('get', 'profile_index'),
('put', 'name_change'),
('put', 'language_change')
(['get', 'put'], 'profile_index'),
(['put'], 'preference_handler'),
(['get'], 'language_info'),
)
@ddt.unpack
def test_require_http_method(self, correct_method, url_name):
wrong_methods = {'get', 'put', 'post', 'head', 'options', 'delete'} - {correct_method}
def test_require_http_method(self, correct_methods, url_name):
wrong_methods = {'get', 'put', 'post', 'head', 'options', 'delete'} - set(correct_methods)
url = reverse(url_name)
for method in wrong_methods:
......@@ -156,27 +169,28 @@ class StudentProfileViewTest(UrlResetMixin, TestCase):
data = {}
if new_name is not None:
# We can't pass a Unicode object to urlencode, so we encode the Unicode object
data['new_name'] = new_name.encode('utf-8')
data['fullName'] = new_name.encode('utf-8')
return self.client.put(
path=reverse('name_change'),
path=reverse('profile_index'),
data=urlencode(data),
content_type='application/x-www-form-urlencoded'
)
def _change_language(self, new_language):
"""Request a language change.
def _change_preferences(self, **preferences):
"""Request a change to the user's preferences.
Returns:
HttpResponse
"""
data = {}
if new_language is not None:
data['new_language'] = new_language
for key, value in preferences.iteritems():
if value is not None:
data[key] = value
return self.client.put(
path=reverse('language_change'),
path=reverse('preference_handler'),
data=urlencode(data),
content_type='application/x-www-form-urlencoded'
)
......@@ -3,6 +3,6 @@ from django.conf.urls import patterns, url
urlpatterns = patterns(
'student_profile.views',
url(r'^$', 'index', name='profile_index'),
url(r'^name_change$', 'name_change_handler', name='name_change'),
url(r'^language_change$', 'language_change_handler', name='language_change'),
url(r'^preferences$', 'preference_handler', name='preference_handler'),
url(r'^preferences/languages$', 'language_info', name='language_info'),
)
""" Views for a student's profile information. """
from django.conf import settings
import json
from django.http import (
QueryDict, HttpResponse,
HttpResponseBadRequest, HttpResponseServerError
)
from django.conf import settings
from django.views.decorators.http import require_http_methods
from django_future.csrf import ensure_csrf_cookie
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from edxmako.shortcuts import render_to_response
from user_api.api import profile as profile_api
from lang_pref import LANGUAGE_KEY, api as language_api
......@@ -16,34 +17,47 @@ from third_party_auth import pipeline
@login_required
@require_http_methods(['GET'])
def index(request):
"""Render the profile info page.
"""View or modify the student's profile.
GET: Retrieve the user's profile information.
PUT: Update the user's profile information. Currently the only accept param is "fullName".
Args:
request (HttpRequest)
Returns:
HttpResponse: 200 if successful
HttpResponse: 200 if successful on GET
HttpResponse: 204 if successful on PUT
HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 400 if the updated information is invalid
HttpResponse: 405 if using an unsupported HTTP method
HttpResponse: 500 if an unexpected error occurs.
Example:
"""
if request.method == "GET":
return _get_profile(request)
elif request.method == "PUT":
return _update_profile(request)
else:
return HttpResponse(status=405)
GET /profile
"""
user = request.user
def _get_profile(request):
"""Retrieve the user's profile information, including an HTML form
that students can use to update the information.
released_languages = language_api.released_languages()
Args:
request (HttpRequest)
preferred_language_code = profile_api.preference_info(user.username).get(LANGUAGE_KEY)
preferred_language = language_api.preferred_language(preferred_language_code)
Returns:
HttpResponse
"""
user = request.user
context = {
'disable_courseware_js': True,
'released_languages': released_languages,
'preferred_language': preferred_language,
'disable_courseware_js': True
}
if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
......@@ -52,34 +66,24 @@ def index(request):
return render_to_response('student_profile/index.html', context)
@login_required
@require_http_methods(['PUT'])
@ensure_csrf_cookie
def name_change_handler(request):
"""Change the user's name.
def _update_profile(request):
"""Update a user's profile information.
Args:
request (HttpRequest)
Returns:
HttpResponse: 204 if successful
HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 400 if the provided name is invalid
HttpResponse: 405 if using an unsupported HTTP method
HttpResponse: 500 if an unexpected error occurs.
Example:
PUT /profile/name_change
HttpResponse
"""
put = QueryDict(request.body)
username = request.user.username
new_name = put.get('new_name')
new_name = put.get('fullName')
if new_name is None:
return HttpResponseBadRequest("Missing param 'new_name'")
return HttpResponseBadRequest("Missing param 'fullName'")
try:
profile_api.update_profile(username, full_name=new_name)
......@@ -94,10 +98,47 @@ def name_change_handler(request):
@login_required
@require_http_methods(['GET'])
def language_info(request):
"""Retrieve information about languages.
Gets the user's preferred language and the list of released
languages, encoding the information as JSON.
Args:
request (HttpRequest)
Returns:
HttpResponse: 200 if successful on GET
HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 405 if using an unsupported HTTP method
HttpResponse: 500 if an unexpected error occurs
Example:
GET /profile/preferences/languages
"""
user = request.user
preferred_language_code = profile_api.preference_info(user.username).get(LANGUAGE_KEY)
preferred_language = language_api.preferred_language(preferred_language_code)
response_data = {'preferredLanguage': {'code': preferred_language.code, 'name': preferred_language.name}}
languages = language_api.released_languages()
response_data['languages'] = [{'code': language.code, 'name': language.name} for language in languages]
return HttpResponse(json.dumps(response_data), content_type='application/json')
@login_required
@require_http_methods(['PUT'])
@ensure_csrf_cookie
def language_change_handler(request):
"""Change the user's language preference.
def preference_handler(request):
"""Change the user's preferences.
At the moment, the only supported preference is the user's
language choice.
Args:
request (HttpRequest)
......@@ -112,16 +153,16 @@ def language_change_handler(request):
Example:
PUT /profile/language_change
PUT /profile/preferences
"""
put = QueryDict(request.body)
username = request.user.username
new_language = put.get('new_language')
new_language = put.get('language')
if new_language is None:
return HttpResponseBadRequest("Missing param 'new_language'")
return HttpResponseBadRequest("Missing param 'language'")
# Check that the provided language code corresponds to a released language
released_languages = language_api.released_languages()
......
......@@ -212,6 +212,14 @@
},
// LMS class loaded explicitly until they are converted to use RequireJS
'js/student_account/account': {
exports: 'js/student_account/account',
deps: ['jquery', 'underscore', 'backbone', 'gettext', 'jquery.cookie']
},
'js/student_profile/profile': {
exports: 'js/student_profile/profile',
deps: ['jquery', 'underscore', 'backbone', 'gettext', 'jquery.cookie']
},
'js/verify_student/photocapture': {
exports: 'js/verify_student/photocapture'
},
......@@ -261,6 +269,8 @@
'lms/include/js/spec/staff_debug_actions_spec.js',
'lms/include/js/spec/views/notification_spec.js',
'lms/include/js/spec/dashboard/donation.js',
'lms/include/js/spec/student_account/account.js',
'lms/include/js/spec/student_profile/profile.js'
]);
}).call(this, requirejs, define);
define(['js/student_account/account'],
function() {
describe("edx.student.account.AccountModel", function() {
'use strict';
var account = null;
var assertValid = function(fields, isValid, expectedErrors) {
account.set(fields);
var errors = account.validate(account.attributes);
if (isValid) {
expect(errors).toBe(undefined);
} else {
expect(errors).toEqual(expectedErrors);
}
};
var EXPECTED_ERRORS = {
email: {
email: "Please enter a valid email address"
},
password: {
password: "Please enter a valid password"
}
};
beforeEach(function() {
account = new edx.student.account.AccountModel();
account.set({
email: "bob@example.com",
password: "password"
});
});
it("accepts valid email addresses", function() {
assertValid({email: "bob@example.com"}, true);
assertValid({email: "bob+smith@example.com"}, true);
assertValid({email: "bob+smith@example.com"}, true);
assertValid({email: "bob+smith@example.com"}, true);
assertValid({email: "bob@test.example.com"}, true);
assertValid({email: "bob@test-example.com"}, true);
});
it("rejects blank email addresses", function() {
assertValid({email: ""}, false, EXPECTED_ERRORS.email);
assertValid({email: " "}, false, EXPECTED_ERRORS.email);
});
it("rejects invalid email addresses", function() {
assertValid({email: "bob"}, false, EXPECTED_ERRORS.email);
assertValid({email: "bob@example"}, false, EXPECTED_ERRORS.email);
assertValid({email: "@"}, false, EXPECTED_ERRORS.email);
assertValid({email: "@example.com"}, false, EXPECTED_ERRORS.email);
// The server will reject emails with non-ASCII unicode
// Technically these are valid email addresses, but the email validator
// in Django 1.4 will reject them anyway, so we should too.
assertValid({email: "fŕáńḱ@example.com"}, false, EXPECTED_ERRORS.email);
assertValid({email: "frank@éxáḿṕĺé.com"}, false, EXPECTED_ERRORS.email);
});
it("rejects a long email address", function() {
// Construct an email exactly one character longer than the maximum length
var longEmail = new Array(account.EMAIL_MAX_LENGTH - 10).join("e") + "@example.com";
assertValid({email: longEmail}, false, EXPECTED_ERRORS.email);
});
it("accepts a valid password", function() {
assertValid({password: "password-test123"}, true, EXPECTED_ERRORS.password);
});
it("rejects a short password", function() {
assertValid({password: ""}, false, EXPECTED_ERRORS.password);
assertValid({password: "a"}, false, EXPECTED_ERRORS.password);
assertValid({password: "aa"}, true, EXPECTED_ERRORS.password);
});
it("rejects a long password", function() {
// Construct a password exactly one character longer than the maximum length
var longPassword = new Array(account.PASSWORD_MAX_LENGTH + 2).join("a");
assertValid({password: longPassword}, false, EXPECTED_ERRORS.password);
});
});
describe("edx.student.account.AccountView", function() {
var view = null,
ajaxSuccess = true;
var requestEmailChange = function(email, password) {
var fakeEvent = {preventDefault: function() {}};
view.model.set({
email: email,
password: password
});
view.submit(fakeEvent);
};
var assertAjax = function(url, method, data) {
expect($.ajax).toHaveBeenCalled();
var ajaxArgs = $.ajax.mostRecentCall.args[0];
expect(ajaxArgs.url).toEqual(url);
expect(ajaxArgs.type).toEqual(method);
expect(ajaxArgs.data).toEqual(data);
expect(ajaxArgs.headers.hasOwnProperty("X-CSRFToken")).toBe(true);
};
var assertEmailStatus = function(success, expectedStatus) {
if (!success) {
expect(view.$emailStatus).toHaveClass("validation-error");
} else {
expect(view.$emailStatus).not.toHaveClass("validation-error");
}
expect(view.$emailStatus.text()).toEqual(expectedStatus);
};
var assertPasswordStatus = function(success, expectedStatus) {
if (!success) {
expect(view.$passwordStatus).toHaveClass("validation-error");
} else {
expect(view.$passwordStatus).not.toHaveClass("validation-error");
}
expect(view.$passwordStatus.text()).toEqual(expectedStatus);
};
var assertRequestStatus = function(success, expectedStatus) {
if (!success) {
expect(view.$requestStatus).toHaveClass("error");
} else {
expect(view.$requestStatus).not.toHaveClass("error");
}
expect(view.$requestStatus.text()).toEqual(expectedStatus);
};
beforeEach(function() {
var fixture = readFixtures("templates/student_account/account.underscore");
setFixtures("<div id=\"account-tpl\">" + fixture + "</div>");
view = new edx.student.account.AccountView().render();
// Stub Ajax cals to return success/failure
spyOn($, "ajax").andCallFake(function() {
return $.Deferred(function(defer) {
if (ajaxSuccess) {
defer.resolve();
} else {
defer.reject();
}
}).promise();
});
});
it("requests an email address change", function() {
requestEmailChange("bob@example.com", "password");
assertAjax("email", "POST", {
email: "bob@example.com",
password: "password"
});
assertRequestStatus(true, "Please check your email to confirm the change");
});
it("displays email validation errors", function() {
// Invalid email should display an error
requestEmailChange("invalid", "password");
assertEmailStatus(false, "Please enter a valid email address");
// Once the error is fixed, the status should return to normal
requestEmailChange("bob@example.com", "password");
assertEmailStatus(true, "");
});
it("displays an invalid password error", function() {
// Password cannot be empty
requestEmailChange("bob@example.com", "");
assertPasswordStatus(false, "Please enter a valid password");
// Once the error is fixed, the status should return to normal
requestEmailChange("bob@example.com", "password");
assertPasswordStatus(true, "");
});
it("displays server errors", function() {
// Simulate an error from the server
ajaxSuccess = false;
requestEmailChange("bob@example.com", "password");
assertRequestStatus(false, "The data could not be saved.");
// On retry, it should succeed
ajaxSuccess = true;
requestEmailChange("bob@example.com", "password");
assertRequestStatus(true, "Please check your email to confirm the change");
});
});
}
);
define(['js/student_profile/profile'],
function() {
describe("edx.student.profile.ProfileModel", function() {
'use strict';
var profile = null;
beforeEach(function() {
profile = new edx.student.profile.ProfileModel();
});
it("validates the full name field", function() {
// Full name cannot be blank
profile.set("fullName", "");
var errors = profile.validate(profile.attributes);
expect(errors).toEqual({
fullName: "Full name cannot be blank"
});
// Fill in the name and expect that the model is valid
profile.set("fullName", "Bob");
errors = profile.validate(profile.attributes);
expect(errors).toBe(undefined);
});
});
describe("edx.student.profile.PreferencesModel", function() {
var preferences = null;
beforeEach(function() {
preferences = new edx.student.profile.PreferencesModel();
});
it("validates the language field", function() {
// Language cannot be blank
preferences.set("language", "");
var errors = preferences.validate(preferences.attributes);
expect(errors).toEqual({
language: "Language cannot be blank"
});
// Fill in the language and expect that the model is valid
preferences.set("language", "eo");
errors = preferences.validate(preferences.attributes);
expect(errors).toBe(undefined);
});
});
describe("edx.student.profile.ProfileView", function() {
var view = null,
ajaxSuccess = true;
var updateProfile = function(fields) {
view.profileModel.set(fields);
view.clearStatus();
view.profileModel.save();
};
var updatePreferences = function(fields) {
view.preferencesModel.set(fields);
view.clearStatus();
view.preferencesModel.save();
};
var assertAjax = function(url, method, data) {
expect($.ajax).toHaveBeenCalled();
var ajaxArgs = $.ajax.mostRecentCall.args[0];
expect(ajaxArgs.url).toEqual(url);
expect(ajaxArgs.type).toEqual(method);
expect(ajaxArgs.data).toEqual(data)
expect(ajaxArgs.headers.hasOwnProperty("X-CSRFToken")).toBe(true);
};
var assertSubmitStatus = function(success, expectedStatus) {
if (!success) {
expect(view.$submitStatus).toHaveClass("error");
} else {
expect(view.$submitStatus).not.toHaveClass("error");
}
expect(view.$submitStatus.text()).toEqual(expectedStatus);
};
var assertValidationError = function(expectedError, selection) {
if (expectedError === null) {
expect(selection).not.toHaveClass("validation-error");
expect(selection.text()).toEqual("");
} else {
expect(selection).toHaveClass("validation-error");
expect(selection.text()).toEqual(expectedError);
}
};
beforeEach(function() {
var profileFixture = readFixtures("templates/student_profile/profile.underscore"),
languageFixture = readFixtures("templates/student_profile/languages.underscore");
setFixtures("<div id=\"profile-tpl\">" + profileFixture + "</div>");
appendSetFixtures("<div id=\"languages-tpl\">" + languageFixture + "</div>");
// Stub AJAX calls to return success / failure
spyOn($, "ajax").andCallFake(function() {
return $.Deferred(function(defer) {
if (ajaxSuccess) {
defer.resolve();
} else {
defer.reject();
}
}).promise();
});
var json = {
preferredLanguage: {code: 'eo', name: 'Dummy language'},
languages: [{code: 'eo', name: 'Dummy language'}]
};
spyOn($, "getJSON").andCallFake(function() {
return $.Deferred(function(defer) {
if (ajaxSuccess) {
defer.resolveWith(this, [json]);
} else {
defer.reject();
}
}).promise();
});
// Stub location.reload() to prevent test suite from reloading repeatedly
spyOn(edx.student.profile, "reloadPage").andCallFake(function() {
return true;
});
view = new edx.student.profile.ProfileView().render();
});
it("updates the student profile", function() {
updateProfile({fullName: "John Smith"});
assertAjax("", "PUT", {fullName: "John Smith"});
assertSubmitStatus(true, "Saved");
});
it("updates the student preferences", function() {
updatePreferences({language: "eo"});
assertAjax("preferences", "PUT", {language: "eo"});
assertSubmitStatus(true, "Saved");
});
it("displays full name validation errors", function() {
// Blank name should display a validation error
updateProfile({fullName: ""});
assertValidationError("Full name cannot be blank", view.$nameStatus);
// If we fix the problem and resubmit, the error should go away
updateProfile({fullName: "John Smith"});
assertValidationError(null, view.$nameStatus);
});
it("displays language validation errors", function() {
// Blank language should display a validation error
updatePreferences({language: ""});
assertValidationError("Language cannot be blank", view.$languageStatus);
// If we fix the problem and resubmit, the error should go away
updatePreferences({language: "eo"});
assertValidationError(null, view.$languageStatus);
});
it("displays an error if the sync fails", function() {
// If we get an error status on the AJAX request, display an error
ajaxSuccess = false;
updateProfile({fullName: "John Smith"});
assertSubmitStatus(false, "The data could not be saved.");
// If we try again and succeed, the error should go away
ajaxSuccess = true;
updateProfile({fullName: "John Smith"});
assertSubmitStatus(true, "Saved");
});
});
}
);
var edx = edx || {};
(function($) {
(function($, _, Backbone, gettext) {
'use strict';
edx.student = edx.student || {};
edx.student.account = (function() {
var _fn = {
init: function() {
_fn.ajax.init();
_fn.eventHandlers.init();
},
eventHandlers: {
init: function() {
_fn.eventHandlers.submit();
},
submit: function() {
$('#email-change-form').submit( _fn.form.submit );
}
},
ajax: {
init: function() {
var csrftoken = _fn.cookie.get( 'csrftoken' );
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if ( settings.type === 'PUT' ) {
xhr.setRequestHeader( 'X-CSRFToken', csrftoken );
}
}
});
},
put: function( url, data ) {
$.ajax({
url: url,
type: 'PUT',
data: data
});
}
},
cookie: {
get: function( name ) {
return $.cookie(name);
}
},
form: {
isValid: true,
submit: function( event ) {
var $email = $('#new-email'),
$password = $('#password'),
data = {
new_email: $email.val(),
password: $password.val()
};
event.preventDefault();
_fn.form.validate( $('#email-change-form') );
if ( _fn.form.isValid ) {
_fn.ajax.put( 'email_change_request', data );
}
},
validate: function( $form ) {
_fn.form.isValid = true;
$form.find('input').each( _fn.valid.input );
}
},
regex: {
email: function() {
// taken from http://parsleyjs.org/
return /^((([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+(\.([a-z]|\d|[!#\$%&'\*\+\-\/=\?\^_`{\|}~]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+)*)|((\x22)((((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(([\x01-\x08\x0b\x0c\x0e-\x1f\x7f]|\x21|[\x23-\x5b]|[\x5d-\x7e]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(\\([\x01-\x09\x0b\x0c\x0d-\x7f]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF]))))*(((\x20|\x09)*(\x0d\x0a))?(\x20|\x09)+)?(\x22)))@((([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|\d|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))\.)+(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])|(([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])([a-z]|\d|-|\.|_|~|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])*([a-z]|[\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])))$/i;
}
},
valid: {
email: function( str ) {
var valid = false,
len = str ? str.length : 0,
regex = _fn.regex.email();
if ( 0 < len && len < 254 ) {
valid = regex.test( str );
}
return valid;
},
input: function() {
var $el = $(this),
validation = $el.data('validate'),
value = $el.val(),
valid = true;
if ( validation && validation.length > 0 ) {
$el.removeClass('error')
.css('border-color', '#c8c8c8'); // temp. for development
// Required field
if ( validation.indexOf('required') > -1 ) {
valid = _fn.valid.required( value );
}
// Email address
if ( valid && validation.indexOf('email') > -1 ) {
valid = _fn.valid.email( value );
}
if ( !valid ) {
$el.addClass('error')
.css('border-color', '#f00'); // temp. for development
_fn.form.isValid = false;
}
}
},
required: function( str ) {
return ( str && str.length > 0 ) ? true : false;
}
edx.student.account = {};
edx.student.account.AccountModel = Backbone.Model.extend({
// These should be the same length limits enforced by the server
EMAIL_MIN_LENGTH: 3,
EMAIL_MAX_LENGTH: 254,
PASSWORD_MIN_LENGTH: 2,
PASSWORD_MAX_LENGTH: 75,
// This is the same regex used to validate email addresses in Django 1.4
EMAIL_REGEX: new RegExp(
"(^[-!#$%&'*+/=?^_`{}|~0-9A-Z]+(\\.[-!#$%&'*+/=?^_`{}|~0-9A-Z]+)*" +
'|^"([\\001-\\010\\013\\014\\016-\\037!#-\\[\\]-\\177]|\\\\[\\001-\\011\\013\\014\\016-\\177])*"' +
')@((?:[A-Z0-9](?:[A-Z0-9-]{0,61}[A-Z0-9])?\\.)+[A-Z]{2,6}\\.?$)' +
'|\\[(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)(\\.(25[0-5]|2[0-4]\\d|[0-1]?\\d?\\d)){3}\\]$',
'i'
),
defaults: {
email: '',
password: ''
},
urlRoot: 'email',
sync: function(method, model) {
var headers = {
'X-CSRFToken': $.cookie('csrftoken')
};
$.ajax({
url: model.urlRoot,
type: 'POST',
data: model.attributes,
headers: headers
})
.done(function() {
model.trigger('sync');
})
.fail(function() {
var error = gettext("The data could not be saved.");
model.trigger('error', error);
});
},
validate: function(attrs) {
var errors = {};
if (attrs.email.length < this.EMAIL_MIN_LENGTH ||
attrs.email.length > this.EMAIL_MAX_LENGTH ||
!this.EMAIL_REGEX.test(attrs.email)
) { errors.email = gettext("Please enter a valid email address"); }
if (attrs.password.length < this.PASSWORD_MIN_LENGTH || attrs.password.length > this.PASSWORD_MAX_LENGTH) {
errors.password = gettext("Please enter a valid password");
}
};
return {
init: _fn.init
};
})();
edx.student.account.init();
if (!$.isEmptyObject(errors)) {
return errors;
}
}
});
edx.student.account.AccountView = Backbone.View.extend({
events: {
'submit': 'submit',
'change': 'change'
},
initialize: function() {
_.bindAll(this, 'render', 'submit', 'change', 'clearStatus', 'invalid', 'error', 'sync');
this.model = new edx.student.account.AccountModel();
this.model.on('invalid', this.invalid);
this.model.on('error', this.error);
this.model.on('sync', this.sync);
},
render: function() {
this.$el.html(_.template($('#account-tpl').html(), {}));
this.$email = $('#new-email', this.$el);
this.$password = $('#password', this.$el);
this.$emailStatus = $('#new-email-status', this.$el);
this.$passwordStatus = $('#password-status', this.$el);
this.$requestStatus = $('#request-email-status', this.$el);
return this;
},
submit: function(event) {
event.preventDefault();
this.clearStatus();
this.model.save();
},
change: function() {
this.model.set({
email: this.$email.val(),
password: this.$password.val()
});
},
invalid: function(model) {
var errors = model.validationError;
if (errors.hasOwnProperty('email')) {
this.$emailStatus
.addClass('validation-error')
.text(errors.email);
}
})(jQuery);
if (errors.hasOwnProperty('password')) {
this.$passwordStatus
.addClass('validation-error')
.text(errors.password);
}
},
error: function(error) {
this.$requestStatus
.addClass('error')
.text(error);
},
sync: function() {
this.$requestStatus
.addClass('success')
.text(gettext("Please check your email to confirm the change"));
},
clearStatus: function() {
this.$emailStatus
.removeClass('validation-error')
.text("");
this.$passwordStatus
.removeClass('validation-error')
.text("");
this.$requestStatus
.removeClass('error')
.text("");
},
});
return new edx.student.account.AccountView({
el: $('#account-container')
}).render();
})(jQuery, _, Backbone, gettext);
var edx = edx || {};
(function($) {
(function($, _, Backbone, gettext) {
'use strict';
edx.student = edx.student || {};
edx.student.profile = (function() {
var _fn = {
init: function() {
_fn.ajax.init();
_fn.eventHandlers.init();
},
eventHandlers: {
init: function() {
_fn.eventHandlers.submit();
_fn.eventHandlers.click();
},
submit: function() {
$('#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: {
submit: function( event, idSelector, key, url, onSuccess ) {
var $selection = $(idSelector),
data = {};
data[key] = $selection.val();
event.preventDefault();
_fn.ajax.put( url, data, onSuccess );
}
},
ajax: {
init: function() {
var csrftoken = _fn.cookie.get( 'csrftoken' );
$.ajaxSetup({
beforeSend: function( xhr, settings ) {
if ( settings.type === 'PUT' ) {
xhr.setRequestHeader( 'X-CSRFToken', csrftoken );
}
}
});
},
put: function( url, data, onSuccess ) {
$.ajax({
url: url,
type: 'PUT',
data: data,
success: onSuccess ? onSuccess : ''
});
}
},
cookie: {
get: function( name ) {
return $.cookie(name);
}
},
};
return {
init: _fn.init
};
})();
edx.student.profile.init();
})(jQuery);
edx.student.profile = {};
var syncErrorMessage = gettext("The data could not be saved.");
edx.student.profile.reloadPage = function() {
location.reload();
};
edx.student.profile.ProfileModel = Backbone.Model.extend({
defaults: {
fullName: ''
},
urlRoot: '',
sync: function(method, model) {
var headers = {
'X-CSRFToken': $.cookie('csrftoken')
};
$.ajax({
url: model.urlRoot,
type: 'PUT',
data: model.attributes,
headers: headers
})
.done(function() {
model.trigger('sync');
})
.fail(function() {
model.trigger('error', syncErrorMessage);
});
},
validate: function(attrs) {
var errors = {};
if (attrs.fullName.length < 1) {
errors.fullName = gettext("Full name cannot be blank");
}
if (!$.isEmptyObject(errors)) {
return errors;
}
}
});
edx.student.profile.PreferencesModel = Backbone.Model.extend({
defaults: {
language: 'en'
},
urlRoot: 'preferences',
sync: function(method, model) {
var headers = {
'X-CSRFToken': $.cookie('csrftoken')
};
$.ajax({
url: model.urlRoot,
type: 'PUT',
data: model.attributes,
headers: headers
})
.done(function() {
model.trigger('sync');
edx.student.profile.reloadPage();
})
.fail(function() {
model.trigger('error', syncErrorMessage);
});
},
validate: function(attrs) {
var errors = {};
if (attrs.language.length < 1) {
errors.language = gettext("Language cannot be blank");
}
if (!$.isEmptyObject(errors)) {
return errors;
}
}
});
edx.student.profile.ProfileView = Backbone.View.extend({
events: {
'submit': 'submit',
'change': 'change'
},
initialize: function() {
_.bindAll(this, 'render', 'change', 'submit', 'invalidProfile', 'invalidPreference', 'error', 'sync', 'clearStatus');
this.profileModel = new edx.student.profile.ProfileModel();
this.profileModel.on('invalid', this.invalidProfile);
this.profileModel.on('error', this.error);
this.profileModel.on('sync', this.sync);
this.preferencesModel = new edx.student.profile.PreferencesModel();
this.preferencesModel.on('invalid', this.invalidPreference);
this.preferencesModel.on('error', this.error);
this.preferencesModel.on('sync', this.sync);
},
render: function() {
this.$el.html(_.template($('#profile-tpl').html()));
this.$nameField = $('#profile-name', this.$el);
this.$nameStatus = $('#profile-name-status', this.$el);
this.$languageChoices = $('#preference-language', this.$el);
this.$languageStatus = $('#preference-language-status', this.$el);
this.$submitStatus = $('#submit-status', this.$el);
var self = this;
$.getJSON('preferences/languages')
.done(function(json) {
/** Asynchronously populate the language choices. */
self.$languageChoices.html(_.template($('#languages-tpl').html(), {languageInfo: json}));
})
.fail(function() {
self.$languageStatus
.addClass('language-list-error')
.text(gettext("We couldn't populate the list of language choices."));
});
return this;
},
change: function() {
this.profileModel.set({
fullName: this.$nameField.val()
});
this.preferencesModel.set({
language: this.$languageChoices.val()
});
},
submit: function(event) {
event.preventDefault();
this.clearStatus();
this.profileModel.save();
this.preferencesModel.save();
},
invalidProfile: function(model) {
var errors = model.validationError;
if (errors.hasOwnProperty('fullName')) {
this.$nameStatus
.addClass('validation-error')
.text(errors.fullName);
}
},
invalidPreference: function(model) {
var errors = model.validationError;
if (errors.hasOwnProperty('language')) {
this.$languageStatus
.addClass('validation-error')
.text(errors.language);
}
},
error: function(error) {
this.$submitStatus
.addClass('error')
.text(error);
},
sync: function() {
this.$submitStatus
.addClass('success')
.text(gettext("Saved"));
},
clearStatus: function() {
this.$nameStatus
.removeClass('validation-error')
.text("");
this.$languageStatus
.removeClass('validation-error')
.text("");
this.$submitStatus
.removeClass('error')
.text("");
}
});
return new edx.student.profile.ProfileView({
el: $('#profile-container')
}).render();
})(jQuery, _, Backbone, gettext);
......@@ -72,6 +72,8 @@ spec_paths:
fixture_paths:
- templates/instructor/instructor_dashboard_2
- templates/dashboard
- templates/student_account
- templates/student_profile
requirejs:
paths:
......
<form id="email-change-form" method="post">
<label for="new-email"><%- gettext('New Address') %></label>
<input id="new-email" type="text" name="new-email" value="" placeholder="xsy@edx.org" data-validate="required email"/>
<div id="new-email-status" />
<label for="password"><%- gettext('Password') %></label>
<input id="password" type="password" name="password" value="" data-validate="required"/>
<div id="password-status" />
<div class="submit-button">
<input type="submit" id="email-change-submit" value="<%- gettext('Change My Email Address') %>">
</div>
<div id="request-email-status" />
</form>
......@@ -18,9 +18,9 @@ ${_("There was recently a request to change the email address associated "
## Confirmation link
% if is_secure:
https://${site}/account/email_change_confirm/${key}
https://${site}/account/email/confirmation/${key}
% else:
http://${site}/account/email_change_confirm/${key}
http://${site}/account/email/confirmation/${key}
% endif
## Closing
......
......@@ -6,23 +6,21 @@
<%block name="pagetitle">${_("Student Account")}</%block>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<%static:js group='student_account'/>
</%block>
<%block name="header_extras">
% for template_name in ["account"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="student_account/${template_name}.underscore" />
</script>
% endfor
</%block>
<h1>Student Account</h1>
<p>This is a placeholder for the student's account page.</p>
<form id="email-change-form" method="post">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<label for="new-email">${_('New Address')}</label>
<input id="new-email" type="text" name="new-email" value="" placeholder="xsy@edx.org" data-validate="required email"/>
<label for="password">${_('Password')}</label>
<input id="password" type="password" name="password" value="" data-validate="required"/>
<div class="submit-button">
<input type="submit" id="email-change-submit" value="${_('Change My Email Address')}">
</div>
</form>
<div id="account-container" />
......@@ -6,55 +6,24 @@
<%block name="pagetitle">${_("Student Profile")}</%block>
<%block name="js_extra">
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/backbone-min.js')}"></script>
<%static:js group='student_profile'/>
</%block>
<h1>Student Profile</h1>
<%block name="header_extras">
% for template_name in ["profile", "languages"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="student_profile/${template_name}.underscore" />
</script>
% endfor
</%block>
<h1>${_("Student Profile")}</h1>
<p>This is a placeholder for the student's profile page.</p>
<form id="name-change-form">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<label for="new-name">${_("Full Name")}</label>
<input id="new-name" type="text" name="new-name" value="" placeholder="Xsy" />
<div class="submit-button">
<input type="submit" id="name-change-submit" value="${_("Change My Name")}">
</div>
</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>
<div id="profile-container"></div>
% if settings.FEATURES.get('ENABLE_THIRD_PARTY_AUTH'):
<%include file="third_party_auth.html" />
......
<% _.each( languageInfo.languages, function( language ){ %>
<% if ( language.name === languageInfo.preferredLanguage.name ){ %>
<option value=<%= language.code %> selected="selected"><%= language.name %></option>
<% } else { %>
<option value=<%= language.code %>><%= language.name %></option>
<% } %>
<% }); %>
<form id="profile-form">
<label for="profile-name"><%- gettext("Full Name") %></label>
<input id="profile-name" type="text" name="profile-name" value="" placeholder="Xsy" />
<div id="profile-name-status" />
<label for="preference-language"><%- gettext('Preferred Language') %></label>
<select id="preference-language" name="preference-language"></select>
<div id="preference-language-status" />
<div class="profile-submit">
<input type="submit" id="submit-button" value="<%- gettext('Update Profile') %>">
</div>
<div id="submit-status" />
</form>
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