Commit 537966ab by Tyler Hallada Committed by GitHub

Merge branch 'master' into EDUCATOR-926

parents 1b2fec21 679bd2c6
......@@ -1194,6 +1194,7 @@ def create_xblock_info(xblock, data=None, metadata=None, include_ancestor_info=F
elif xblock.category == 'sequential':
xblock_info.update({
'is_proctored_exam': xblock.is_proctored_exam,
'online_proctoring_rules': settings.PROCTORING_SETTINGS.get('LINK_URLS', {}).get('online_proctoring_rules', {}),
'is_practice_exam': xblock.is_practice_exam,
'is_time_limited': xblock.is_time_limited,
'exam_review_rules': xblock.exam_review_rules,
......
......@@ -36,6 +36,7 @@ from lms.envs.test import (
MEDIA_URL,
COMPREHENSIVE_THEME_DIRS,
JWT_AUTH,
REGISTRATION_EXTRA_FIELDS,
)
# mongo connection settings
......
......@@ -42,7 +42,18 @@
<textarea cols="50" maxlength="255" aria-describedby="review-rules-description"
class="review-rules input input-text" autocomplete="off" />
</label>
<p class='field-message' id='review-rules-description'><%- gettext('Specify any additional rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed.') %></p>
<% var online_proctoring_rules = xblockInfo.get('online_proctoring_rules'); %>
<p class='field-message' id='review-rules-description'>
<%= edx.HtmlUtils.interpolateHtml(
gettext('Specify any rules or rule exceptions that the proctoring review team should enforce when reviewing the videos. For example, you could specify that calculators are allowed. These specified rules are visible to learners before the learners start the exam, along with the {linkStart}general proctored exam rules{linkEnd}.'),
{
linkStart: edx.HtmlUtils.interpolateHtml(
edx.HtmlUtils.HTML('<a href="{onlineProctoringUrl}" title="{onlineProctoringTitle}">'),
{ onlineProctoringUrl: online_proctoring_rules, onlineProctoringTitle: gettext('General Proctored Exam Rules')}),
linkEnd: edx.HtmlUtils.HTML('</a>')
})
%>
</p>
</div>
</div>
</div>
......
......@@ -7,6 +7,7 @@ import requests
from django.contrib.sites.models import Site
from django.http import Http404
from django.utils.functional import cached_property
from django_countries import countries
from social_core.backends.saml import OID_EDU_PERSON_ENTITLEMENT, SAMLAuth, SAMLIdentityProvider
from social_core.exceptions import AuthForbidden
......@@ -134,6 +135,77 @@ class SapSuccessFactorsIdentityProvider(EdXSAMLIdentityProvider):
'odata_client_id',
)
# Define the relationships between SAPSF record fields and Open edX logistration fields.
default_field_mapping = {
'username': 'username',
'firstName': 'first_name',
'lastName': 'last_name',
'defaultFullName': 'fullname',
'email': 'email',
'country': 'country',
'city': 'city',
}
# Define a simple mapping to relate SAPSF values to Open edX-compatible values for
# any given field. By default, this only contains the Country field, as SAPSF supplies
# a country name, which has to be translated to a country code.
default_value_mapping = {
'country': {name: code for code, name in countries}
}
# Unfortunately, not everything has a 1:1 name mapping between Open edX and SAPSF, so
# we need some overrides. TODO: Fill in necessary mappings
default_value_mapping.update({
'United States': 'US',
})
def get_registration_fields(self, response):
"""
Get a dictionary mapping registration field names to default values.
"""
field_mapping = self.field_mappings
registration_fields = {edx_name: response['d'].get(odata_name, '') for odata_name, edx_name in field_mapping.items()}
value_mapping = self.value_mappings
for field, value in registration_fields.items():
if field in value_mapping and value in value_mapping[field]:
registration_fields[field] = value_mapping[field][value]
return registration_fields
@property
def field_mappings(self):
"""
Get a dictionary mapping the field names returned in an SAP SuccessFactors
user entity to the field names with which those values should be used in
the Open edX registration form.
"""
overrides = self.conf.get('sapsf_field_mappings', {})
base = self.default_field_mapping.copy()
base.update(overrides)
return base
@property
def value_mappings(self):
"""
Get a dictionary mapping of field names to override objects which each
map values received from SAP SuccessFactors to values expected in the
Open edX platform registration form.
"""
overrides = self.conf.get('sapsf_value_mappings', {})
base = self.default_value_mapping.copy()
for field, override in overrides.items():
if field in base:
base[field].update(override)
else:
base[field] = override[field]
return base
@property
def timeout(self):
"""
The number of seconds OData API requests should wait for a response before failing.
"""
return self.conf.get('odata_api_request_timeout', 10)
@property
def sapsf_idp_url(self):
return self.conf['sapsf_oauth_root_url'] + 'idp'
......@@ -187,7 +259,7 @@ class SapSuccessFactorsIdentityProvider(EdXSAMLIdentityProvider):
'token_url': self.sapsf_token_url,
'private_key': self.sapsf_private_key,
},
timeout=10,
timeout=self.timeout,
)
assertion.raise_for_status()
assertion = assertion.text
......@@ -199,7 +271,7 @@ class SapSuccessFactorsIdentityProvider(EdXSAMLIdentityProvider):
'grant_type': 'urn:ietf:params:oauth:grant-type:saml2-bearer',
'assertion': assertion,
},
timeout=10,
timeout=self.timeout,
)
token.raise_for_status()
token = token.json()['access_token']
......@@ -220,12 +292,14 @@ class SapSuccessFactorsIdentityProvider(EdXSAMLIdentityProvider):
username = details['username']
try:
client = self.get_odata_api_client(user_id=username)
fields = ','.join(self.field_mappings)
response = client.get(
'{root_url}User(userId=\'{user_id}\')?$select=username,firstName,lastName,defaultFullName,email'.format(
'{root_url}User(userId=\'{user_id}\')?$select={fields}'.format(
root_url=self.odata_api_root_url,
user_id=username
user_id=username,
fields=fields,
),
timeout=10,
timeout=self.timeout,
)
response.raise_for_status()
response = response.json()
......@@ -237,13 +311,7 @@ class SapSuccessFactorsIdentityProvider(EdXSAMLIdentityProvider):
self.odata_company_id,
)
return details
return {
'username': response['d']['username'],
'first_name': response['d']['firstName'],
'last_name': response['d']['lastName'],
'fullname': response['d']['defaultFullName'],
'email': response['d']['email'],
}
return self.get_registration_fields(response)
def get_saml_idp_choices():
......
......@@ -54,7 +54,7 @@ class IntegrationTestMixin(object):
self.addCleanup(patcher.stop)
# Override this method in a subclass and enable at least one provider.
def test_register(self):
def test_register(self, **extra_defaults):
# The user goes to the register page, and sees a button to register with the provider:
provider_register_url = self._check_register_page()
# The user clicks on the Dummy button:
......@@ -76,6 +76,8 @@ class IntegrationTestMixin(object):
self.assertEqual(form_fields['email']['defaultValue'], self.USER_EMAIL)
self.assertEqual(form_fields['name']['defaultValue'], self.USER_NAME)
self.assertEqual(form_fields['username']['defaultValue'], self.USER_USERNAME)
for field_name, value in extra_defaults.items():
self.assertEqual(form_fields[field_name]['defaultValue'], value)
registration_values = {
'email': 'email-edited@tpa-test.none',
'name': 'My Customized Name',
......
......@@ -309,6 +309,7 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
'lastName': 'Smith',
'defaultFullName': 'John Smith',
'email': 'john@smith.com',
'country': 'Australia',
}
})
)
......@@ -331,23 +332,119 @@ class SuccessFactorsIntegrationTest(SamlIntegrationTestUtilities, IntegrationTes
self.USER_USERNAME = "myself"
super(SuccessFactorsIntegrationTest, self).test_register()
@patch.dict('django.conf.settings.REGISTRATION_EXTRA_FIELDS', country='optional')
def test_register_sapsf_metadata_present(self):
"""
Configure the provider such that it can talk to a mocked-out version of the SAP SuccessFactors
API, and ensure that the data it gets that way gets passed to the registration form.
Check that value mappings overrides work in cases where we override a value other than
what we're looking for, and when an empty override is provided (expected behavior is that
existing value maps will be left alone).
"""
expected_country = 'AU'
provider_settings = {
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
'sapsf_private_key': 'fake_private_key_here',
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
'odata_company_id': 'NCC1701D',
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
}
self._configure_testshib_provider(
identity_provider_type='sap_success_factors',
metadata_source=TESTSHIB_METADATA_URL,
other_settings=json.dumps({
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
'sapsf_private_key': 'fake_private_key_here',
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
'odata_company_id': 'NCC1701D',
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
})
other_settings=json.dumps(provider_settings)
)
super(SuccessFactorsIntegrationTest, self).test_register()
super(SuccessFactorsIntegrationTest, self).test_register(country=expected_country)
@patch.dict('django.conf.settings.REGISTRATION_EXTRA_FIELDS', country='optional')
def test_register_sapsf_metadata_present_override_relevant_value(self):
"""
Configure the provider such that it can talk to a mocked-out version of the SAP SuccessFactors
API, and ensure that the data it gets that way gets passed to the registration form.
Check that value mappings overrides work in cases where we override a value other than
what we're looking for, and when an empty override is provided (expected behavior is that
existing value maps will be left alone).
"""
value_map = {'country': {'Australia': 'NZ'}}
expected_country = 'NZ'
provider_settings = {
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
'sapsf_private_key': 'fake_private_key_here',
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
'odata_company_id': 'NCC1701D',
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
}
if value_map:
provider_settings['sapsf_value_mappings'] = value_map
self._configure_testshib_provider(
identity_provider_type='sap_success_factors',
metadata_source=TESTSHIB_METADATA_URL,
other_settings=json.dumps(provider_settings)
)
super(SuccessFactorsIntegrationTest, self).test_register(country=expected_country)
@patch.dict('django.conf.settings.REGISTRATION_EXTRA_FIELDS', country='optional')
def test_register_sapsf_metadata_present_override_other_value(self):
"""
Configure the provider such that it can talk to a mocked-out version of the SAP SuccessFactors
API, and ensure that the data it gets that way gets passed to the registration form.
Check that value mappings overrides work in cases where we override a value other than
what we're looking for, and when an empty override is provided (expected behavior is that
existing value maps will be left alone).
"""
value_map = {'country': {'United States': 'blahfake'}}
expected_country = 'AU'
provider_settings = {
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
'sapsf_private_key': 'fake_private_key_here',
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
'odata_company_id': 'NCC1701D',
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
}
if value_map:
provider_settings['sapsf_value_mappings'] = value_map
self._configure_testshib_provider(
identity_provider_type='sap_success_factors',
metadata_source=TESTSHIB_METADATA_URL,
other_settings=json.dumps(provider_settings)
)
super(SuccessFactorsIntegrationTest, self).test_register(country=expected_country)
@patch.dict('django.conf.settings.REGISTRATION_EXTRA_FIELDS', country='optional')
def test_register_sapsf_metadata_present_empty_value_override(self):
"""
Configure the provider such that it can talk to a mocked-out version of the SAP SuccessFactors
API, and ensure that the data it gets that way gets passed to the registration form.
Check that value mappings overrides work in cases where we override a value other than
what we're looking for, and when an empty override is provided (expected behavior is that
existing value maps will be left alone).
"""
value_map = {'country': {}}
expected_country = 'AU'
provider_settings = {
'sapsf_oauth_root_url': 'http://successfactors.com/oauth/',
'sapsf_private_key': 'fake_private_key_here',
'odata_api_root_url': 'http://api.successfactors.com/odata/v2/',
'odata_company_id': 'NCC1701D',
'odata_client_id': 'TatVotSEiCMteSNWtSOnLanCtBGwNhGB',
}
if value_map:
provider_settings['sapsf_value_mappings'] = value_map
self._configure_testshib_provider(
identity_provider_type='sap_success_factors',
metadata_source=TESTSHIB_METADATA_URL,
other_settings=json.dumps(provider_settings)
)
super(SuccessFactorsIntegrationTest, self).test_register(country=expected_country)
def test_register_http_failure(self):
"""
......
......@@ -13,10 +13,6 @@ from common.test.acceptance.tests.helpers import select_option_by_value
PROFILE_VISIBILITY_SELECTOR = '#u-field-select-account_privacy option[value="{}"]'
PROFILE_VISIBILITY_INPUT = '#u-field-select-account_privacy'
FIELD_ICONS = {
'country': 'fa-map-marker',
'language_proficiencies': 'fa-comment',
}
class Badge(PageObject):
......@@ -214,18 +210,6 @@ class LearnerProfilePage(FieldsMixin, PageObject):
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.
......
......@@ -367,8 +367,6 @@ class OwnLearnerProfilePageTest(LearnerProfileTestMixin, AcceptanceTest):
profile_page.make_field_editable('country')
self.assertEqual(profile_page.mode_for_field('country'), 'edit')
self.assertTrue(profile_page.field_icon_present('country'))
def test_language_field(self):
"""
Test behaviour of `Language` field.
......@@ -396,8 +394,6 @@ class OwnLearnerProfilePageTest(LearnerProfileTestMixin, AcceptanceTest):
profile_page.make_field_editable('language_proficiencies')
self.assertTrue(profile_page.mode_for_field('language_proficiencies'), 'edit')
self.assertTrue(profile_page.field_icon_present('language_proficiencies'))
def test_about_me_field(self):
"""
Test behaviour of `About Me` field.
......
......@@ -515,15 +515,16 @@
'click .wrapper-u-field': 'startEditing',
'click .u-field-placeholder': 'startEditing',
'focusout textarea': 'finishEditing',
'change textarea': 'adjustTextareaHeight',
'keyup textarea': 'adjustTextareaHeight',
'change textarea': 'manageTextareaContentChange',
'keyup textarea': 'manageTextareaContentChange',
'keydown textarea': 'onKeyDown',
'paste textarea': 'adjustTextareaHeight',
'cut textarea': 'adjustTextareaHeight'
'paste textarea': 'manageTextareaContentChange',
'cut textarea': 'manageTextareaContentChange'
},
initialize: function(options) {
_.bindAll(this, 'render', 'onKeyDown', 'adjustTextareaHeight', 'fieldValue', 'saveValue', 'updateView');
_.bindAll(this, 'render', 'onKeyDown', 'adjustTextareaHeight', 'manageTextareaContentChange',
'fieldValue', 'saveValue', 'updateView');
this._super(options);
this.listenTo(this.model, 'change:' + this.options.valueAttribute, this.updateView);
},
......@@ -541,7 +542,8 @@
value: value,
message: this.helpMessage,
messagePosition: this.options.messagePosition || 'footer',
placeholderValue: this.options.placeholderValue
placeholderValue: this.options.placeholderValue,
maxCharacters: this.options.maxCharacters || ''
}));
this.delegateEvents();
this.title((this.modelValue() || this.mode === 'edit') ?
......@@ -562,12 +564,26 @@
}
},
updateCharCount: function() {
var curCharCount;
// Update character count for textarea
if (this.options.maxCharacters) {
curCharCount = $('#u-field-textarea-' + this.options.valueAttribute).val().length;
$('.u-field-footer .current-char-count').text(curCharCount);
}
},
adjustTextareaHeight: function() {
if (this.persistChanges === false) { return; }
var textarea = this.$('textarea');
textarea.css('height', 'auto').css('height', textarea.prop('scrollHeight') + 10);
},
manageTextareaContentChange: function() {
this.updateCharCount();
this.adjustTextareaHeight();
},
modelValue: function() {
var value = this._super();
return value ? $.trim(value) : '';
......
......@@ -55,7 +55,6 @@
// base - specific views
@import "views/account-settings";
@import "views/learner-profile";
@import 'views/login-register';
@import 'views/verification';
@import 'views/decoupled-verification';
......@@ -69,6 +68,7 @@
// features
@import 'features/bookmarks-v1';
@import 'features/learner-profile';
// search
@import 'search/search';
......
......@@ -74,7 +74,7 @@
margin: 0 ($baseline*0.75);
padding: ($baseline/4);
text-align: center;
color: $gray;
color: $gray-d1;
}
.current-page {
......
......@@ -25,8 +25,6 @@
}
.profile-image-field {
@include float(left);
button {
background: transparent !important;
border: none !important;
......@@ -41,13 +39,18 @@
.image-wrapper {
width: $profile-image-dimension;
position: relative;
margin: auto;
.image-frame {
display: block;
position: relative;
width: $profile-image-dimension;
height: $profile-image-dimension;
border-radius: ($baseline/4);
border-radius: ($profile-image-dimension/2);
overflow: hidden;
border: 3px solid $gray-lightest;
margin-top: $baseline*-0.75;
background: $white;
}
.u-field-upload-button {
......@@ -55,13 +58,12 @@
top: 0;
width: $profile-image-dimension;
height: $profile-image-dimension;
border-radius: ($baseline/4);
border-radius: ($profile-image-dimension/2);
border: 2px dashed transparent;
background: rgba(229,241,247, .8);
color: $link-color;
text-shadow: none;
@include transition(all $tmg-f1 ease-in-out 0s);
opacity: 0;
z-index: 6;
i {
......@@ -87,17 +89,20 @@
line-height: 1.3em;
text-align: center;
z-index: 7;
color: $base-font-color;
}
.upload-button-input {
position: absolute;
top: -($profile-image-dimension * 2);
top: 0;
@include left(0);
width: $profile-image-dimension;
border-radius: ($profile-image-dimension/2);
height: 100%;
cursor: pointer;
z-index: 5;
outline: 0;
opacity: 0;
}
.u-field-remove-button {
......@@ -113,6 +118,7 @@
.wrapper-profile {
min-height: 200px;
background-color: $gray-lightest;
.ui-loading-indicator {
margin-top: 100px;
......@@ -133,7 +139,7 @@
@extend .container;
border: none;
box-shadow: none;
padding: 0 ($baseline*1.5);
padding: 0 ($baseline*3);
}
.u-field-title {
......@@ -164,53 +170,93 @@
.wrapper-profile-sections {
@extend .container;
padding: 0 ($baseline*1.5);
@include padding($baseline*1.5, $baseline*1.5, $baseline*1.5, 0);
min-width: 0;
@media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
@include margin-left(0);
@include padding($baseline*1.5, 0, $baseline*1.5, 0);
}
}
.profile-header {
@include padding(0, $baseline*2, $baseline, $baseline*3);
.header {
@extend %t-title4;
@extend %t-ultrastrong;
display: inline-block;
}
.subheader {
@extend %t-title6;
}
}
.wrapper-profile-section-one {
width: 100%;
display: inline-block;
margin-top: ($baseline*1.5);
@include margin-left($baseline/2);
@include float(left);
@include margin-left($baseline*3);
width: 300px;
background-color: $white;
border-top: 5px solid $blue;
padding-bottom: $baseline;
@media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
@include margin-left(0);
width: 100%;
}
}
.profile-section-one-fields {
@include float(left);
width: flex-grid(4, 12);
@include margin-left($baseline);
margin: 0 $baseline/2;
.u-field {
margin-bottom: ($baseline/4);
padding-top: 3px;
padding-bottom: 3px;
@include padding-left(3px);
}
@extend %t-weight4;
@include padding(0, 0, 0, 3px);
color: $base-font-color;
.u-field-username {
.u-field-value {
@extend %t-weight4;
width: calc(100% - 40px);
input[type="text"] {
font-weight: 600;
.u-field-value-readonly {
@extend %t-weight3;
}
}
.u-field-value {
width: 350px;
@extend %t-title4;
.u-field-title {
color: $base-font-color;
font-size: $body-font-size;
display: block;
}
}
.u-field-icon {
display: inline-block;
vertical-align: baseline;
}
&.u-field-dropdown {
margin-top: $baseline/5;
.u-field-title {
width: 0;
&:not(.editable-never) {
cursor: pointer;
}
&:not(:last-child) {
padding-bottom: $baseline/4;
border-bottom: 1px solid $gray-lighter;
}
}
}
.u-field-value {
width: 200px;
display: inline-block;
vertical-align: baseline;
&>.u-field {
&:not(:first-child) {
font-size: $body-font-size;
color: $base-font-color;
font-weight: $font-light;
margin-bottom: 0;
}
&:first-child {
@extend %t-title4;
@extend %t-weight4;
font-size: em(24);
}
}
select {
......@@ -230,16 +276,29 @@
}
}
.wrapper-profile-section-two {
padding-top: 1em;
width: flex-grid(8, 12);
}
.wrapper-profile-section-container-two {
@include float(left);
width: calc(100% - 380px);
max-width: $learner-profile-container-flex; // Switch to map-get($grid-breakpoints,md) for bootstrap
.profile-section-two-fields {
@media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
width: 100%;
margin-top: $baseline;
}
.u-field-textarea {
margin-bottom: ($baseline/2);
padding: ($baseline/2) ($baseline*.75) ($baseline*.75) ($baseline*.75);
padding: 0 ($baseline*.75) ($baseline*.75) ($baseline*.75);
.u-field-header {
position: relative;
.u-field-message {
@include right(0);
top: $baseline/4;
position: absolute;
}
}
&.editable-toggle {
cursor: pointer;
......@@ -247,22 +306,30 @@
}
.u-field-title {
@extend %t-title5;
@extend %t-weight4;
@extend %t-title6;
@extend %t-weight5;
display: inline-block;
margin-top: 0;
margin-bottom: ($baseline/4);
color: inherit;
color: $gray-dark;
width: 100%;
}
.u-field-value {
@extend %t-copy-base;
width: 100%;
overflow: scroll;
textarea {
width: 100%;
background-color: transparent;
border-radius: 5px;
border-color: $gray-d1;
resize: none;
white-space: pre-line;
outline: 0;
box-shadow: none;
-webkit-appearance: none;
}
a {
......@@ -273,16 +340,22 @@
.u-field-message {
@include float(right);
width: auto;
.message-can-edit {
position: absolute;
}
}
.u-field.mode-placeholder {
padding: $baseline;
margin: $baseline * 0.75;
border: 2px dashed $gray-l3;
i {
font-size: 12px;
@include padding-right(5px);
vertical-align: middle;
color: $gray;
color: $base-font-color;
}
.u-field-title {
width: 100%;
......@@ -293,7 +366,7 @@
text-align: center;
line-height: 1.5em;
@extend %t-copy-sub1;
color: $gray;
color: $base-font-color;
}
}
......@@ -304,6 +377,28 @@
color: $link-color;
}
}
.wrapper-u-field {
font-size: $body-font-size;
color: $base-font-color;
.u-field-header .u-field-title{
color: $base-font-color;
}
.u-field-footer {
.field-textarea-character-count {
@extend %t-weight1;
@include float(right);
margin-top: $baseline/4;
}
}
}
.profile-private-message {
@include padding-left($baseline*0.75);
line-height: 3.0em;
}
}
.badge-paging-header {
......
......@@ -223,6 +223,9 @@ $success-color-hover: rgb(0, 129, 0) !default;
// ----------------------------
// #COLORS- Bootstrap-style
// ----------------------------
$gray-dark: #4e5455 !default;
$gray-lighter: #eceeef !default;
$gray-lightest: #f7f7f9 !default;
$state-success-text: $black !default;
$state-success-bg: #dff0d8 !default;
......@@ -545,6 +548,9 @@ $palette-success-border: #b9edb9;
$palette-success-back: #ecfaec;
$palette-success-text: #008100;
// learner profile elements
$learner-profile-container-flex: 768px;
// course elements
$content-wrapper-bg: $white !default;
$course-bg-color: $uxpl-grayscale-x-back !default;
......
......@@ -12,7 +12,7 @@
border-radius: 3px;
span {
color: $gray;
color: $gray-d1;
}
&:hover {
......@@ -84,7 +84,7 @@
.u-field-title {
width: flex-grid(3, 12);
display: inline-block;
color: $gray;
color: $gray-d1;
vertical-align: top;
margin-bottom: 0;
-webkit-font-smoothing: antialiased;
......
......@@ -3,7 +3,7 @@
.header-global {
@extend %ui-depth1;
border-bottom: 2px solid $header-border-color;
border-bottom: 1px solid $header-border-color;
box-shadow: 0 1px 5px 0 $shadow-l1;
background: $header-bg;
position: relative;
......
<% if (editable !== 'never') { %>
<% if (title && titleVisible) { %>
<label class="u-field-title" for="u-field-select-<%- id %>">
<%- title %>
</label>
<% } else { %>
<label class="sr" for="u-field-select-<%- id %>">
<%- screenReaderTitle %>
</label>
<% } %>
<% if (title && titleVisible) { %>
<label class="u-field-title" for="u-field-select-<%- id %>">
<%- title %>
</label>
<% } else { %>
<label class="sr" for="u-field-select-<%- id %>">
<%- screenReaderTitle %>
</label>
<% } %>
<% if (iconName) { %>
<span class="u-field-icon icon fa <%- iconName %> fa-fw" aria-hidden="true"></span>
<% if (editable !== 'never') { %>
<% if (iconName) { %>
<span class="u-field-icon icon fa <%- iconName %> fa-fw" aria-hidden="true"></span>
<% } %>
<% } %>
<span class="u-field-value">
......
<div class="u-field-value field">
<% if (editable !== 'never') { %>
<% if (title && titleVisible) { %>
<label class="u-field-title field-label" for="u-field-select-<%- id %>">
<%- title %>
</label>
<% } else { %>
<label class="sr" for="u-field-select-<%- id %>">
<%- screenReaderTitle %>
</label>
<% } %>
<% if (title && titleVisible) { %>
<label class="u-field-title field-label" for="u-field-select-<%- id %>">
<%- title %>
</label>
<% } else { %>
<label class="sr" for="u-field-select-<%- id %>">
<%- screenReaderTitle %>
</label>
<% } %>
<% if (iconName) { %>
......
<% if (title) { %>
<span class="u-field-title" aria-hidden="true"><%- title %></span>
<span class="u-field-title" aria-hidden="true"><%- title %></span>
<% } %>
<span class="sr" for="u-field-value-<%- id %>"><%- screenReaderTitle %></span>
<span class="u-field-value" id="u-field-value-<%- id %>" aria-describedby="u-field-message-<%- id %>"><%- value %></span>
<span class="u-field-message" id="u-field-message-<%- id %>">
......
<div class="wrapper-u-field">
<div class="wrapper-u-field" role="group">
<div class="u-field-header">
<% if (mode === 'edit') { %>
<label class="u-field-title" for="u-field-textarea-<%- id %>" id="u-field-title-<%- id %>"></label>
......@@ -15,7 +15,7 @@
<div class="u-field-value" id="u-field-value-<%- id %>"
<% if (mode === 'edit') { %>
aria-labelledby="u-field-title-<%- id %>"><textarea id="u-field-textarea-<%- id %>" rows="4"
aria-labelledby="u-field-title-<%- id %>"><textarea maxlength="<%- maxCharacters%>" id="u-field-textarea-<%- id %>" rows="4"
<% if (message) { %>
aria-describedby="u-field-message-help-<%- id %>"
<% } %>
......@@ -43,5 +43,20 @@
<span class="u-field-message-help" id="u-field-message-help-<%- id %>"> <%- message %></span>
</span>
<% } %>
<% if (mode === 'edit' && maxCharacters) { %>
<div class="field-textarea-character-count">
<%=
HtmlUtils.interpolateHtml(
gettext('{currentCountOpeningTag}{currentCharacterCount}{currentCountClosingTag} of {maxCharacters}'),
{
currentCountOpeningTag: HtmlUtils.HTML('<span class="current-char-count">'),
currentCountClosingTag: HtmlUtils.HTML('</span>'),
currentCharacterCount: value.length,
maxCharacters: maxCharacters
}
)
%>
</div>
<% } %>
</div>
</div>
......@@ -20,8 +20,8 @@
</form>
</div>
<div class="form-actions">
<a class="btn btn-brand action-resume-course" href="${resume_course_url}">
Start Course
<a class="btn btn-brand action-resume-course" href="/courses/course-v1:edX+DemoX+Demo_Course/courseware/19a30717eff543078a5d94ae9d6c18a5/">
<span data-action-type="start">Start Course</span>
</a>
</div>
</div>
......
......@@ -2,6 +2,19 @@
export class CourseHome { // eslint-disable-line import/prefer-default-export
constructor(options) {
// Logging for 'Resume Course' or 'Start Course' button click
const $resumeCourseLink = $(options.resumeCourseLink);
$resumeCourseLink.on('click', (event) => {
const eventType = $resumeCourseLink.find('span').data('action-type');
Logger.log(
'edx.course.home.resume_course.clicked',
{
event_type: eventType,
url: event.currentTarget.href,
},
);
});
// Logging for course tool click events
const $courseToolLink = $(options.courseToolLink);
$courseToolLink.on('click', (event) => {
......
......@@ -9,11 +9,24 @@ describe('Course Home factory', () => {
beforeEach(() => {
loadFixtures('course_experience/fixtures/course-home-fragment.html');
home = new CourseHome({
resumeCourseLink: '.action-resume-course',
courseToolLink: '.course-tool-link',
});
spyOn(Logger, 'log');
});
it('sends an event when resume or start course is clicked', () => {
$('.action-resume-course').click();
expect(Logger.log).toHaveBeenCalledWith(
'edx.course.home.resume_course.clicked',
{
event_type: 'start',
url: `http://${window.location.host}/courses/course-v1:edX+DemoX+Demo_Course/courseware` +
'/19a30717eff543078a5d94ae9d6c18a5/',
},
);
});
it('sends an event when an course tool is clicked', () => {
const courseToolNames = document.querySelectorAll('.course-tool-link');
for (let i = 0; i < courseToolNames.length; i += 1) {
......
......@@ -45,9 +45,9 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
% if resume_course_url:
<a class="btn btn-brand action-resume-course" href="${resume_course_url}">
% if has_visited_course:
${_("Resume Course")}
<span data-action-type="resume">${_("Resume Course")}</span>
% else:
${_("Start Course")}
<span data-action-type="start">${_("Start Course")}</span>
% endif
</a>
% endif
......@@ -109,6 +109,7 @@ from openedx.features.course_experience import UNIFIED_COURSE_TAB_FLAG, SHOW_REV
<%static:webpack entry="CourseHome">
new CourseHome({
resumeCourseLink: ".action-resume-course",
courseToolLink: ".course-tool-link",
});
</%static:webpack>
......
......@@ -114,11 +114,8 @@ class CourseHomeMessageFragmentView(EdxFragmentView):
CourseHomeMessages.register_info_message(
request,
Text(_(
"{add_reminder_open_tag}Don't forget to add a calendar reminder!{add_reminder_close_tag}."
)).format(
add_reminder_open_tag='',
add_reminder_close_tag=''
),
"Don't forget to add a calendar reminder!"
)),
title=Text("Course starts in {days_until_start_string} on {course_start_date}.").format(
days_until_start_string=course_start_data['days_until_start_string'],
course_start_date=course_start_data['course_start_date']
......
......@@ -48,6 +48,7 @@
var accountPrivacyFieldView,
profileImageFieldView,
usernameFieldView,
nameFieldView,
sectionOneFieldViews,
sectionTwoFieldViews,
BadgeCollection,
......@@ -65,10 +66,7 @@
required: true,
editable: 'always',
showMessages: false,
title: StringUtils.interpolate(
gettext('{platform_name} learners can see my:'),
{platform_name: options.platform_name}
),
title: gettext('Profile Visibility:'),
valueAttribute: 'account_privacy',
options: [
['private', gettext('Limited Profile')],
......@@ -97,29 +95,37 @@
helpMessage: ''
});
nameFieldView = new FieldsView.ReadonlyFieldView({
model: accountSettingsModel,
screenReaderTitle: gettext('Full Name'),
valueAttribute: 'name',
helpMessage: ''
});
sectionOneFieldViews = [
new FieldsView.DropdownFieldView({
title: gettext('Location'),
titleVisible: true,
model: accountSettingsModel,
screenReaderTitle: gettext('Country'),
titleVisible: false,
required: true,
editable: editable,
showMessages: false,
iconName: 'fa-map-marker',
placeholderValue: gettext('Add Country'),
valueAttribute: 'country',
options: options.country_options,
helpMessage: '',
persistChanges: true
}),
new AccountSettingsFieldViews.LanguageProficienciesFieldView({
title: gettext('Language'),
titleVisible: true,
model: accountSettingsModel,
screenReaderTitle: gettext('Preferred Language'),
titleVisible: false,
required: false,
editable: editable,
showMessages: false,
iconName: 'fa-comment',
placeholderValue: gettext('Add language'),
valueAttribute: 'language_proficiencies',
options: options.language_options,
......@@ -139,7 +145,8 @@
valueAttribute: 'bio',
helpMessage: '',
persistChanges: true,
messagePosition: 'header'
messagePosition: 'header',
maxCharacters: 300
})
];
......@@ -172,9 +179,11 @@
accountPrivacyFieldView: accountPrivacyFieldView,
profileImageFieldView: profileImageFieldView,
usernameFieldView: usernameFieldView,
nameFieldView: nameFieldView,
sectionOneFieldViews: sectionOneFieldViews,
sectionTwoFieldViews: sectionTwoFieldViews,
badgeListContainer: badgeListContainer
badgeListContainer: badgeListContainer,
platformName: options.platform_name
});
getProfileVisibility = function() {
......
/* eslint-disable vars-on-top */
define(
[
'gettext',
'backbone',
'jquery',
'underscore',
......@@ -18,7 +19,7 @@ define(
'js/student_account/views/account_settings_fields',
'js/views/message_banner'
],
function(Backbone, $, _, PagingCollection, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers,
function(gettext, Backbone, $, _, PagingCollection, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers,
FieldViews, UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView,
BadgeListContainer, AccountSettingsFieldViews, MessageBannerView) {
'use strict';
......@@ -73,13 +74,19 @@ define(
helpMessage: ''
});
var nameFieldView = new FieldViews.ReadonlyFieldView({
model: accountSettingsModel,
valueAttribute: 'name',
helpMessage: ''
});
var sectionOneFieldViews = [
new FieldViews.DropdownFieldView({
title: gettext('Location'),
model: accountSettingsModel,
required: false,
editable: editable,
showMessages: false,
iconName: 'fa-map-marker',
placeholderValue: '',
valueAttribute: 'country',
options: Helpers.FIELD_OPTIONS,
......@@ -87,11 +94,11 @@ define(
}),
new AccountSettingsFieldViews.LanguageProficienciesFieldView({
title: gettext('Language'),
model: accountSettingsModel,
required: false,
editable: editable,
showMessages: false,
iconName: 'fa-comment',
placeholderValue: 'Add language',
valueAttribute: 'language_proficiencies',
options: Helpers.FIELD_OPTIONS,
......@@ -131,6 +138,7 @@ define(
preferencesModel: accountPreferencesModel,
accountPrivacyFieldView: accountPrivacyFieldView,
usernameFieldView: usernameFieldView,
nameFieldView: nameFieldView,
profileImageFieldView: profileImageFieldView,
sectionOneFieldViews: sectionOneFieldViews,
sectionTwoFieldViews: sectionTwoFieldViews,
......
......@@ -65,7 +65,7 @@ define(
view.render();
var bio = view.$el.find('.u-field-bio');
expect(bio.length).toBe(0);
var msg = view.$el.find('span.profile-private--message');
var msg = view.$el.find('span.profile-private-message');
expect(msg.length).toBe(1);
expect(_.count(msg.html(), messageString)).toBeTruthy();
};
......
......@@ -41,11 +41,12 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
var expectSectionOneTobeRendered = function(learnerProfileView) {
var sectionOneFieldElements = $(learnerProfileView.$('.wrapper-profile-section-one')).find('.u-field');
expect(sectionOneFieldElements.length).toBe(4);
expect(sectionOneFieldElements.length).toBe(5);
expectProfileElementContainsField(sectionOneFieldElements[0], learnerProfileView.options.profileImageFieldView);
expectProfileElementContainsField(sectionOneFieldElements[1], learnerProfileView.options.usernameFieldView);
expectProfileElementContainsField(sectionOneFieldElements[2], learnerProfileView.options.nameFieldView);
_.each(_.rest(sectionOneFieldElements, 2), function(sectionFieldElement, fieldIndex) {
_.each(_.rest(sectionOneFieldElements, 3), function(sectionFieldElement, fieldIndex) {
expectProfileElementContainsField(
sectionFieldElement,
learnerProfileView.options.sectionOneFieldViews[fieldIndex]
......@@ -89,10 +90,10 @@ define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'
);
if (othersProfile) {
expect($('.profile-private--message').text())
expect($('.profile-private-message').text())
.toBe('This learner is currently sharing a limited profile.');
} else {
expect($('.profile-private--message').text()).toBe('You are currently sharing a limited profile.');
expect($('.profile-private-message').text()).toBe('You are currently sharing a limited profile.');
}
};
......
......@@ -6,9 +6,10 @@
'gettext', 'jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/html-utils',
'common/js/components/views/tabbed_view',
'learner_profile/js/views/section_two_tab',
'text!learner_profile/templates/learner_profile.underscore'
'text!learner_profile/templates/learner_profile.underscore',
'edx-ui-toolkit/js/utils/string-utils'
],
function(gettext, $, _, Backbone, HtmlUtils, TabbedView, SectionTwoTab, learnerProfileTemplate) {
function(gettext, $, _, Backbone, HtmlUtils, TabbedView, SectionTwoTab, learnerProfileTemplate, StringUtils) {
var LearnerProfileView = Backbone.View.extend({
initialize: function(options) {
......@@ -53,10 +54,19 @@
ownProfile: this.options.ownProfile
});
HtmlUtils.setHtml(this.$el, HtmlUtils.template(learnerProfileTemplate)({
username: self.options.accountSettingsModel.get('username'),
name: self.options.accountSettingsModel.get('name'),
ownProfile: self.options.ownProfile,
showFullProfile: self.showFullProfile()
showFullProfile: self.showFullProfile(),
profile_header: gettext('My Profile'),
profile_subheader:
StringUtils.interpolate(
gettext('Build out your profile to personalize your identity on {platform_name}.'), {
platform_name: self.options.platformName
}
)
}));
this.renderFields();
......@@ -98,7 +108,7 @@
Backbone.history.start();
}
} else {
this.$el.find('.account-settings-container').append(this.sectionTwoView.render().el);
this.$el.find('.wrapper-profile-section-container-two').append(this.sectionTwoView.render().el);
}
return this;
},
......@@ -120,6 +130,10 @@
fieldView.delegateEvents();
}
// Do not show name when in limited mode or no name has been set
if (this.showFullProfile() && this.options.accountSettingsModel.get('name')) {
this.$('.profile-section-one-fields').append(this.options.nameFieldView.render().el);
}
this.$('.profile-section-one-fields').append(this.options.usernameFieldView.render().el);
imageView = this.options.profileImageFieldView;
......
<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-image-field">
<% if (ownProfile) { %>
<div class="profile-header">
<div class="header"> <%- profile_header %></div>
<div class="subheader"> <%- profile_subheader %></div>
</div>
<div class="profile-section-one-fields">
<% } %>
<div class="wrapper-profile-section-container-one">
<div class="wrapper-profile-section-one">
<div class="profile-image-field">
</div>
<div class="profile-section-one-fields">
</div>
</div>
<div class="ui-loading-error is-hidden">
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
<span class="copy"><%- gettext("An error occurred. Try loading the page again.") %></span>
</div>
</div>
<div class="ui-loading-error is-hidden">
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
<span class="copy"><%- gettext("An error occurred. Try loading the page again.") %></span>
<div class="wrapper-profile-section-container-two">
</div>
</div>
</div>
......@@ -2,9 +2,9 @@
<div class="field-container"></div>
<% if (!showFullProfile) { %>
<% if(ownProfile) { %>
<span class="profile-private--message" tabindex="0"><%- gettext("You are currently sharing a limited profile.") %></span>
<span class="profile-private-message"><%- gettext("You are currently sharing a limited profile.") %></span>
<% } else { %>
<span class="profile-private--message" tabindex="0"><%- gettext("This learner is currently sharing a limited profile.") %></span>
<span class="profile-private-message"><%- gettext("This learner is currently sharing a limited profile.") %></span>
<% } %>
<% } %>
</div>
\ No newline at end of file
......@@ -8,7 +8,7 @@
# For Harvard courses:
-e git+https://github.com/gsehub/xblock-mentoring.git@4d1cce78dc232d5da6ffd73817b5c490e87a6eee#egg=xblock-mentoring
git+https://github.com/open-craft/problem-builder.git@v2.6.10#egg=xblock-problem-builder==2.6.10
git+https://github.com/open-craft/problem-builder.git@v2.6.5#egg=xblock-problem-builder==2.6.5
# Oppia XBlock
-e git+https://github.com/oppia/xblock.git@9f6b95b7eb7dbabb96b77198a3202604f96adf65#egg=oppia-xblock
......
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