Commit 4f97cd7a by muzaffaryousaf Committed by Andy Armstrong

Learner profile page.

TNL-1502
parent 229a37b9
...@@ -202,3 +202,9 @@ class DashboardPage(PageObject): ...@@ -202,3 +202,9 @@ class DashboardPage(PageObject):
Click on `Account Settings` link. Click on `Account Settings` link.
""" """
self.q(css='.dropdown-menu li a').first.click() self.q(css='.dropdown-menu li a').first.click()
def click_my_profile_link(self):
"""
Click on `My Profile` link.
"""
self.q(css='.dropdown-menu li a').nth(1).click()
...@@ -30,6 +30,40 @@ class FieldsMixin(object): ...@@ -30,6 +30,40 @@ class FieldsMixin(object):
"Field with id \"{0}\" is in DOM.".format(field_id) "Field with id \"{0}\" is in DOM.".format(field_id)
).fulfill() ).fulfill()
def mode_for_field(self, field_id):
"""
Extract current field mode.
Returns:
`placeholder`/`edit`/`display`
"""
self.wait_for_field(field_id)
query = self.q(css='.u-field-{}'.format(field_id))
if not query.present:
return None
field_classes = query.attrs('class')[0].split()
if 'mode-placeholder' in field_classes:
return 'placeholder'
if 'mode-display' in field_classes:
return 'display'
if 'mode-edit' in field_classes:
return 'edit'
def icon_for_field(self, field_id, icon_id):
"""
Check if field icon is present.
"""
self.wait_for_field(field_id)
query = self.q(css='.u-field-{} .u-field-icon'.format(field_id))
return query.present and icon_id in query.attrs('class')[0].split()
def title_for_field(self, field_id): def title_for_field(self, field_id):
""" """
Return the title of a field. Return the title of a field.
...@@ -79,6 +113,23 @@ class FieldsMixin(object): ...@@ -79,6 +113,23 @@ class FieldsMixin(object):
"Indicator \"{0}\" is visible.".format(self.indicator_for_field(field_id)) "Indicator \"{0}\" is visible.".format(self.indicator_for_field(field_id))
).fulfill() ).fulfill()
def make_field_editable(self, field_id):
"""
Make a field editable.
"""
query = self.q(css='.u-field-{}'.format(field_id))
if not query.present:
return None
field_classes = query.attrs('class')[0].split()
if 'mode-placeholder' in field_classes or 'mode-display' in field_classes:
if field_id == 'bio':
self.q(css='.u-field-bio > .wrapper-u-field').first.click()
else:
self.q(css='.u-field-{}'.format(field_id)).first.click()
def value_for_readonly_field(self, field_id): def value_for_readonly_field(self, field_id):
""" """
Return the value in a readonly field. Return the value in a readonly field.
...@@ -104,19 +155,54 @@ class FieldsMixin(object): ...@@ -104,19 +155,54 @@ class FieldsMixin(object):
query.results[0].send_keys(u'\ue007') # Press Enter query.results[0].send_keys(u'\ue007') # Press Enter
return query.attrs('value')[0] return query.attrs('value')[0]
def value_for_textarea_field(self, field_id, value=None):
"""
Get or set the value of a textarea field.
"""
self.wait_for_field(field_id)
self.make_field_editable(field_id)
query = self.q(css='.u-field-{} textarea'.format(field_id))
if not query.present:
return None
if value is not None:
query.fill(value)
query.results[0].send_keys(u'\ue004') # Focus Out using TAB
if self.mode_for_field(field_id) == 'edit':
return query.text[0]
else:
return self.get_non_editable_mode_value(field_id)
def get_non_editable_mode_value(self, field_id):
"""
Return value of field in `display` or `placeholder` mode.
"""
self.wait_for_field(field_id)
return self.q(css='.u-field-{} .u-field-value'.format(field_id)).text[0]
def value_for_dropdown_field(self, field_id, value=None): def value_for_dropdown_field(self, field_id, value=None):
""" """
Get or set the value in a dropdown field. Get or set the value in a dropdown field.
""" """
self.wait_for_field(field_id) self.wait_for_field(field_id)
self.make_field_editable(field_id)
query = self.q(css='.u-field-{} select'.format(field_id)) query = self.q(css='.u-field-{} select'.format(field_id))
if not query.present: if not query.present:
return None return None
if value is not None: if value is not None:
select_option_by_text(query, value) select_option_by_text(query, value)
return get_selected_option_text(query)
if self.mode_for_field(field_id) == 'edit':
return get_selected_option_text(query)
else:
return self.get_non_editable_mode_value(field_id)
def link_title_for_link_field(self, field_id): def link_title_for_link_field(self, field_id):
""" """
......
"""
Bok-Choy PageObject class for learner profile page.
"""
from . import BASE_URL
from bok_choy.page_object import PageObject
from .fields import FieldsMixin
from bok_choy.promise import EmptyPromise
PROFILE_VISIBILITY_SELECTOR = '#u-field-select-account_privacy option[value="{}"]'
FIELD_ICONS = {
'country': 'fa-map-marker',
'language_proficiencies': 'fa-comment',
}
class LearnerProfilePage(FieldsMixin, PageObject):
"""
PageObject methods for Learning Profile Page.
"""
def __init__(self, browser, username):
"""
Initialize the page.
Arguments:
browser (Browser): The browser instance.
username (str): Profile username.
"""
super(LearnerProfilePage, self).__init__(browser)
self.username = username
@property
def url(self):
"""
Construct a URL to the page.
"""
return BASE_URL + "/u/" + self.username
def is_browser_on_page(self):
"""
Check if browser is showing correct page.
"""
return 'Learner Profile' in self.browser.title
@property
def privacy(self):
"""
Get user profile privacy.
Returns:
'all_users' or 'private'
"""
return 'all_users' if self.q(css=PROFILE_VISIBILITY_SELECTOR.format('all_users')).selected else 'private'
@privacy.setter
def privacy(self, privacy):
"""
Set user profile privacy.
Arguments:
privacy (str): 'all_users' or 'private'
"""
self.wait_for_element_visibility('select#u-field-select-account_privacy', 'Privacy dropdown is visiblie')
if privacy != self.privacy:
self.q(css=PROFILE_VISIBILITY_SELECTOR.format(privacy)).first.click()
EmptyPromise(lambda: privacy == self.privacy, 'Privacy is set to {}'.format(privacy)).fulfill()
self.wait_for_ajax()
if privacy == 'all_users':
self.wait_for_public_fields()
def field_is_visible(self, field_id):
"""
Check if a field with id set to `field_id` is shown.
Arguments:
field_id (str): field id
Returns:
True/False
"""
self.wait_for_ajax()
return self.q(css='.u-field-{}'.format(field_id)).visible
def field_is_editable(self, field_id):
"""
Check if a field with id set to `field_id` is editable.
Arguments:
field_id (str): field id
Returns:
True/False
"""
self.wait_for_field(field_id)
self.make_field_editable(field_id)
return self.mode_for_field(field_id) == 'edit'
@property
def visible_fields(self):
"""
Return list of visible fields.
"""
self.wait_for_field('username')
fields = ['username', 'country', 'language_proficiencies', 'bio']
return [field for field in fields if self.field_is_visible(field)]
@property
def editable_fields(self):
"""
Return list of editable fields currently shown on page.
"""
self.wait_for_ajax()
self.wait_for_element_visibility('.u-field-username', 'username is not visible')
fields = ['country', 'language_proficiencies', 'bio']
return [field for field in fields if self.field_is_editable(field)]
@property
def privacy_field_visible(self):
"""
Check if profile visibility selector is shown or not.
Returns:
True/False
"""
self.wait_for_ajax()
return self.q(css='#u-field-select-account_privacy').visible
def field_icon_present(self, field_id):
"""
Check if an icon is present for a field. Only dropdown fields have icons.
Arguments:
field_id (str): field id
Returns:
True/False
"""
return self.icon_for_field(field_id, FIELD_ICONS[field_id])
def wait_for_public_fields(self):
"""
Wait for `country`, `language` and `bio` fields to be visible.
"""
EmptyPromise(lambda: self.field_is_visible('country'), 'Country field is visible').fulfill()
EmptyPromise(lambda: self.field_is_visible('language_proficiencies'), 'Language field is visible').fulfill()
EmptyPromise(lambda: self.field_is_visible('bio'), 'About Me field is visible').fulfill()
# -*- coding: utf-8 -*-
""" Tests for student profile views. """
from django.conf import settings
from django.core.urlresolvers import reverse
from django.test import TestCase
from util.testing import UrlResetMixin
from student.tests.factories import UserFactory
from student_profile.views import learner_profile_context
class LearnerProfileViewTest(UrlResetMixin, TestCase):
""" Tests for the student profile view. """
USERNAME = "username"
PASSWORD = "password"
CONTEXT_DATA = [
'default_public_account_fields',
'accounts_api_url',
'preferences_api_url',
'account_settings_page_url',
'has_preferences_access',
'own_profile',
'country_options',
'language_options',
]
def setUp(self):
super(LearnerProfileViewTest, self).setUp()
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
def test_context(self):
"""
Verify learner profile page context data.
"""
context = learner_profile_context(self.user.username, self.USERNAME, self.user.is_staff)
self.assertEqual(
context['data']['default_public_account_fields'],
settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields']
)
self.assertEqual(
context['data']['accounts_api_url'],
reverse("accounts_api", kwargs={'username': self.user.username})
)
self.assertEqual(
context['data']['preferences_api_url'],
reverse('preferences_api', kwargs={'username': self.user.username})
)
self.assertEqual(context['data']['account_settings_page_url'], reverse('account_settings'))
for attribute in self.CONTEXT_DATA:
self.assertIn(attribute, context['data'])
def test_view(self):
"""
Verify learner profile page view.
"""
profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME})
response = self.client.get(path=profile_path)
for attribute in self.CONTEXT_DATA:
self.assertIn(attribute, response.content)
""" Views for a student's profile information. """
from django.conf import settings
from django_countries import countries
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django.views.decorators.http import require_http_methods
from edxmako.shortcuts import render_to_response
@login_required
@require_http_methods(['GET'])
def learner_profile(request, username):
"""
Render the students profile page.
Args:
request (HttpRequest)
username (str): username of user whose profile is requested.
Returns:
HttpResponse: 200 if the page was sent successfully
HttpResponse: 302 if not logged in (redirect to login page)
HttpResponse: 405 if using an unsupported HTTP method
Example usage:
GET /account/profile
"""
return render_to_response(
'student_profile/learner_profile.html',
learner_profile_context(request.user.username, username, request.user.is_staff)
)
def learner_profile_context(logged_in_username, profile_username, user_is_staff):
"""
Context for the learner profile page.
Args:
logged_in_username (str): Username of user logged In user.
profile_username (str): username of user whose profile is requested.
user_is_staff (bool): Logged In user has staff access.
Returns:
dict
"""
language_options = [language for language in settings.ALL_LANGUAGES]
country_options = [
(country_code, unicode(country_name))
for country_code, country_name in sorted(
countries.countries, key=lambda(__, name): unicode(name)
)
]
context = {
'data': {
'default_public_account_fields': settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields'],
'accounts_api_url': reverse("accounts_api", kwargs={'username': profile_username}),
'preferences_api_url': reverse('preferences_api', kwargs={'username': profile_username}),
'account_settings_page_url': reverse('account_settings'),
'has_preferences_access': (logged_in_username == profile_username or user_is_staff),
'own_profile': (logged_in_username == profile_username),
'country_options': country_options,
'language_options': language_options,
}
}
return context
...@@ -90,6 +90,9 @@ ...@@ -90,6 +90,9 @@
'js/student_account/views/RegisterView': 'js/student_account/views/RegisterView', 'js/student_account/views/RegisterView': 'js/student_account/views/RegisterView',
'js/student_account/views/AccessView': 'js/student_account/views/AccessView', 'js/student_account/views/AccessView': 'js/student_account/views/AccessView',
'js/student_profile/profile': 'js/student_profile/profile', 'js/student_profile/profile': 'js/student_profile/profile',
'js/student_profile/views/learner_profile_fields': 'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory',
'js/student_profile/views/learner_profile_view': 'js/student_profile/views/learner_profile_view',
// edxnotes // edxnotes
'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min' 'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min'
...@@ -593,6 +596,8 @@ ...@@ -593,6 +596,8 @@
'lms/include/js/spec/student_account/account_settings_view_spec.js', 'lms/include/js/spec/student_account/account_settings_view_spec.js',
'lms/include/js/spec/student_profile/profile_spec.js', 'lms/include/js/spec/student_profile/profile_spec.js',
'lms/include/js/spec/views/fields_spec.js', 'lms/include/js/spec/views/fields_spec.js',
'lms/include/js/spec/student_profile/learner_profile_factory_spec.js',
'lms/include/js/spec/student_profile/learner_profile_view_spec.js',
'lms/include/js/spec/verify_student/pay_and_verify_view_spec.js', 'lms/include/js/spec/verify_student/pay_and_verify_view_spec.js',
'lms/include/js/spec/verify_student/webcam_photo_view_spec.js', 'lms/include/js/spec/verify_student/webcam_photo_view_spec.js',
'lms/include/js/spec/verify_student/image_input_spec.js', 'lms/include/js/spec/verify_student/image_input_spec.js',
......
...@@ -132,7 +132,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -132,7 +132,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
expect(sectionsData[0].fields.length).toBe(5); expect(sectionsData[0].fields.length).toBe(5);
var textFields = [sectionsData[0].fields[1], sectionsData[0].fields[2]]; var textFields = [sectionsData[0].fields[1], sectionsData[0].fields[2]];
for (var i = 0; i < textFields ; i++) { for (var i = 0; i < textFields.length ; i++) {
var view = textFields[i].view; var view = textFields[i].view;
FieldViewsSpecHelpers.verifyTextField(view, { FieldViewsSpecHelpers.verifyTextField(view, {
...@@ -154,9 +154,9 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -154,9 +154,9 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
title: view.options.title, title: view.options.title,
valueAttribute: view.options.valueAttribute, valueAttribute: view.options.valueAttribute,
helpMessage: '', helpMessage: '',
validValue: Helpers.FIELD_OPTIONS[0][0], validValue: Helpers.FIELD_OPTIONS[1][0],
invalidValue1: Helpers.FIELD_OPTIONS[1][0], invalidValue1: Helpers.FIELD_OPTIONS[2][0],
invalidValue2: Helpers.FIELD_OPTIONS[2][0], invalidValue2: Helpers.FIELD_OPTIONS[3][0],
validationError: "Nope, this will not do!" validationError: "Nope, this will not do!"
}, requests); }, requests);
} }
......
...@@ -9,11 +9,13 @@ define(['underscore'], function(_) { ...@@ -9,11 +9,13 @@ define(['underscore'], function(_) {
name: 'Student', name: 'Student',
email: 'student@edx.org', email: 'student@edx.org',
level_of_education: '1', level_of_education: '0',
gender: '2', gender: '0',
year_of_birth: '3', year_of_birth: '0',
country: '1', country: '0',
language: '2' language: '0',
bio: "About the student",
language_proficiencies: [{code: '1'}]
}; };
var USER_PREFERENCES_DATA = { var USER_PREFERENCES_DATA = {
...@@ -21,6 +23,7 @@ define(['underscore'], function(_) { ...@@ -21,6 +23,7 @@ define(['underscore'], function(_) {
}; };
var FIELD_OPTIONS = [ var FIELD_OPTIONS = [
['0', 'Option 0'],
['1', 'Option 1'], ['1', 'Option 1'],
['2', 'Option 2'], ['2', 'Option 2'],
['3', 'Option 3'], ['3', 'Option 3'],
...@@ -28,9 +31,9 @@ define(['underscore'], function(_) { ...@@ -28,9 +31,9 @@ define(['underscore'], function(_) {
var expectLoadingIndicatorIsVisible = function (view, visible) { var expectLoadingIndicatorIsVisible = function (view, visible) {
if (visible) { if (visible) {
expect(view.$('.ui-loading-indicator')).not.toHaveClass('is-hidden'); expect($('.ui-loading-indicator')).not.toHaveClass('is-hidden');
} else { } else {
expect(view.$('.ui-loading-indicator')).toHaveClass('is-hidden'); expect($('.ui-loading-indicator')).toHaveClass('is-hidden');
} }
}; };
...@@ -95,6 +98,6 @@ define(['underscore'], function(_) { ...@@ -95,6 +98,6 @@ define(['underscore'], function(_) {
expectLoadingErrorIsVisible: expectLoadingErrorIsVisible, expectLoadingErrorIsVisible: expectLoadingErrorIsVisible,
expectElementContainsField: expectElementContainsField, expectElementContainsField: expectElementContainsField,
expectSettingsSectionsButNotFieldsToBeRendered: expectSettingsSectionsButNotFieldsToBeRendered, expectSettingsSectionsButNotFieldsToBeRendered: expectSettingsSectionsButNotFieldsToBeRendered,
expectSettingsSectionsAndFieldsToBeRendered: expectSettingsSectionsAndFieldsToBeRendered expectSettingsSectionsAndFieldsToBeRendered: expectSettingsSectionsAndFieldsToBeRendered,
}; };
}); });
define(['underscore'], function(_) {
'use strict';
var expectProfileElementContainsField = function(element, view) {
var $element = $(element);
var fieldTitle = $element.find('.u-field-title').text().trim();
if (!_.isUndefined(view.options.title)) {
expect(fieldTitle).toBe(view.options.title);
}
if ('fieldValue' in view) {
expect(view.model.get(view.options.valueAttribute)).toBeTruthy();
if (view.fieldValue()) {
expect(view.fieldValue()).toBe(view.modelValue());
} else if ('optionForValue' in view) {
expect($($element.find('.u-field-value')[0]).text()).toBe(view.displayValue(view.modelValue()));
}else {
expect($($element.find('.u-field-value')[0]).text()).toBe(view.modelValue());
}
} else {
throw new Error('Unexpected field type: ' + view.fieldType);
}
};
var expectProfilePrivacyFieldTobeRendered = function(learnerProfileView, othersProfile) {
var accountPrivacyElement = learnerProfileView.$('.wrapper-profile-field-account-privacy');
var privacyFieldElement = $(accountPrivacyElement).find('.u-field');
if (othersProfile) {
expect(privacyFieldElement.length).toBe(0);
} else {
expect(privacyFieldElement.length).toBe(1);
expectProfileElementContainsField(privacyFieldElement, learnerProfileView.options.accountPrivacyFieldView)
}
};
var expectSectionOneTobeRendered = function(learnerProfileView) {
var sectionOneFieldElements = $(learnerProfileView.$('.wrapper-profile-section-one')).find('.u-field');
expect(sectionOneFieldElements.length).toBe(learnerProfileView.options.sectionOneFieldViews.length);
_.each(sectionOneFieldElements, function (sectionFieldElement, fieldIndex) {
expectProfileElementContainsField(sectionFieldElement, learnerProfileView.options.sectionOneFieldViews[fieldIndex]);
});
};
var expectSectionTwoTobeRendered = function(learnerProfileView) {
var sectionTwoElement = learnerProfileView.$('.wrapper-profile-section-two');
var sectionTwoFieldElements = $(sectionTwoElement).find('.u-field');
expect(sectionTwoFieldElements.length).toBe(learnerProfileView.options.sectionTwoFieldViews.length);
_.each(sectionTwoFieldElements, function (sectionFieldElement, fieldIndex) {
expectProfileElementContainsField(sectionFieldElement, learnerProfileView.options.sectionTwoFieldViews[fieldIndex]);
});
};
var expectProfileSectionsAndFieldsToBeRendered = function (learnerProfileView, othersProfile) {
expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile);
expectSectionOneTobeRendered(learnerProfileView);
expectSectionTwoTobeRendered(learnerProfileView);
};
var expectLimitedProfileSectionsAndFieldsToBeRendered = function (learnerProfileView, othersProfile) {
expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile);
var sectionOneFieldElements = $(learnerProfileView.$('.wrapper-profile-section-one')).find('.u-field');
expect(sectionOneFieldElements.length).toBe(1);
_.each(sectionOneFieldElements, function (sectionFieldElement, fieldIndex) {
expectProfileElementContainsField(sectionFieldElement, learnerProfileView.options.sectionOneFieldViews[fieldIndex]);
});
if (othersProfile) {
expect($('.profile-private--message').text()).toBe('This edX learner is currently sharing a limited profile.')
} else {
expect($('.profile-private--message').text()).toBe('You are currently sharing a limited profile.')
}
};
var expectProfileSectionsNotToBeRendered = function(learnerProfileView) {
expect(learnerProfileView.$('.wrapper-profile-field-account-privacy').length).toBe(0);
expect(learnerProfileView.$('.wrapper-profile-section-one').length).toBe(0);
expect(learnerProfileView.$('.wrapper-profile-section-two').length).toBe(0);
};
return {
expectLimitedProfileSectionsAndFieldsToBeRendered: expectLimitedProfileSectionsAndFieldsToBeRendered,
expectProfileSectionsAndFieldsToBeRendered: expectProfileSectionsAndFieldsToBeRendered,
expectProfileSectionsNotToBeRendered: expectProfileSectionsNotToBeRendered
};
});
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
'js/spec/student_account/helpers',
'js/spec/student_profile/helpers',
'js/views/fields',
'js/student_account/models/user_account_model',
'js/student_account/models/user_preferences_model',
'js/student_profile/views/learner_profile_view',
'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_factory'
],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews, UserAccountModel, UserPreferencesModel,
LearnerProfileView, LearnerProfileFields, LearnerProfilePage) {
'use strict';
describe("edx.user.LearnerProfileFactory", function () {
var requests;
beforeEach(function () {
setFixtures('<div class="wrapper-profile"><div class="ui-loading-indicator"><p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">Loading</span></p></div><div class="ui-loading-error is-hidden"><i class="fa fa-exclamation-triangle message-error" aria-hidden=true></i><span class="copy">An error occurred. Please reload the page.</span></div></div>');
TemplateHelpers.installTemplate('templates/fields/field_readonly');
TemplateHelpers.installTemplate('templates/fields/field_dropdown');
TemplateHelpers.installTemplate('templates/fields/field_textarea');
TemplateHelpers.installTemplate('templates/student_profile/learner_profile');
});
it("show loading error when UserAccountModel fails to load", function() {
requests = AjaxHelpers.requests(this);
var context = LearnerProfilePage({
'accounts_api_url': Helpers.USER_ACCOUNTS_API_URL,
'preferences_api_url': Helpers.USER_PREFERENCES_API_URL,
'own_profile': true,
'account_settings_page_url': Helpers.USER_ACCOUNTS_API_URL,
'country_options': Helpers.FIELD_OPTIONS,
'language_options': Helpers.FIELD_OPTIONS,
'has_preferences_access': true
}),
learnerProfileView = context.learnerProfileView;
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectProfileSectionsNotToBeRendered(learnerProfileView);
var userAccountRequest = requests[0];
expect(userAccountRequest.method).toBe('GET');
expect(userAccountRequest.url).toBe(Helpers.USER_ACCOUNTS_API_URL);
AjaxHelpers.respondWithError(requests, 500);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, true);
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectProfileSectionsNotToBeRendered(learnerProfileView);
});
it("shows loading error when UserPreferencesModel fails to load", function() {
requests = AjaxHelpers.requests(this);
var context = LearnerProfilePage({
'accounts_api_url': Helpers.USER_ACCOUNTS_API_URL,
'preferences_api_url': Helpers.USER_PREFERENCES_API_URL,
'own_profile': true,
'account_settings_page_url': Helpers.USER_ACCOUNTS_API_URL,
'country_options': Helpers.FIELD_OPTIONS,
'language_options': Helpers.FIELD_OPTIONS,
'has_preferences_access': true
}),
learnerProfileView = context.learnerProfileView;
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectProfileSectionsNotToBeRendered(learnerProfileView);
var userAccountRequest = requests[0];
expect(userAccountRequest.method).toBe('GET');
expect(userAccountRequest.url).toBe(Helpers.USER_ACCOUNTS_API_URL);
AjaxHelpers.respondWithJson(requests, Helpers.USER_ACCOUNTS_DATA);
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectProfileSectionsNotToBeRendered(learnerProfileView);
var userPreferencesRequest = requests[1];
expect(userPreferencesRequest.method).toBe('GET');
expect(userPreferencesRequest.url).toBe(Helpers.USER_PREFERENCES_API_URL);
AjaxHelpers.respondWithError(requests, 500);
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, false);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, true);
LearnerProfileHelpers.expectProfileSectionsNotToBeRendered(learnerProfileView);
});
it("renders the limited profile after models are successfully fetched", function() {
requests = AjaxHelpers.requests(this);
var context = LearnerProfilePage({
'accounts_api_url': Helpers.USER_ACCOUNTS_API_URL,
'preferences_api_url': Helpers.USER_PREFERENCES_API_URL,
'own_profile': true,
'account_settings_page_url': Helpers.USER_ACCOUNTS_API_URL,
'country_options': Helpers.FIELD_OPTIONS,
'language_options': Helpers.FIELD_OPTIONS,
'has_preferences_access': true
});
var learnerProfileView = context.learnerProfileView;
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectProfileSectionsNotToBeRendered(learnerProfileView);
AjaxHelpers.respondWithJson(requests, Helpers.USER_ACCOUNTS_DATA);
AjaxHelpers.respondWithJson(requests, Helpers.USER_PREFERENCES_DATA);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView)
});
it("renders the full profile after models are successfully fetched", function() {
requests = AjaxHelpers.requests(this);
var context = LearnerProfilePage({
'accounts_api_url': Helpers.USER_ACCOUNTS_API_URL,
'preferences_api_url': Helpers.USER_PREFERENCES_API_URL,
'own_profile': true,
'account_settings_page_url': Helpers.USER_ACCOUNTS_API_URL,
'country_options': Helpers.FIELD_OPTIONS,
'language_options': Helpers.FIELD_OPTIONS,
'has_preferences_access': true
}),
learnerProfileView = context.learnerProfileView;
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectProfileSectionsNotToBeRendered(learnerProfileView);
AjaxHelpers.respondWithJson(requests, Helpers.USER_ACCOUNTS_DATA);
AjaxHelpers.respondWithJson(requests, Helpers.USER_PREFERENCES_DATA);
// sets the profile for full view.
context.accountPreferencesModel.set({account_privacy: 'all_users'});
LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView, false)
});
});
});
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
'js/spec/student_account/helpers',
'js/spec/student_profile/helpers',
'js/views/fields',
'js/student_account/models/user_account_model',
'js/student_account/models/user_preferences_model',
'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_view',
'js/student_account/views/account_settings_fields'
],
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews, UserAccountModel,
AccountPreferencesModel, LearnerProfileFields, LearnerProfileView, AccountSettingsFieldViews) {
'use strict';
describe("edx.user.LearnerProfileView", function (options) {
var createLearnerProfileView = function (ownProfile, accountPrivacy, profileIsPublic) {
var accountSettingsModel = new UserAccountModel();
accountSettingsModel.set(Helpers.USER_ACCOUNTS_DATA);
accountSettingsModel.set({'profile_is_public': profileIsPublic});
var accountPreferencesModel = new AccountPreferencesModel();
accountPreferencesModel.set({account_privacy: accountPrivacy});
accountPreferencesModel.url = Helpers.USER_PREFERENCES_API_URL;
var editable = ownProfile ? 'toggle' : 'never';
var accountPrivacyFieldView = new LearnerProfileFields.AccountPrivacyFieldView({
model: accountPreferencesModel,
required: true,
editable: 'always',
showMessages: false,
title: 'edX learners can see my:',
valueAttribute: "account_privacy",
options: [
['all_users', 'Full Profile'],
['private', 'Limited Profile']
],
helpMessage: '',
accountSettingsPageUrl: '/account/settings/'
});
var usernameFieldView = new FieldViews.ReadonlyFieldView({
model: accountSettingsModel,
valueAttribute: "username",
helpMessage: ""
});
var sectionOneFieldViews = [
usernameFieldView,
new FieldViews.DropdownFieldView({
model: accountSettingsModel,
required: false,
editable: editable,
showMessages: false,
iconName: 'fa-map-marker',
placeholderValue: 'Add country',
valueAttribute: "country",
options: Helpers.FIELD_OPTIONS,
helpMessage: ''
}),
new AccountSettingsFieldViews.LanguageProficienciesFieldView({
model: accountSettingsModel,
required: false,
editable: editable,
showMessages: false,
iconName: 'fa-comment',
placeholderValue: 'Add language',
valueAttribute: "language_proficiencies",
options: Helpers.FIELD_OPTIONS,
helpMessage: ''
})
];
var sectionTwoFieldViews = [
new FieldViews.TextareaFieldView({
model: accountSettingsModel,
editable: editable,
showMessages: false,
title: 'About me',
placeholderValue: "Tell other edX learners a little about yourself: where you live, what your interests are, why you're taking courses on edX, or what you hope to learn.",
valueAttribute: "bio",
helpMessage: ''
})
];
return new LearnerProfileView(
{
el: $('.wrapper-profile'),
own_profile: ownProfile,
has_preferences_access: true,
accountSettingsModel: accountSettingsModel,
preferencesModel: accountPreferencesModel,
accountPrivacyFieldView: accountPrivacyFieldView,
usernameFieldView: usernameFieldView,
sectionOneFieldViews: sectionOneFieldViews,
sectionTwoFieldViews: sectionTwoFieldViews
});
};
beforeEach(function () {
setFixtures('<div class="wrapper-profile"><div class="ui-loading-indicator"><p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">Loading</span></p></div><div class="ui-loading-error is-hidden"><i class="fa fa-exclamation-triangle message-error" aria-hidden=true></i><span class="copy">An error occurred. Please reload the page.</span></div></div>');
TemplateHelpers.installTemplate('templates/fields/field_readonly');
TemplateHelpers.installTemplate('templates/fields/field_dropdown');
TemplateHelpers.installTemplate('templates/fields/field_textarea');
TemplateHelpers.installTemplate('templates/student_profile/learner_profile');
});
it("shows loading error correctly", function() {
var learnerProfileView = createLearnerProfileView(false, 'all_users');
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
learnerProfileView.render();
learnerProfileView.showLoadingError();
Helpers.expectLoadingErrorIsVisible(learnerProfileView, true);
});
it("renders all fields as expected for self with full access", function() {
var learnerProfileView = createLearnerProfileView(true, 'all_users', true);
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
learnerProfileView.render();
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView);
});
it("renders all fields as expected for self with limited access", function() {
var learnerProfileView = createLearnerProfileView(true, 'private', false);
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
learnerProfileView.render();
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
});
it("renders the fields as expected for others with full access", function() {
var learnerProfileView = createLearnerProfileView(false, 'all_users', true);
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
learnerProfileView.render();
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView, true)
});
it("renders the fields as expected for others with limited access", function() {
var learnerProfileView = createLearnerProfileView(false, 'private', false);
Helpers.expectLoadingIndicatorIsVisible(learnerProfileView, true);
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
learnerProfileView.render();
Helpers.expectLoadingErrorIsVisible(learnerProfileView, false);
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView, true);
});
});
});
...@@ -27,7 +27,8 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -27,7 +27,8 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
model: fieldData.model || new UserAccountModel({}), model: fieldData.model || new UserAccountModel({}),
title: fieldData.title || 'Field Title', title: fieldData.title || 'Field Title',
valueAttribute: fieldData.valueAttribute, valueAttribute: fieldData.valueAttribute,
helpMessage: fieldData.helpMessage || 'I am a field message' helpMessage: fieldData.helpMessage || 'I am a field message',
placeholderValue: fieldData.placeholderValue || 'I am a placeholder message'
}; };
switch (fieldType) { switch (fieldType) {
...@@ -58,8 +59,12 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -58,8 +59,12 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
} }
}; };
var expectTitleAndMessageToBe = function(view, expectedTitle, expectedMessage) { var expectTitleToBe = function(view, expectedTitle) {
expect(view.$('.u-field-title').text().trim()).toBe(expectedTitle); expect(view.$('.u-field-title').text().trim()).toBe(expectedTitle);
};
var expectTitleAndMessageToBe = function(view, expectedTitle, expectedMessage) {
expectTitleToBe(view, expectedTitle);
expect(view.$('.u-field-message').text().trim()).toBe(expectedMessage); expect(view.$('.u-field-message').text().trim()).toBe(expectedMessage);
}; };
...@@ -125,9 +130,19 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -125,9 +130,19 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
var request_data = {}; var request_data = {};
var url = view.model.url; var url = view.model.url;
expectTitleAndMessageToBe(view, data.title, data.helpMessage); if (data.editable === 'toggle') {
expect(view.el).toHaveClass('mode-placeholder');
expectTitleToBe(view, data.title);
expectMessageContains(view, view.indicators['canEdit']);
view.$el.click();
} else {
expectTitleAndMessageToBe(view, data.title, data.helpMessage);
}
expect(view.el).toHaveClass('mode-edit');
expect(view.fieldValue()).not.toBe(data.validValue);
view.$(data.valueElementSelector).val(data.validValue).change(); view.$(data.valueInputSelector).val(data.validValue).change();
// When the value in the field is changed // When the value in the field is changed
expect(view.fieldValue()).toBe(data.validValue); expect(view.fieldValue()).toBe(data.validValue);
expectMessageContains(view, view.indicators['inProgress']); expectMessageContains(view, view.indicators['inProgress']);
...@@ -139,9 +154,14 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -139,9 +154,14 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
AjaxHelpers.respondWithNoContent(requests); AjaxHelpers.respondWithNoContent(requests);
// When server returns success. // When server returns success.
expectMessageContains(view, view.indicators['success']); if (data.editable === 'toggle') {
expect(view.el).toHaveClass('mode-display');
view.$el.click();
} else {
expectMessageContains(view, view.indicators['success']);
}
view.$(data.valueElementSelector).val(data.invalidValue1).change(); view.$(data.valueInputSelector).val(data.invalidValue1).change();
request_data[data.valueAttribute] = data.invalidValue1; request_data[data.valueAttribute] = data.invalidValue1;
AjaxHelpers.expectJsonRequest( AjaxHelpers.expectJsonRequest(
requests, 'PATCH', url, request_data requests, 'PATCH', url, request_data
...@@ -150,8 +170,9 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -150,8 +170,9 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
// When server returns a 500 error // When server returns a 500 error
expectMessageContains(view, view.indicators['error']); expectMessageContains(view, view.indicators['error']);
expectMessageContains(view, view.messages['error']); expectMessageContains(view, view.messages['error']);
expect(view.el).toHaveClass('mode-edit');
view.$(data.valueElementSelector).val(data.invalidValue2).change(); view.$(data.valueInputSelector).val(data.invalidValue2).change();
request_data[data.valueAttribute] = data.invalidValue2; request_data[data.valueAttribute] = data.invalidValue2;
AjaxHelpers.expectJsonRequest( AjaxHelpers.expectJsonRequest(
requests, 'PATCH', url, request_data requests, 'PATCH', url, request_data
...@@ -160,12 +181,29 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -160,12 +181,29 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
// When server returns a validation error // When server returns a validation error
expectMessageContains(view, view.indicators['validationError']); expectMessageContains(view, view.indicators['validationError']);
expectMessageContains(view, data.validationError); expectMessageContains(view, data.validationError);
expect(view.el).toHaveClass('mode-edit');
view.$(data.valueInputSelector).val('').change();
// When the value in the field is changed
expect(view.fieldValue()).toBe('');
request_data[data.valueAttribute] = '';
AjaxHelpers.expectJsonRequest(
requests, 'PATCH', url, request_data
);
AjaxHelpers.respondWithNoContent(requests);
// When server returns success.
if (data.editable === 'toggle') {
expect(view.el).toHaveClass('mode-placeholder');
} else {
expect(view.el).toHaveClass('mode-edit');
}
}; };
var verifyTextField = function (view, data, requests) { var verifyTextField = function (view, data, requests) {
var selector = '.u-field-value > input'; var selector = '.u-field-value > input';
verifyEditableField(view, _.extend({ verifyEditableField(view, _.extend({
valueElementSelector: selector, valueSelector: '.u-field-value',
valueInputSelector: '.u-field-value > input'
}, data }, data
), requests); ), requests);
} }
...@@ -173,7 +211,8 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -173,7 +211,8 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
var verifyDropDownField = function (view, data, requests) { var verifyDropDownField = function (view, data, requests) {
var selector = '.u-field-value > select'; var selector = '.u-field-value > select';
verifyEditableField(view, _.extend({ verifyEditableField(view, _.extend({
valueElementSelector: selector, valueSelector: '.u-field-value',
valueInputSelector: '.u-field-value > select'
}, data }, data
), requests); ), requests);
} }
...@@ -183,6 +222,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -183,6 +222,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
UserAccountModel: UserAccountModel, UserAccountModel: UserAccountModel,
createFieldData: createFieldData, createFieldData: createFieldData,
createErrorMessage: createErrorMessage, createErrorMessage: createErrorMessage,
expectTitleToBe: expectTitleToBe,
expectTitleAndMessageToBe: expectTitleAndMessageToBe, expectTitleAndMessageToBe: expectTitleAndMessageToBe,
expectMessageContains: expectMessageContains, expectMessageContains: expectMessageContains,
expectAjaxRequestWithData: expectAjaxRequestWithData, expectAjaxRequestWithData: expectAjaxRequestWithData,
......
...@@ -7,7 +7,8 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -7,7 +7,8 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
var USERNAME = 'Legolas', var USERNAME = 'Legolas',
FULLNAME = 'Legolas Thranduil', FULLNAME = 'Legolas Thranduil',
EMAIL = 'legolas@woodland.middlearth'; EMAIL = 'legolas@woodland.middlearth',
BIO = "My Name is Theon Greyjoy. I'm member of House Greyjoy";
describe("edx.FieldViews", function () { describe("edx.FieldViews", function () {
...@@ -19,6 +20,8 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -19,6 +20,8 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
FieldViews.TextFieldView, FieldViews.TextFieldView,
FieldViews.DropdownFieldView, FieldViews.DropdownFieldView,
FieldViews.LinkFieldView, FieldViews.LinkFieldView,
FieldViews.TextareaFieldView
]; ];
beforeEach(function () { beforeEach(function () {
...@@ -26,6 +29,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -26,6 +29,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
TemplateHelpers.installTemplate('templates/fields/field_dropdown'); TemplateHelpers.installTemplate('templates/fields/field_dropdown');
TemplateHelpers.installTemplate('templates/fields/field_link'); TemplateHelpers.installTemplate('templates/fields/field_link');
TemplateHelpers.installTemplate('templates/fields/field_text'); TemplateHelpers.installTemplate('templates/fields/field_text');
TemplateHelpers.installTemplate('templates/fields/field_textarea');
timerCallback = jasmine.createSpy('timerCallback'); timerCallback = jasmine.createSpy('timerCallback');
jasmine.Clock.useMock(); jasmine.Clock.useMock();
...@@ -55,7 +59,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -55,7 +59,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
title: 'Username', title: 'Username',
valueAttribute: 'username', valueAttribute: 'username',
helpMessage: 'The username that you use to sign in to edX.' helpMessage: 'The username that you use to sign in to edX.'
}) });
var view = new fieldViewClass(fieldData).render(); var view = new fieldViewClass(fieldData).render();
FieldViewsSpecHelpers.verifySuccessMessageReset(view, fieldData, timerCallback); FieldViewsSpecHelpers.verifySuccessMessageReset(view, fieldData, timerCallback);
...@@ -66,7 +70,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -66,7 +70,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
requests = AjaxHelpers.requests(this); requests = AjaxHelpers.requests(this);
var fieldViewClass = FieldViews.FieldView; var fieldViewClass = FieldViews.EditableFieldView;
var fieldData = FieldViewsSpecHelpers.createFieldData(fieldViewClass, { var fieldData = FieldViewsSpecHelpers.createFieldData(fieldViewClass, {
title: 'Preferred Language', title: 'Preferred Language',
valueAttribute: 'language', valueAttribute: 'language',
...@@ -101,7 +105,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -101,7 +105,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
expect(view.$('.u-field-value input').val().trim()).toBe('bookworm'); expect(view.$('.u-field-value input').val().trim()).toBe('bookworm');
}); });
it("correctly renders, updates and persists changes to TextFieldView", function() { it("correctly renders, updates and persists changes to TextFieldView when editable == always", function() {
requests = AjaxHelpers.requests(this); requests = AjaxHelpers.requests(this);
...@@ -123,7 +127,28 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -123,7 +127,28 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
}, requests); }, requests);
}); });
it("correctly renders, updates and persists changes to DropdownFieldView", function() { it("correctly renders and updates DropdownFieldView when editable == never", function() {
requests = AjaxHelpers.requests(this);
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.DropdownFieldView, {
title: 'Full Name',
valueAttribute: 'name',
helpMessage: 'edX full name',
editable: 'never'
});
var view = new FieldViews.DropdownFieldView(fieldData).render();
FieldViewsSpecHelpers.expectTitleAndMessageToBe(view, fieldData.title, fieldData.helpMessage);
expect(view.el).toHaveClass('mode-hidden');
view.model.set({'name': fieldData.options[1][0]});
expect(view.el).toHaveClass('mode-display');
view.$el.click();
expect(view.el).toHaveClass('mode-display');
});
it("correctly renders, updates and persists changes to DropdownFieldView when editable == always", function() {
requests = AjaxHelpers.requests(this); requests = AjaxHelpers.requests(this);
...@@ -145,6 +170,93 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -145,6 +170,93 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
}, requests); }, requests);
}); });
it("correctly renders, updates and persists changes to DropdownFieldView when editable == toggle", function() {
requests = AjaxHelpers.requests(this);
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.DropdownFieldView, {
title: 'Full Name',
valueAttribute: 'name',
helpMessage: 'edX full name',
editable: 'toggle'
});
var view = new FieldViews.DropdownFieldView(fieldData).render();
FieldViewsSpecHelpers.verifyDropDownField(view, {
title: fieldData.title,
valueAttribute: fieldData.valueAttribute,
helpMessage: fieldData.helpMessage,
editable: 'toggle',
validValue: FieldViewsSpecHelpers.SELECT_OPTIONS[0][0],
invalidValue1: FieldViewsSpecHelpers.SELECT_OPTIONS[1][0],
invalidValue2: FieldViewsSpecHelpers.SELECT_OPTIONS[2][0],
validationError: "Nope, this will not do!"
}, requests);
});
it("correctly renders and updates TextAreaFieldView when editable == never", function() {
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.TextareaFieldView, {
title: 'About me',
valueAttribute: 'bio',
helpMessage: 'Wicked is good',
placeholderValue: "Tell other edX learners a little about yourself: where you live, what your interests are, why you’re taking courses on edX, or what you hope to learn.",
editable: 'never'
});
// set bio to empty to see the placeholder.
fieldData.model.set({bio: ''});
var view = new FieldViews.TextareaFieldView(fieldData).render();
FieldViewsSpecHelpers.expectTitleAndMessageToBe(view, fieldData.title, fieldData.helpMessage);
expect(view.el).toHaveClass('mode-hidden');
expect(view.$('.u-field-value').text()).toBe(fieldData.placeholderValue);
var bio = 'Too much to tell!'
view.model.set({'bio': bio});
expect(view.el).toHaveClass('mode-display');
expect(view.$('.u-field-value').text()).toBe(bio);
view.$el.click();
expect(view.el).toHaveClass('mode-display');
});
it("correctly renders, updates and persists changes to TextAreaFieldView when editable == toggle", function() {
requests = AjaxHelpers.requests(this);
var valueInputSelector = '.u-field-value > textarea'
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.TextareaFieldView, {
title: 'About me',
valueAttribute: 'bio',
helpMessage: 'Wicked is good',
placeholderValue: "Tell other edX learners a little about yourself: where you live, what your interests are, why you’re taking courses on edX, or what you hope to learn.",
editable: 'toggle'
});
fieldData.model.set({'bio': ''});
var view = new FieldViews.TextareaFieldView(fieldData).render();
FieldViewsSpecHelpers.expectTitleToBe(view, fieldData.title);
FieldViewsSpecHelpers.expectMessageContains(view, view.indicators['canEdit']);
expect(view.el).toHaveClass('mode-placeholder');
expect(view.$('.u-field-value').text()).toBe(fieldData.placeholderValue);
view.$('.wrapper-u-field').click();
expect(view.el).toHaveClass('mode-edit');
view.$(valueInputSelector).val(BIO).focusout();
expect(view.fieldValue()).toBe(BIO);
AjaxHelpers.expectJsonRequest(
requests, 'PATCH', view.model.url, {'bio': BIO}
);
AjaxHelpers.respondWithNoContent(requests);
expect(view.el).toHaveClass('mode-display');
view.$('.wrapper-u-field').click();
view.$(valueInputSelector).val('').focusout();
AjaxHelpers.respondWithNoContent(requests);
expect(view.el).toHaveClass('mode-placeholder');
expect(view.$('.u-field-value').text()).toBe(fieldData.placeholderValue);
});
it("correctly renders LinkFieldView", function() { it("correctly renders LinkFieldView", function() {
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.LinkFieldView, { var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.LinkFieldView, {
title: 'Title', title: 'Title',
...@@ -157,5 +269,4 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -157,5 +269,4 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
expect(view.$('.u-field-value > a').text().trim()).toBe(fieldData.linkTitle); expect(view.$('.u-field-value > a').text().trim()).toBe(fieldData.linkTitle);
}); });
}); });
}); });
...@@ -19,7 +19,24 @@ ...@@ -19,7 +19,24 @@
level_of_education: null, level_of_education: null,
mailing_address: "", mailing_address: "",
year_of_birth: null, year_of_birth: null,
language_proficiencies: [] bio: null,
language_proficiencies: [],
requires_parental_consent: true,
default_public_account_fields: []
},
parse : function(response, xhr) {
if (_.isNull(response)) {
return {};
}
// Currently when a non-staff user A access user B's profile, the only way to tell whether user B's
// profile is public is to check if the api has returned fields other than the default public fields
// specified in settings.ACCOUNT_VISIBILITY_CONFIGURATION.
var profileIsPublic = _.size(_.difference(_.keys(response), this.get('default_public_account_fields'))) > 0;
this.set({'profile_is_public': profileIsPublic}, { silent: true });
return response;
} }
}); });
......
;(function (define, undefined) {
'use strict';
define([
'gettext', 'jquery', 'underscore', 'backbone',
'js/student_account/models/user_account_model',
'js/student_account/models/user_preferences_model',
'js/views/fields',
'js/student_profile/views/learner_profile_fields',
'js/student_profile/views/learner_profile_view',
'js/student_account/views/account_settings_fields'
], function (gettext, $, _, Backbone, AccountSettingsModel, AccountPreferencesModel, FieldsView,
LearnerProfileFieldsView, LearnerProfileView, AccountSettingsFieldViews) {
return function (options) {
var learnerProfileElement = $('.wrapper-profile');
var accountPreferencesModel = new AccountPreferencesModel();
accountPreferencesModel.url = options['preferences_api_url'];
var accountSettingsModel = new AccountSettingsModel({
'default_public_account_fields': options['default_public_account_fields']
});
accountSettingsModel.url = options['accounts_api_url'];
var editable = options['own_profile'] ? 'toggle' : 'never';
var accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({
model: accountPreferencesModel,
required: true,
editable: 'always',
showMessages: false,
title: gettext('edX learners can see my:'),
valueAttribute: "account_privacy",
options: [
['private', gettext('Limited Profile')],
['all_users', gettext('Full Profile')]
],
helpMessage: '',
accountSettingsPageUrl: options['account_settings_page_url']
});
var usernameFieldView = new FieldsView.ReadonlyFieldView({
model: accountSettingsModel,
valueAttribute: "username",
helpMessage: ""
});
var sectionOneFieldViews = [
usernameFieldView,
new FieldsView.DropdownFieldView({
model: accountSettingsModel,
required: true,
editable: editable,
showMessages: false,
iconName: 'fa-map-marker',
placeholderValue: gettext('Add country'),
valueAttribute: "country",
options: options['country_options'],
helpMessage: ''
}),
new AccountSettingsFieldViews.LanguageProficienciesFieldView({
model: accountSettingsModel,
required: false,
editable: editable,
showMessages: false,
iconName: 'fa-comment',
placeholderValue: gettext('Add language'),
valueAttribute: "language_proficiencies",
options: options['language_options'],
helpMessage: ''
})
];
var sectionTwoFieldViews = [
new FieldsView.TextareaFieldView({
model: accountSettingsModel,
editable: editable,
showMessages: false,
title: gettext('About me'),
placeholderValue: gettext("Tell other edX learners a little about yourself: where you live, what your interests are, why you're taking courses on edX, or what you hope to learn."),
valueAttribute: "bio",
helpMessage: ''
})
];
var learnerProfileView = new LearnerProfileView({
el: learnerProfileElement,
own_profile: options['own_profile'],
has_preferences_access: options['has_preferences_access'],
accountSettingsModel: accountSettingsModel,
preferencesModel: accountPreferencesModel,
accountPrivacyFieldView: accountPrivacyFieldView,
usernameFieldView: usernameFieldView,
sectionOneFieldViews: sectionOneFieldViews,
sectionTwoFieldViews: sectionTwoFieldViews
});
var showLoadingError = function () {
learnerProfileView.showLoadingError();
};
var renderLearnerProfileView = function() {
learnerProfileView.render();
};
accountSettingsModel.fetch({
success: function () {
if (options['has_preferences_access']) {
accountPreferencesModel.fetch({
success: renderLearnerProfileView,
error: showLoadingError
});
}
else {
renderLearnerProfileView();
}
},
error: showLoadingError
});
return {
accountSettingsModel: accountSettingsModel,
accountPreferencesModel: accountPreferencesModel,
learnerProfileView: learnerProfileView
};
};
})
}).call(this, define || RequireJS.define);
;(function (define, undefined) {
'use strict';
define([
'gettext', 'jquery', 'underscore', 'backbone', 'js/views/fields', 'backbone-super'
], function (gettext, $, _, Backbone, FieldViews) {
var LearnerProfileFieldViews = {};
LearnerProfileFieldViews.AccountPrivacyFieldView = FieldViews.DropdownFieldView.extend({
render: function () {
this._super();
this.message();
return this;
},
message: function () {
if (this.profileIsPrivate) {
this._super(interpolate_text(
gettext("You must specify your birth year before you can share your full profile. To specify your birth year, go to the {account_settings_page_link}"),
{'account_settings_page_link': '<a href="' + this.options.accountSettingsPageUrl + '">' + gettext('Account Settings page.') + '</a>'}
));
} else if (this.requiresParentalConsent) {
this._super(interpolate_text(
gettext('You must be over 13 to share a full profile. If you are over 13, make sure that you have specified a birth year on the {account_settings_page_link}'),
{'account_settings_page_link': '<a href="' + this.options.accountSettingsPageUrl + '">' + gettext('Account Settings page.') + '</a>'}
));
}
else {
this._super('');
}
return this._super();
}
});
return LearnerProfileFieldViews;
})
}).call(this, define || RequireJS.define);
;(function (define, undefined) {
'use strict';
define([
'gettext', 'jquery', 'underscore', 'backbone'
], function (gettext, $, _, Backbone) {
var LearnerProfileView = Backbone.View.extend({
initialize: function (options) {
this.template = _.template($('#learner_profile-tpl').text());
_.bindAll(this, 'showFullProfile', 'render', 'renderFields', 'showLoadingError');
this.listenTo(this.options.preferencesModel, "change:" + 'account_privacy', this.render);
},
showFullProfile: function () {
if (this.options.own_profile) {
return this.options.preferencesModel.get('account_privacy') === 'all_users';
} else {
return this.options.accountSettingsModel.get('profile_is_public');
}
},
render: function () {
this.$el.html(this.template({
username: this.options.accountSettingsModel.get('username'),
profilePhoto: 'http://www.teachthought.com/wp-content/uploads/2012/07/edX-120x120.jpg',
ownProfile: this.options.own_profile,
showFullProfile: this.showFullProfile()
}));
this.renderFields();
return this;
},
renderFields: function() {
var view = this;
if (this.options.own_profile) {
var fieldView = this.options.accountPrivacyFieldView;
fieldView.profileIsPrivate = (!this.options.accountSettingsModel.get('year_of_birth'));
fieldView.requiresParentalConsent = (this.options.accountSettingsModel.get('requires_parental_consent'));
fieldView.undelegateEvents();
this.$('.wrapper-profile-field-account-privacy').append(fieldView.render().el);
fieldView.delegateEvents();
}
this.$('.profile-section-one-fields').append(this.options.usernameFieldView.render().el);
if (this.showFullProfile()) {
_.each(this.options.sectionOneFieldViews, function (fieldView, index) {
fieldView.undelegateEvents();
view.$('.profile-section-one-fields').append(fieldView.render().el);
fieldView.delegateEvents();
});
_.each(this.options.sectionTwoFieldViews, function (fieldView, index) {
fieldView.undelegateEvents();
view.$('.profile-section-two-fields').append(fieldView.render().el);
fieldView.delegateEvents();
});
}
},
showLoadingError: function () {
this.$('.ui-loading-indicator').addClass('is-hidden');
this.$('.ui-loading-error').removeClass('is-hidden');
}
});
return LearnerProfileView;
})
}).call(this, define || RequireJS.define);
...@@ -46,6 +46,7 @@ ...@@ -46,6 +46,7 @@
// base - specific views // base - specific views
@import "views/account-settings"; @import "views/account-settings";
@import "views/learner-profile";
@import 'views/login-register'; @import 'views/login-register';
@import 'views/verification'; @import 'views/verification';
@import 'views/decoupled-verification'; @import 'views/decoupled-verification';
......
...@@ -5,6 +5,49 @@ ...@@ -5,6 +5,49 @@
.u-field { .u-field {
padding: $baseline 0; padding: $baseline 0;
border-bottom: 1px solid $gray-l5; border-bottom: 1px solid $gray-l5;
border: 1px dashed transparent;
&.mode-placeholder {
border: 2px dashed transparent;
border-radius: 3px;
span {
color: $gray-l1;
}
&:hover {
border: 2px dashed $link-color;
span {
color: $link-color;
}
}
}
&.editable-toggle.mode-display:hover {
background-color: $m-blue-l4;
border-radius: 3px;
.message-can-edit {
display: inline-block;
color: $link-color;
}
}
&.mode-hidden {
display: none;
}
i {
color: $gray-l2;
vertical-align:text-bottom;
margin-right: 5px;
}
.message-can-edit {
display: none;
}
.message-error { .message-error {
color: $alert-color; color: $alert-color;
...@@ -33,10 +76,15 @@ ...@@ -33,10 +76,15 @@
} }
} }
.u-field-icon {
width: $baseline;
color: $gray-l2;
}
.u-field-title { .u-field-title {
width: flex-grid(3, 12); width: flex-grid(3, 12);
display: inline-block; display: inline-block;
color: $dark-gray1; color: $gray;
vertical-align: top; vertical-align: top;
margin-bottom: 0; margin-bottom: 0;
...@@ -56,12 +104,12 @@ ...@@ -56,12 +104,12 @@
} }
.u-field-message { .u-field-message {
@extend small; @extend %t-copy-sub1;
@include padding-left($baseline/2); @include padding-left($baseline/2);
width: flex-grid(6, 12); width: flex-grid(6, 12);
display: inline-block; display: inline-block;
vertical-align: top; vertical-align: top;
color: $dark-gray1; color: $gray-l1;
i { i {
@include margin-right($baseline/4); @include margin-right($baseline/4);
......
// lms - application - learner profile
// ====================
// Table of Contents
// * +Container - Learner Profile
// * +Main - Header
// * +Settings Section
.view-profile {
$profile-photo-dimension: 120px;
.content-wrapper {
background-color: $white;
}
.ui-loading-indicator {
@extend .ui-loading-base;
padding-bottom: $baseline;
// center horizontally
@include margin-left(auto);
@include margin-right(auto);
width: ($baseline*5);
}
.wrapper-profile {
min-height: 200px;
.ui-loading-indicator {
margin-top: 100px;
}
}
.profile-self {
.wrapper-profile-field-account-privacy {
@include clearfix();
@include box-sizing(border-box);
margin: 0 auto 0;
padding: ($baseline*0.75) 0;
width: 100%;
background-color: $gray-l3;
.u-field-account_privacy {
@extend .container;
border: none;
box-shadow: none;
padding: 0 ($baseline*1.5);
}
.u-field-title {
width: auto;
color: $base-font-color;
font-weight: $font-bold;
cursor: text;
}
.u-field-value {
width: auto;
@include margin-left($baseline/2);
}
.u-field-message {
@include float(left);
width: 100%;
padding: 0;
color: $base-font-color;
}
}
}
.wrapper-profile-sections {
@extend .container;
padding: 0 ($baseline*1.5);
}
.wrapper-profile-section-one {
width: 100%;
display: inline-block;
margin-top: ($baseline*1.5);
.profile-photo {
@include float(left);
height: $profile-photo-dimension;
width: $profile-photo-dimension;
display: inline-block;
vertical-align: top;
}
}
.profile-section-one-fields {
float: left;
width: flex-grid(4, 12);
@include margin-left($baseline*1.5);
.u-field {
margin-bottom: ($baseline/4);
padding-top: 0;
padding-bottom: 0;
@include padding-left(3px);
}
.u-field-username {
margin-bottom: ($baseline/2);
input[type="text"] {
font-weight: 600;
}
.u-field-value {
width: 350px;
@extend %t-title4;
}
}
.u-field-title {
width: 0;
}
.u-field-value {
width: 200px;
}
select {
width: 100%
}
.u-field-message {
@include float(right);
width: 20px;
margin-top: 2px;
}
}
.wrapper-profile-section-two {
width: flex-grid(8, 12);
margin-top: ($baseline*1.5);
}
.profile-section-two-fields {
.u-field-textarea {
margin-bottom: ($baseline/2);
padding: ($baseline/4) ($baseline/2) ($baseline/2);
}
.u-field-title {
font-size: 1.1em;
@extend %t-weight4;
margin-bottom: ($baseline/4);
}
.u-field-value {
width: 100%;
white-space: pre-line;
line-height: 1.5em;
textarea {
width: 100%;
background-color: transparent;
}
}
.u-field-message {
@include float(right);
width: auto;
padding-top: ($baseline/4);
}
.u-field.mode-placeholder {
padding: $baseline;
border: 2px dashed $gray-l3;
i {
font-size: 12px;
padding-right: 5px;
vertical-align: middle;
color: $gray;
}
.u-field-title {
width: 100%;
text-align: center;
}
.u-field-value {
text-align: center;
line-height: 1.5em;
@extend %t-copy-sub1;
color: $gray;
}
}
.u-field.mode-placeholder:hover {
border: 2px dashed $link-color;
.u-field-title,
i {
color: $link-color;
}
}
}
}
<label class="u-field-title" for="u-field-select-<%- id %>"> <% if (title) { %>
<%- gettext(title) %> <label class="u-field-title" for="u-field-select-<%- id %>">
</label> <%- gettext(title) %>
</label>
<% } %>
<% if (iconName) { %>
<i class="u-field-icon icon fa <%- iconName %> fa-fw" area-hidden="true" ></i>
<% } %>
<span class="u-field-value"> <span class="u-field-value">
<select name="select" id="u-field-select-<%- id %>" aria-describedby="u-field-message-<%- id %>"> <% if (mode === 'edit') { %>
<% if (!required) { %> <select name="select" id="u-field-select-<%- id %>" aria-describedby="u-field-message-<%- id %>">
<option value=""></option> <% if (!required) { %>
<% } %> <option value=""></option>
<% _.each(selectOptions, function(selectOption) { %> <% } %>
<option value="<%- selectOption[0] %>"><%- selectOption[1] %></option> <% _.each(selectOptions, function(selectOption) { %>
<% }); %> <option value="<%- selectOption[0] %>"><%- selectOption[1] %></option>
</select> <% }); %>
</select>
<% } %>
</span> </span>
<span class="u-field-message" id="u-field-message-<%- id %>"> <span class="u-field-message" id="u-field-message-<%- id %>">
<%- gettext(message) %> <%- gettext(message) %>
</span> </span>
<div class="wrapper-u-field">
<div class="u-field-header">
<label class="u-field-title" for="u-field-textarea-<%- id %>" aria-describedby="u-field-message-<%- id %>"></label>
<span class="u-field-message" id="u-field-message-<%- id %>"><%- message %></span>
</div>
<div class="u-field-value"><%
if (mode === 'edit') {
%><textarea id="u-field-textarea-<%- id %>" rows="4"><%- value %></textarea><%
} else {
%><%- value %><%
}
%></div>
</div>
...@@ -83,8 +83,9 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -83,8 +83,9 @@ site_status_msg = get_site_status_msg(course_id)
<ul class="dropdown-menu" aria-label="More Options" role="menu"> <ul class="dropdown-menu" aria-label="More Options" role="menu">
<%block name="navigation_dropdown_menu_links" > <%block name="navigation_dropdown_menu_links" >
<li><a href="${reverse('account_settings')}">${_("Account Settings")}</a></li> <li><a href="${reverse('account_settings')}">${_("Account Settings")}</a></li>
<li><a href="${reverse('learner_profile', kwargs={'username': user.username})}">${_("My Profile")}</a></li>
</%block> </%block>
<li><a href="${reverse('logout')}" role="menuitem">${_("Sign out")}</a></li> <li><a href="${reverse('logout')}" role="menuitem">${_("Sign Out")}</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>
......
...@@ -91,8 +91,9 @@ site_status_msg = get_site_status_msg(course_id) ...@@ -91,8 +91,9 @@ site_status_msg = get_site_status_msg(course_id)
<ul class="dropdown-menu" aria-label="More Options" role="menu"> <ul class="dropdown-menu" aria-label="More Options" role="menu">
<%block name="navigation_dropdown_menu_links" > <%block name="navigation_dropdown_menu_links" >
<li><a href="${reverse('account_settings')}">${_("Account Settings")}</a></li> <li><a href="${reverse('account_settings')}">${_("Account Settings")}</a></li>
<li><a href="${reverse('learner_profile', kwargs={'username': user.username})}">${_("My Profile")}</a></li>
</%block> </%block>
<li><a href="${reverse('logout')}" role="menuitem">${_("Sign out")}</a></li> <li><a href="${reverse('logout')}" role="menuitem">${_("Sign Out")}</a></li>
</ul> </ul>
</li> </li>
</ol> </ol>
......
<%! import json %>
<%! from django.core.urlresolvers import reverse %>
<%! from django.utils.translation import ugettext as _ %>
<%inherit file="/main.html" />
<%namespace name='static' file='/static_content.html'/>
<%block name="pagetitle">${_("Learner Profile")}</%block>
<%block name="bodyclass">view-profile</%block>
<%block name="header_extras">
% for template_name in ["field_dropdown", "field_textarea", "field_readonly"]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="fields/${template_name}.underscore" />
</script>
% endfor
% for template_name in ["learner_profile",]:
<script type="text/template" id="${template_name}-tpl">
<%static:include path="student_profile/${template_name}.underscore" />
</script>
% endfor
</%block>
<div class="wrapper-profile">
<div class="ui-loading-indicator">
<p><span class="spin"><i class="icon fa fa-refresh"></i></span> <span class="copy">${_("Loading")}</span></p>
</div>
</div>
<%block name="headextra">
<%static:css group='style-course'/>
<script>
(function (require) {
require(['js/student_profile/views/learner_profile_factory'], function(setupLearnerProfile) {
var options = ${ json.dumps(data) };
setupLearnerProfile(options);
});
}).call(this, require || RequireJS.require);
</script>
</%block>
<div class="profile <%- ownProfile ? 'profile-self' : 'profile-other' %>">
<div class="wrapper-profile-field-account-privacy"></div>
<div class="wrapper-profile-sections account-settings-container">
<div class="wrapper-profile-section-one">
<div class="profile-photo">
<img src="<%- profilePhoto %>" alt="Profile image for <%- username %>">
</div>
<div class="profile-section-one-fields">
</div>
</div>
<div class="ui-loading-error is-hidden">
<i class="fa fa-exclamation-triangle message-error" aria-hidden=true></i>
<span class="copy"><%- gettext("An error occurred. Please reload the page.") %></span>
</div>
<div class="wrapper-profile-section-two">
<div class="profile-section-two-fields">
<% if (!showFullProfile) { %>
<% if(ownProfile) { %>
<span class="profile-private--message"><%- gettext("You are currently sharing a limited profile.") %></span>
<% } else { %>
<span class="profile-private--message"><%- gettext("This edX learner is currently sharing a limited profile.") %></span>
<% } %>
<% } %>
</div>
</div>
</div>
</div>
...@@ -417,9 +417,12 @@ if settings.COURSEWARE_ENABLED: ...@@ -417,9 +417,12 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/{}/lti_rest_endpoints/'.format(settings.COURSE_ID_PATTERN), url(r'^courses/{}/lti_rest_endpoints/'.format(settings.COURSE_ID_PATTERN),
'courseware.views.get_course_lti_endpoints', name='lti_rest_endpoints'), 'courseware.views.get_course_lti_endpoints', name='lti_rest_endpoints'),
# Student account and profile # Student account
url(r'^account/', include('student_account.urls')), url(r'^account/', include('student_account.urls')),
# Student profile
url(r'^u/(?P<username>[\w.@+-]+)$', 'student_profile.views.learner_profile', name='learner_profile'),
# Student Notes # Student Notes
url(r'^courses/{}/edxnotes'.format(settings.COURSE_ID_PATTERN), url(r'^courses/{}/edxnotes'.format(settings.COURSE_ID_PATTERN),
include('edxnotes.urls'), name="edxnotes_endpoints"), include('edxnotes.urls'), name="edxnotes_endpoints"),
......
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