Commit 00d976b8 by Renzo Lucioni

Use Backbone for student account and profile JS.

Validate student account and profile form fields. Use RequireJS for Jasmine tests of account and profile JS.
parent 56d51c4f
......@@ -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