FC-0001: Account pages -> micro-frontend (#30336)

* feat: Account pages. Learner Profile page

* feat: Account pages. Account Settings page

* feat: Account pages. Removed unused styles

* feat: Account pages. Removed unused toggles

* feat: fixed tests and pylint errors

* feat: update redirect to account settings for student_dashboard

* feat: fix pylint errors
This commit is contained in:
Sagirov Evgeniy
2023-03-07 14:41:52 +02:00
committed by GitHub
parent 20ef29da53
commit 0f02c7b3d9
85 changed files with 84 additions and 7528 deletions

View File

@@ -26,7 +26,8 @@ from xmodule.modulestore.modulestore_settings import update_module_store_setting
from .common import *
# import settings from LMS for consistent behavior with CMS
from lms.envs.test import ( # pylint: disable=wrong-import-order
from lms.envs.test import ( # pylint: disable=wrong-import-order, disable=unused-import
ACCOUNT_MICROFRONTEND_URL,
BLOCKSTORE_USE_BLOCKSTORE_APP_API,
BLOCKSTORE_API_URL,
COMPREHENSIVE_THEME_DIRS, # unimport:skip
@@ -37,8 +38,10 @@ from lms.envs.test import ( # pylint: disable=wrong-import-order
LOGIN_ISSUE_SUPPORT_LINK,
MEDIA_ROOT,
MEDIA_URL,
ORDER_HISTORY_MICROFRONTEND_URL,
PLATFORM_DESCRIPTION,
PLATFORM_NAME,
PROFILE_MICROFRONTEND_URL,
REGISTRATION_EXTRA_FIELDS,
GRADES_DOWNLOAD,
SITE_NAME,

View File

@@ -1,6 +1,7 @@
"""
Test that various filters are fired for models/views in the student app.
"""
from django.conf import settings
from django.http import HttpResponse
from django.test import override_settings
from django.urls import reverse
@@ -421,7 +422,7 @@ class StudentDashboardFiltersTest(ModuleStoreTestCase):
response = self.client.get(self.dashboard_url)
self.assertEqual(status.HTTP_302_FOUND, response.status_code)
self.assertEqual(reverse("account_settings"), response.url)
self.assertEqual(settings.ACCOUNT_MICROFRONTEND_URL, response.url)
@override_settings(
OPEN_EDX_FILTERS_CONFIG={

View File

@@ -234,7 +234,7 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin,
"""
UserProfile.objects.get(user=self.user).delete()
response = self.client.get(self.path)
self.assertRedirects(response, reverse('account_settings'))
self.assertRedirects(response, settings.ACCOUNT_MICROFRONTEND_URL, target_status_code=302)
@patch('common.djangoapps.student.views.dashboard.should_redirect_to_learner_home_mfe')
def test_redirect_to_learner_home(self, mock_should_redirect_to_learner_home_mfe):

View File

@@ -519,7 +519,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
"""
user = request.user
if not UserProfile.objects.filter(user=user).exists():
return redirect(reverse('account_settings'))
return redirect(settings.ACCOUNT_MICROFRONTEND_URL)
if should_redirect_to_learner_home_mfe(user):
return redirect(settings.LEARNER_HOME_MICROFRONTEND_URL)
@@ -624,7 +624,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
"Go to {link_start}your Account Settings{link_end}.")
).format(
link_start=HTML("<a href='{account_setting_page}'>").format(
account_setting_page=reverse('account_settings'),
account_setting_page=settings.ACCOUNT_MICROFRONTEND_URL,
),
link_end=HTML("</a>")
)
@@ -897,7 +897,7 @@ def student_dashboard(request): # lint-amnesty, pylint: disable=too-many-statem
except DashboardRenderStarted.RenderInvalidDashboard as exc:
response = render_to_response(exc.dashboard_template, exc.template_context)
except DashboardRenderStarted.RedirectToPage as exc:
response = HttpResponseRedirect(exc.redirect_to or reverse('account_settings'))
response = HttpResponseRedirect(exc.redirect_to or settings.ACCOUNT_MICROFRONTEND_URL)
except DashboardRenderStarted.RenderCustomResponse as exc:
response = exc.response
else:

View File

@@ -2,10 +2,11 @@
Tests for the Third Party Auth REST API
"""
import urllib
from unittest.mock import patch
import ddt
import six
from django.conf import settings
from django.http import QueryDict
from django.test.utils import override_settings
from django.urls import reverse
@@ -219,7 +220,7 @@ class UserViewV2APITests(UserViewsMixin, TpaAPITestCase):
"""
return '?'.join([
reverse('third_party_auth_users_api_v2'),
six.moves.urllib.parse.urlencode(identifier)
urllib.parse.urlencode(identifier)
])
@@ -377,11 +378,12 @@ class TestThirdPartyAuthUserStatusView(ThirdPartyAuthTestMixin, APITestCase):
"""
self.client.login(username=self.user.username, password=PASSWORD)
response = self.client.get(self.url, content_type="application/json")
next_url = urllib.parse.quote(settings.ACCOUNT_MICROFRONTEND_URL, safe="")
assert response.status_code == 200
assert (response.data ==
[{
'accepts_logins': True, 'name': 'Google',
'disconnect_url': '/auth/disconnect/google-oauth2/?',
'connect_url': '/auth/login/google-oauth2/?auth_entry=account_settings&next=%2Faccount%2Fsettings',
'connect_url': f'/auth/login/google-oauth2/?auth_entry=account_settings&next={next_url}',
'connected': False, 'id': 'oa2-google-oauth2'
}])

View File

@@ -9,7 +9,6 @@ from django.conf import settings
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
from django.db.models import Q
from django.http import Http404
from django.urls import reverse
from edx_rest_framework_extensions.auth.jwt.authentication import JwtAuthentication
from edx_rest_framework_extensions.auth.session.authentication import SessionAuthenticationAllowInactiveUser
from rest_framework import exceptions, permissions, status, throttling
@@ -425,7 +424,7 @@ class ThirdPartyAuthUserStatusView(APIView):
state.provider.provider_id,
pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS,
# The url the user should be directed to after the auth process has completed.
redirect_url=reverse('account_settings'),
redirect_url=settings.ACCOUNT_MICROFRONTEND_URL,
),
'accepts_logins': state.provider.accepts_logins,
# If the user is connected, sending a POST request to this url removes the connection

View File

@@ -11,7 +11,7 @@ from unittest import mock
import pytest
from django import test
from django.conf import settings
from django.contrib import auth
from django.contrib import auth, messages
from django.contrib.auth import models as auth_models
from django.contrib.messages.storage import fallback
from django.contrib.sessions.backends import cache
@@ -28,7 +28,6 @@ from openedx.core.djangoapps.user_authn.views.login_form import login_and_regist
from openedx.core.djangoapps.user_authn.views.register import RegistrationView
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.user_api.accounts.settings_views import account_settings_context
from common.djangoapps.student import models as student_models
from common.djangoapps.student.tests.factories import UserFactory
@@ -99,6 +98,43 @@ class HelperMixin:
if prepopulated_form_data in required_fields:
self.assertContains(response, form_field_data[prepopulated_form_data])
def _get_user_providers_state(self, request):
"""
Return provider user states and duplicated providers.
"""
data = {
'auth': {},
}
data['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request))
auth_states = pipeline.get_provider_user_states(request.user)
data['auth']['providers'] = [{
'name': state.provider.name,
'connected': state.has_account,
} for state in auth_states if state.provider.display_for_login or state.has_account]
return data
def assert_third_party_accounts_state(self, request, duplicate=False, linked=None):
"""
Asserts the user's third party account in the expected state.
If duplicate is True, we expect data['duplicate_provider'] to contain
the duplicate provider backend name. If linked is passed, we conditionally
check that the provider is included in data['auth']['providers'] and
its connected state is correct.
"""
data = self._get_user_providers_state(request)
if duplicate:
assert data['duplicate_provider'] == self.provider.backend_name
else:
assert data['duplicate_provider'] is None
if linked is not None:
expected_provider = [
provider for provider in data['auth']['providers'] if provider['name'] == self.provider.name
][0]
assert expected_provider is not None
assert expected_provider['connected'] == linked
def assert_register_form_populates_unicode_username_correctly(self, request): # lint-amnesty, pylint: disable=invalid-name
"""
Check the registration form username field behaviour with unicode values.
@@ -118,27 +154,6 @@ class HelperMixin:
with mock.patch.dict('django.conf.settings.FEATURES', {'ENABLE_UNICODE_USERNAME': True}):
self._check_registration_form_username(pipeline_kwargs, unicode_username, unicode_username)
# pylint: disable=invalid-name
def assert_account_settings_context_looks_correct(self, context, duplicate=False, linked=None):
"""Asserts the user's account settings page context is in the expected state.
If duplicate is True, we expect context['duplicate_provider'] to contain
the duplicate provider backend name. If linked is passed, we conditionally
check that the provider is included in context['auth']['providers'] and
its connected state is correct.
"""
if duplicate:
assert context['duplicate_provider'] == self.provider.backend_name
else:
assert context['duplicate_provider'] is None
if linked is not None:
expected_provider = [
provider for provider in context['auth']['providers'] if provider['name'] == self.provider.name
][0]
assert expected_provider is not None
assert expected_provider['connected'] == linked
def assert_exception_redirect_looks_correct(self, expected_uri, auth_entry=None):
"""Tests middleware conditional redirection.
@@ -611,7 +626,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
# First we expect that we're in the unlinked state, and that there
# really is no association in the backend.
self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=False)
self.assert_third_party_accounts_state(get_request, linked=False)
self.assert_social_auth_does_not_exist_for_user(get_request.user, strategy)
# We should be redirected back to the complete page, setting
@@ -630,7 +645,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
# Now we expect to be in the linked state, with a backend entry.
self.assert_social_auth_exists_for_user(get_request.user, strategy)
self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=True)
self.assert_third_party_accounts_state(get_request, linked=True)
def test_full_pipeline_succeeds_for_unlinking_account(self):
# First, create, the GET request and strategy that store pipeline state,
@@ -662,7 +677,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
get_request.user = post_request.user
# First we expect that we're in the linked state, with a backend entry.
self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=True)
self.assert_third_party_accounts_state(get_request, linked=True)
self.assert_social_auth_exists_for_user(get_request.user, strategy)
# Fire off the disconnect pipeline to unlink.
@@ -676,7 +691,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
)
# Now we expect to be in the unlinked state, with no backend entry.
self.assert_account_settings_context_looks_correct(account_settings_context(get_request), linked=False)
self.assert_third_party_accounts_state(get_request, linked=False)
self.assert_social_auth_does_not_exist_for_user(user, strategy)
def test_linking_already_associated_account_raises_auth_already_associated(self):
@@ -734,8 +749,8 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
post_request,
exceptions.AuthAlreadyAssociated(self.provider.backend_name, 'account is already in use.'))
self.assert_account_settings_context_looks_correct(
account_settings_context(post_request), duplicate=True, linked=True)
self.assert_third_party_accounts_state(
post_request, duplicate=True, linked=True)
@mock.patch('common.djangoapps.third_party_auth.pipeline.segment.track')
def test_full_pipeline_succeeds_for_signing_in_to_existing_active_account(self, _mock_segment_track):
@@ -795,7 +810,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
self.assert_redirect_after_pipeline_completes(
self.do_complete(strategy, get_request, partial_pipeline_token, partial_data, user)
)
self.assert_account_settings_context_looks_correct(account_settings_context(get_request))
self.assert_third_party_accounts_state(get_request)
def test_signin_fails_if_account_not_active(self):
_, strategy = self.get_request_and_strategy(
@@ -937,7 +952,7 @@ class IntegrationTest(testutil.TestCase, test.TestCase, HelperMixin):
)
# Now the user has been redirected to the dashboard. Their third party account should now be linked.
self.assert_social_auth_exists_for_user(created_user, strategy)
self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True)
self.assert_third_party_accounts_state(request, linked=True)
def test_new_account_registration_assigns_distinct_username_on_collision(self):
original_username = self.get_username()

View File

@@ -27,7 +27,6 @@ from common.djangoapps.third_party_auth.saml import SapSuccessFactorsIdentityPro
from common.djangoapps.third_party_auth.saml import log as saml_log
from common.djangoapps.third_party_auth.tasks import fetch_saml_metadata
from common.djangoapps.third_party_auth.tests import testutil, utils
from openedx.core.djangoapps.user_api.accounts.settings_views import account_settings_context
from openedx.core.djangoapps.user_authn.views.login import login_user
from openedx.features.enterprise_support.tests.factories import EnterpriseCustomerFactory
@@ -239,12 +238,10 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin
}
@patch('openedx.features.enterprise_support.api.enterprise_customer_for_request')
@patch('openedx.core.djangoapps.user_api.accounts.settings_views.enterprise_customer_for_request')
@patch('openedx.features.enterprise_support.utils.third_party_auth.provider.Registry.get')
def test_full_pipeline_succeeds_for_unlinking_testshib_account(
self,
mock_auth_provider,
mock_enterprise_customer_for_request_settings_view,
mock_enterprise_customer_for_request,
):
@@ -284,7 +281,6 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin
}
mock_auth_provider.return_value.backend_name = 'tpa-saml'
mock_enterprise_customer_for_request.return_value = enterprise_customer_data
mock_enterprise_customer_for_request_settings_view.return_value = enterprise_customer_data
# Instrument the pipeline to get to the dashboard with the full expected state.
self.client.get(
@@ -299,7 +295,7 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin
request=request)
# First we expect that we're in the linked state, with a backend entry.
self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=True)
self.assert_third_party_accounts_state(request, linked=True)
self.assert_social_auth_exists_for_user(request.user, strategy)
FEATURES_WITH_ENTERPRISE_ENABLED = settings.FEATURES.copy()
@@ -327,7 +323,7 @@ class TestShibIntegrationTest(SamlIntegrationTestUtilities, IntegrationTestMixin
)
)
# Now we expect to be in the unlinked state, with no backend entry.
self.assert_account_settings_context_looks_correct(account_settings_context(request), linked=False)
self.assert_third_party_accounts_state(request, linked=False)
self.assert_social_auth_does_not_exist_for_user(user, strategy)
assert EnterpriseCustomerUser.objects\
.filter(enterprise_customer=enterprise_customer, user_id=user.id).count() == 0

View File

@@ -159,10 +159,6 @@ segment:
djangojs-partial.po:
djangojs-studio.po:
- cms/*
djangojs-account-settings-view.po:
- lms/static/js/student_account/views/account_settings_view.js
# Segregating student account settings view strings, so that beta language message
# can be translated for wide set of partially supported languages.
mako.po:
mako-studio.po:
- cms/*

View File

@@ -129,7 +129,7 @@ def _get_course_email_context(course):
'course_url': course_url,
'course_image_url': image_url,
'course_end_date': course_end_date,
'account_settings_url': '{}{}'.format(lms_root_url, reverse('account_settings')),
'account_settings_url': settings.ACCOUNT_MICROFRONTEND_URL,
'email_settings_url': '{}{}'.format(lms_root_url, reverse('dashboard')),
'logo_url': get_logo_url_for_email(),
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),

View File

@@ -743,7 +743,7 @@ class TestCourseEmailContext(SharedModuleStoreTestCase):
assert email_context['course_image_url'] == \
f'{scheme}://edx.org/asset-v1:{course_id_fragment}+type@asset+block@images_course_image.jpg'
assert email_context['email_settings_url'] == f'{scheme}://edx.org/dashboard'
assert email_context['account_settings_url'] == f'{scheme}://edx.org/account/settings'
assert email_context['account_settings_url'] == settings.ACCOUNT_MICROFRONTEND_URL
@override_settings(LMS_ROOT_URL="http://edx.org")
def test_insecure_email_context(self):

View File

@@ -1996,7 +1996,7 @@ def financial_assistance_form(request, course_id=None):
'header_text': _get_fa_header(FINANCIAL_ASSISTANCE_HEADER),
'student_faq_url': marketing_link('FAQ'),
'dashboard_url': reverse('dashboard'),
'account_settings_url': reverse('account_settings'),
'account_settings_url': settings.ACCOUNT_MICROFRONTEND_URL,
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
'user_details': {
'email': user.email,

View File

@@ -4,6 +4,7 @@ Views handling read (GET) requests for the Discussion tab and inline discussions
import logging
from functools import wraps
from urllib.parse import urljoin
from django.conf import settings
from django.contrib.auth import get_user_model
@@ -629,7 +630,7 @@ def create_user_profile_context(request, course_key, user_id):
'page': query_params['page'],
'num_pages': query_params['num_pages'],
'sort_preference': user.default_sort_key,
'learner_profile_page_url': reverse('learner_profile', kwargs={'username': django_user.username}),
'learner_profile_page_url': urljoin(settings.PROFILE_MICROFRONTEND_URL, f'/u/{django_user.username}'),
})
return context

View File

@@ -3196,7 +3196,6 @@ INSTALLED_APPS = [
'openedx.features.course_bookmarks',
'openedx.features.course_experience',
'openedx.features.enterprise_support.apps.EnterpriseSupportConfig',
'openedx.features.learner_profile',
'openedx.features.course_duration_limits',
'openedx.features.content_type_gating',
'openedx.features.discounts',

View File

@@ -375,6 +375,7 @@ EDXNOTES_CLIENT_NAME = 'edx_notes_api-backend-service'
############## Settings for Microfrontends #########################
LEARNING_MICROFRONTEND_URL = 'http://localhost:2000'
ACCOUNT_MICROFRONTEND_URL = 'http://localhost:1997'
PROFILE_MICROFRONTEND_URL = 'http://localhost:1995'
COMMUNICATIONS_MICROFRONTEND_URL = 'http://localhost:1984'
AUTHN_MICROFRONTEND_URL = 'http://localhost:1999'
AUTHN_MICROFRONTEND_DOMAIN = 'localhost:1999'

View File

@@ -587,7 +587,7 @@ PDF_RECEIPT_BILLING_ADDRESS = 'add your own billing address here with appropriat
PDF_RECEIPT_TERMS_AND_CONDITIONS = 'add your own terms and conditions'
PDF_RECEIPT_TAX_ID_LABEL = 'Tax ID'
PROFILE_MICROFRONTEND_URL = "http://profile-mfe/abc/"
PROFILE_MICROFRONTEND_URL = "http://profile-mfe"
ORDER_HISTORY_MICROFRONTEND_URL = "http://order-history-mfe/"
ACCOUNT_MICROFRONTEND_URL = "http://account-mfe"
AUTHN_MICROFRONTEND_URL = "http://authn-mfe"

View File

@@ -1,334 +0,0 @@
define(['backbone',
'jquery',
'underscore',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/spec/views/fields_helpers',
'js/spec/student_account/helpers',
'js/spec/student_account/account_settings_fields_helpers',
'js/student_account/views/account_settings_factory',
'js/student_account/views/account_settings_view'
],
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViewsSpecHelpers, Helpers,
AccountSettingsFieldViewSpecHelpers, AccountSettingsPage) {
'use strict';
describe('edx.user.AccountSettingsFactory', function() {
var createAccountSettingsPage = function() {
var context = AccountSettingsPage(
Helpers.FIELDS_DATA,
false,
[],
Helpers.AUTH_DATA,
Helpers.PASSWORD_RESET_SUPPORT_LINK,
Helpers.USER_ACCOUNTS_API_URL,
Helpers.USER_PREFERENCES_API_URL,
1,
Helpers.PLATFORM_NAME,
Helpers.CONTACT_EMAIL,
true,
Helpers.ENABLE_COPPA_COMPLIANCE
);
return context.accountSettingsView;
};
var requests;
beforeEach(function() {
setFixtures('<div class="wrapper-account-settings"></div>');
});
it('shows loading error when UserAccountModel fails to load', function() {
requests = AjaxHelpers.requests(this);
var accountSettingsView = createAccountSettingsPage();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
var request = requests[0];
expect(request.method).toBe('GET');
expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL);
AjaxHelpers.respondWithError(requests, 500);
Helpers.expectLoadingErrorIsVisible(accountSettingsView, true);
});
it('shows loading error when UserPreferencesModel fails to load', function() {
requests = AjaxHelpers.requests(this);
var accountSettingsView = createAccountSettingsPage();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
var request = requests[0];
expect(request.method).toBe('GET');
expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL);
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
request = requests[1];
expect(request.method).toBe('GET');
expect(request.url).toBe('/api/user/v1/preferences/time_zones/?country_code=1');
AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE);
request = requests[2];
expect(request.method).toBe('GET');
expect(request.url).toBe(Helpers.USER_PREFERENCES_API_URL);
AjaxHelpers.respondWithError(requests, 500);
Helpers.expectLoadingErrorIsVisible(accountSettingsView, true);
});
it('renders fields after the models are successfully fetched', function() {
requests = AjaxHelpers.requests(this);
var accountSettingsView = createAccountSettingsPage();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE);
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
accountSettingsView.render();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
Helpers.expectSettingsSectionsAndFieldsToBeRendered(accountSettingsView);
});
it('expects all fields to behave correctly', function() {
var i, view;
requests = AjaxHelpers.requests(this);
var accountSettingsView = createAccountSettingsPage();
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE);
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
AjaxHelpers.respondWithJson(requests, {}); // Page viewed analytics event
var sectionsData = accountSettingsView.options.tabSections.aboutTabSections;
expect(sectionsData[0].fields.length).toBe(7);
var textFields = [sectionsData[0].fields[1], sectionsData[0].fields[2]];
for (i = 0; i < textFields.length; i++) {
view = textFields[i].view;
FieldViewsSpecHelpers.verifyTextField(view, {
title: view.options.title,
valueAttribute: view.options.valueAttribute,
helpMessage: view.options.helpMessage,
validValue: 'My Name',
invalidValue1: '',
invalidValue2: '@',
validationError: 'Think again!',
defaultValue: ''
}, requests);
}
expect(sectionsData[1].fields.length).toBe(4);
var dropdownFields = [
sectionsData[1].fields[0],
sectionsData[1].fields[1],
sectionsData[1].fields[2]
];
_.each(dropdownFields, function(field) {
var view = field.view;
FieldViewsSpecHelpers.verifyDropDownField(view, {
title: view.options.title,
valueAttribute: view.options.valueAttribute,
helpMessage: '',
validValue: Helpers.FIELD_OPTIONS[1][0],
invalidValue1: Helpers.FIELD_OPTIONS[2][0],
invalidValue2: Helpers.FIELD_OPTIONS[3][0],
validationError: 'Nope, this will not do!',
defaultValue: null
}, requests);
});
});
});
describe('edx.user.AccountSettingsFactory', function() {
var createEnterpriseLearnerAccountSettingsPage = function() {
var context = AccountSettingsPage(
Helpers.FIELDS_DATA,
false,
[],
Helpers.AUTH_DATA,
Helpers.PASSWORD_RESET_SUPPORT_LINK,
Helpers.USER_ACCOUNTS_API_URL,
Helpers.USER_PREFERENCES_API_URL,
1,
Helpers.PLATFORM_NAME,
Helpers.CONTACT_EMAIL,
true,
Helpers.ENABLE_COPPA_COMPLIANCE,
'',
Helpers.SYNC_LEARNER_PROFILE_DATA,
Helpers.ENTERPRISE_NAME,
Helpers.ENTERPRISE_READ_ONLY_ACCOUNT_FIELDS,
Helpers.EDX_SUPPORT_URL
);
return context.accountSettingsView;
};
var requests;
var accountInfoTab = {
BASIC_ACCOUNT_INFORMATION: 0,
ADDITIONAL_INFORMATION: 1
};
var basicAccountInfoFields = {
USERNAME: 0,
FULL_NAME: 1,
EMAIL_ADDRESS: 2,
PASSWORD: 3,
LANGUAGE: 4,
COUNTRY: 5,
TIMEZONE: 6
};
var additionalInfoFields = {
EDUCATION: 0,
GENDER: 1,
YEAR_OF_BIRTH: 2,
PREFERRED_LANGUAGE: 3
};
beforeEach(function() {
setFixtures('<div class="wrapper-account-settings"></div>');
});
it('shows loading error when UserAccountModel fails to load for enterprise learners', function() {
var accountSettingsView, request;
requests = AjaxHelpers.requests(this);
accountSettingsView = createEnterpriseLearnerAccountSettingsPage();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
request = requests[0];
expect(request.method).toBe('GET');
expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL);
AjaxHelpers.respondWithError(requests, 500);
Helpers.expectLoadingErrorIsVisible(accountSettingsView, true);
});
it('shows loading error when UserPreferencesModel fails to load for enterprise learners', function() {
var accountSettingsView, request;
requests = AjaxHelpers.requests(this);
accountSettingsView = createEnterpriseLearnerAccountSettingsPage();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
request = requests[0];
expect(request.method).toBe('GET');
expect(request.url).toBe(Helpers.USER_ACCOUNTS_API_URL);
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
request = requests[1];
expect(request.method).toBe('GET');
expect(request.url).toBe('/api/user/v1/preferences/time_zones/?country_code=1');
AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE);
request = requests[2];
expect(request.method).toBe('GET');
expect(request.url).toBe(Helpers.USER_PREFERENCES_API_URL);
AjaxHelpers.respondWithError(requests, 500);
Helpers.expectLoadingErrorIsVisible(accountSettingsView, true);
});
it('renders fields after the models are successfully fetched for enterprise learners', function() {
var accountSettingsView;
requests = AjaxHelpers.requests(this);
accountSettingsView = createEnterpriseLearnerAccountSettingsPage();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE);
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
accountSettingsView.render();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
Helpers.expectSettingsSectionsAndFieldsToBeRenderedWithMessage(accountSettingsView);
});
it('expects all fields to behave correctly for enterprise learners', function() {
var accountSettingsView, i, view, sectionsData, textFields, dropdownFields;
requests = AjaxHelpers.requests(this);
accountSettingsView = createEnterpriseLearnerAccountSettingsPage();
AjaxHelpers.respondWithJson(requests, Helpers.createAccountSettingsData());
AjaxHelpers.respondWithJson(requests, Helpers.TIME_ZONE_RESPONSE);
AjaxHelpers.respondWithJson(requests, Helpers.createUserPreferencesData());
AjaxHelpers.respondWithJson(requests, {}); // Page viewed analytics event
sectionsData = accountSettingsView.options.tabSections.aboutTabSections;
expect(sectionsData[accountInfoTab.BASIC_ACCOUNT_INFORMATION].fields.length).toBe(7);
// Verify that username, name and email fields are readonly
textFields = [
sectionsData[accountInfoTab.BASIC_ACCOUNT_INFORMATION].fields[basicAccountInfoFields.USERNAME],
sectionsData[accountInfoTab.BASIC_ACCOUNT_INFORMATION].fields[basicAccountInfoFields.FULL_NAME],
sectionsData[accountInfoTab.BASIC_ACCOUNT_INFORMATION].fields[basicAccountInfoFields.EMAIL_ADDRESS]
];
for (i = 0; i < textFields.length; i++) {
view = textFields[i].view;
FieldViewsSpecHelpers.verifyReadonlyTextField(view, {
title: view.options.title,
valueAttribute: view.options.valueAttribute,
helpMessage: view.options.helpMessage,
validValue: 'My Name',
defaultValue: ''
}, requests);
}
// Verify un-editable country dropdown field
view = sectionsData[
accountInfoTab.BASIC_ACCOUNT_INFORMATION
].fields[basicAccountInfoFields.COUNTRY].view;
FieldViewsSpecHelpers.verifyReadonlyDropDownField(view, {
title: view.options.title,
valueAttribute: view.options.valueAttribute,
helpMessage: '',
validValue: Helpers.FIELD_OPTIONS[1][0],
editable: 'never',
defaultValue: null
});
expect(sectionsData[accountInfoTab.ADDITIONAL_INFORMATION].fields.length).toBe(4);
dropdownFields = [
sectionsData[accountInfoTab.ADDITIONAL_INFORMATION].fields[additionalInfoFields.EDUCATION],
sectionsData[accountInfoTab.ADDITIONAL_INFORMATION].fields[additionalInfoFields.GENDER],
sectionsData[accountInfoTab.ADDITIONAL_INFORMATION].fields[additionalInfoFields.YEAR_OF_BIRTH]
];
_.each(dropdownFields, function(field) {
view = field.view;
FieldViewsSpecHelpers.verifyDropDownField(view, {
title: view.options.title,
valueAttribute: view.options.valueAttribute,
helpMessage: '',
validValue: Helpers.FIELD_OPTIONS[1][0], // dummy option for dropdown field
invalidValue1: Helpers.FIELD_OPTIONS[2][0], // dummy option for dropdown field
invalidValue2: Helpers.FIELD_OPTIONS[3][0], // dummy option for dropdown field
validationError: 'Nope, this will not do!',
defaultValue: null
}, requests);
});
});
});
});

View File

@@ -1,34 +0,0 @@
define(['backbone',
'jquery',
'underscore',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/spec/views/fields_helpers',
'string_utils'],
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, FieldViewsSpecHelpers) {
'use strict';
var verifyAuthField = function(view, data, requests) {
var selector = '.u-field-value .u-field-link-title-' + view.options.valueAttribute;
spyOn(view, 'redirect_to');
FieldViewsSpecHelpers.expectTitleAndMessageToContain(view, data.title, data.helpMessage);
expect(view.$(selector).text().trim()).toBe('Unlink This Account');
view.$(selector).click();
FieldViewsSpecHelpers.expectMessageContains(view, 'Unlinking');
AjaxHelpers.expectRequest(requests, 'POST', data.disconnectUrl);
AjaxHelpers.respondWithNoContent(requests);
expect(view.$(selector).text().trim()).toBe('Link Your Account');
FieldViewsSpecHelpers.expectMessageContains(view, 'Successfully unlinked.');
view.$(selector).click();
FieldViewsSpecHelpers.expectMessageContains(view, 'Linking');
expect(view.redirect_to).toHaveBeenCalledWith(data.connectUrl);
};
return {
verifyAuthField: verifyAuthField
};
});

View File

@@ -1,216 +0,0 @@
define(['backbone',
'jquery',
'underscore',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/student_account/models/user_account_model',
'js/views/fields',
'js/spec/views/fields_helpers',
'js/spec/student_account/account_settings_fields_helpers',
'js/student_account/views/account_settings_fields',
'js/student_account/models/user_account_model',
'string_utils'],
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, UserAccountModel, FieldViews, FieldViewsSpecHelpers,
AccountSettingsFieldViewSpecHelpers, AccountSettingsFieldViews) {
'use strict';
describe('edx.AccountSettingsFieldViews', function() {
var requests,
timerCallback, // eslint-disable-line no-unused-vars
data;
beforeEach(function() {
timerCallback = jasmine.createSpy('timerCallback');
jasmine.clock().install();
});
afterEach(function() {
jasmine.clock().uninstall();
});
it('sends request to reset password on clicking link in PasswordFieldView', function() {
requests = AjaxHelpers.requests(this);
var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.PasswordFieldView, {
linkHref: '/password_reset',
emailAttribute: 'email',
valueAttribute: 'password'
});
var view = new AccountSettingsFieldViews.PasswordFieldView(fieldData).render();
expect(view.$('.u-field-value > button').is(':disabled')).toBe(false);
view.$('.u-field-value > button').click();
expect(view.$('.u-field-value > button').is(':disabled')).toBe(true);
AjaxHelpers.expectRequest(requests, 'POST', '/password_reset', 'email=legolas%40woodland.middlearth');
AjaxHelpers.respondWithJson(requests, {success: 'true'});
FieldViewsSpecHelpers.expectMessageContains(
view,
"We've sent a message to legolas@woodland.middlearth. " +
'Click the link in the message to reset your password.'
);
});
it('update time zone dropdown after country dropdown changes', function() {
var baseSelector = '.u-field-value > select';
var groupsSelector = baseSelector + '> optgroup';
var groupOptionsSelector = groupsSelector + '> option';
var timeZoneData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.TimeZoneFieldView, {
valueAttribute: 'time_zone',
groupOptions: [{
groupTitle: gettext('All Time Zones'),
selectOptions: FieldViewsSpecHelpers.SELECT_OPTIONS,
nullValueOptionLabel: 'Default (Local Time Zone)'
}],
persistChanges: true,
required: true
});
var countryData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, {
valueAttribute: 'country',
options: [['KY', 'Cayman Islands'], ['CA', 'Canada'], ['GY', 'Guyana']],
persistChanges: true
});
var countryChange = {country: 'GY'};
var timeZoneChange = {time_zone: 'Pacific/Kosrae'};
var timeZoneView = new AccountSettingsFieldViews.TimeZoneFieldView(timeZoneData).render();
var countryView = new AccountSettingsFieldViews.DropdownFieldView(countryData).render();
requests = AjaxHelpers.requests(this);
timeZoneView.listenToCountryView(countryView);
// expect time zone dropdown to have single subheader ('All Time Zones')
expect(timeZoneView.$(groupsSelector).length).toBe(1);
expect(timeZoneView.$(groupOptionsSelector).length).toBe(3);
expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe(FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]);
// change country
countryView.$(baseSelector).val(countryChange[countryData.valueAttribute]).change();
countryView.$(baseSelector).focusout();
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, countryChange);
AjaxHelpers.respondWithJson(requests, {success: 'true'});
AjaxHelpers.expectRequest(
requests,
'GET',
'/api/user/v1/preferences/time_zones/?country_code=GY'
);
AjaxHelpers.respondWithJson(requests, [
{time_zone: 'America/Guyana', description: 'America/Guyana (ECT, UTC-0500)'},
{time_zone: 'Pacific/Kosrae', description: 'Pacific/Kosrae (KOST, UTC+1100)'}
]);
// expect time zone dropdown to have two subheaders (country/all time zone sub-headers) with new values
expect(timeZoneView.$(groupsSelector).length).toBe(2);
expect(timeZoneView.$(groupOptionsSelector).length).toBe(6);
expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe('America/Guyana');
// select time zone option from option
timeZoneView.$(baseSelector).val(timeZoneChange[timeZoneData.valueAttribute]).change();
timeZoneView.$(baseSelector).focusout();
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, timeZoneChange);
AjaxHelpers.respondWithJson(requests, {success: 'true'});
timeZoneView.render();
// expect time zone dropdown to have three subheaders (currently selected/country/all time zones)
expect(timeZoneView.$(groupsSelector).length).toBe(3);
expect(timeZoneView.$(groupOptionsSelector).length).toBe(6);
expect(timeZoneView.$(groupOptionsSelector)[0].value).toBe('Pacific/Kosrae');
});
it('sends request to /i18n/setlang/ after changing language in LanguagePreferenceFieldView', function() {
requests = AjaxHelpers.requests(this);
var selector = '.u-field-value > select';
var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, {
valueAttribute: 'language',
options: FieldViewsSpecHelpers.SELECT_OPTIONS,
persistChanges: true
});
var view = new AccountSettingsFieldViews.LanguagePreferenceFieldView(fieldData).render();
data = {language: FieldViewsSpecHelpers.SELECT_OPTIONS[2][0]};
view.$(selector).val(data[fieldData.valueAttribute]).change();
view.$(selector).focusout();
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data);
AjaxHelpers.respondWithNoContent(requests);
AjaxHelpers.expectRequest(
requests,
'POST',
'/i18n/setlang/',
$.param({
language: data[fieldData.valueAttribute],
next: window.location.href
})
);
// Django will actually respond with a 302 redirect, but that would cause a page load during these
// unittests. 204 should work fine for testing.
AjaxHelpers.respondWithNoContent(requests);
FieldViewsSpecHelpers.expectMessageContains(view, 'Your changes have been saved.');
data = {language: FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]};
view.$(selector).val(data[fieldData.valueAttribute]).change();
view.$(selector).focusout();
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data);
AjaxHelpers.respondWithNoContent(requests);
AjaxHelpers.expectRequest(
requests,
'POST',
'/i18n/setlang/',
$.param({
language: data[fieldData.valueAttribute],
next: window.location.href
})
);
AjaxHelpers.respondWithError(requests, 500);
FieldViewsSpecHelpers.expectMessageContains(
view,
'You must sign out and sign back in before your language changes take effect.'
);
});
it('reads and saves the value correctly for LanguageProficienciesFieldView', function() {
requests = AjaxHelpers.requests(this);
var selector = '.u-field-value > select';
var fieldData = FieldViewsSpecHelpers.createFieldData(AccountSettingsFieldViews.DropdownFieldView, {
valueAttribute: 'language_proficiencies',
options: FieldViewsSpecHelpers.SELECT_OPTIONS,
persistChanges: true
});
fieldData.model.set({language_proficiencies: [{code: FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]}]});
var view = new AccountSettingsFieldViews.LanguageProficienciesFieldView(fieldData).render();
expect(view.modelValue()).toBe(FieldViewsSpecHelpers.SELECT_OPTIONS[0][0]);
data = {language_proficiencies: [{code: FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]}]};
view.$(selector).val(FieldViewsSpecHelpers.SELECT_OPTIONS[1][0]).change();
view.$(selector).focusout();
FieldViewsSpecHelpers.expectAjaxRequestWithData(requests, data);
AjaxHelpers.respondWithNoContent(requests);
});
it('correctly links and unlinks from AuthFieldView', function() {
requests = AjaxHelpers.requests(this);
var fieldData = FieldViewsSpecHelpers.createFieldData(FieldViews.LinkFieldView, {
title: 'Yet another social network',
helpMessage: '',
valueAttribute: 'auth-yet-another',
connected: true,
acceptsLogins: 'true',
connectUrl: 'yetanother.com/auth/connect',
disconnectUrl: 'yetanother.com/auth/disconnect'
});
var view = new AccountSettingsFieldViews.AuthFieldView(fieldData).render();
AccountSettingsFieldViewSpecHelpers.verifyAuthField(view, fieldData, requests);
});
});
});

View File

@@ -1,91 +0,0 @@
define(['backbone',
'jquery',
'underscore',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/spec/student_account/helpers',
'js/views/fields',
'js/student_account/models/user_account_model',
'js/student_account/views/account_settings_view'
],
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, FieldViews, UserAccountModel,
AccountSettingsView) {
'use strict';
describe('edx.user.AccountSettingsView', function() {
var createAccountSettingsView = function() {
var model = new UserAccountModel();
model.set(Helpers.createAccountSettingsData());
var aboutSectionsData = [
{
title: 'Basic Account Information',
messageType: 'info',
message: 'Your profile settings are managed by Test Enterprise. ' +
'Contact your administrator or <a href="https://support.edx.org/">edX Support</a> for help.',
fields: [
{
view: new FieldViews.ReadonlyFieldView({
model: model,
title: 'Username',
valueAttribute: 'username'
})
},
{
view: new FieldViews.TextFieldView({
model: model,
title: 'Full Name',
valueAttribute: 'name'
})
}
]
},
{
title: 'Additional Information',
fields: [
{
view: new FieldViews.DropdownFieldView({
model: model,
title: 'Education Completed',
valueAttribute: 'level_of_education',
options: Helpers.FIELD_OPTIONS
})
}
]
}
];
var accountSettingsView = new AccountSettingsView({
el: $('.wrapper-account-settings'),
model: model,
tabSections: {
aboutTabSections: aboutSectionsData
}
});
return accountSettingsView;
};
beforeEach(function() {
setFixtures('<div class="wrapper-account-settings"></div>');
});
it('shows loading error correctly', function() {
var accountSettingsView = createAccountSettingsView();
accountSettingsView.render();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
accountSettingsView.showLoadingError();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, true);
});
it('renders all fields as expected', function() {
var accountSettingsView = createAccountSettingsView();
accountSettingsView.render();
Helpers.expectLoadingErrorIsVisible(accountSettingsView, false);
Helpers.expectSettingsSectionsAndFieldsToBeRendered(accountSettingsView);
});
});
});

View File

@@ -1,46 +0,0 @@
(function(define, undefined) {
'use strict';
define([
'gettext',
'jquery',
'underscore',
'backbone',
'edx-ui-toolkit/js/utils/html-utils',
'text!templates/student_account/account_settings_section.underscore'
], function(gettext, $, _, Backbone, HtmlUtils, sectionTemplate) {
var AccountSectionView = Backbone.View.extend({
initialize: function(options) {
this.options = options;
_.bindAll(this, 'render', 'renderFields');
},
render: function() {
HtmlUtils.setHtml(
this.$el,
HtmlUtils.template(sectionTemplate)({
HtmlUtils: HtmlUtils,
sections: this.options.sections,
tabName: this.options.tabName,
tabLabel: this.options.tabLabel
})
);
this.renderFields();
},
renderFields: function() {
var view = this;
_.each(view.$('.' + view.options.tabName + '-section-body'), function(sectionEl, index) {
_.each(view.options.sections[index].fields, function(field) {
$(sectionEl).append(field.view.render().el);
});
});
return this;
}
});
return AccountSectionView;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,491 +0,0 @@
(function(define, undefined) {
'use strict';
define([
'gettext', 'jquery', 'underscore', 'backbone', 'logger',
'js/student_account/models/user_account_model',
'js/student_account/models/user_preferences_model',
'js/student_account/views/account_settings_fields',
'js/student_account/views/account_settings_view',
'edx-ui-toolkit/js/utils/string-utils',
'edx-ui-toolkit/js/utils/html-utils'
], function(gettext, $, _, Backbone, Logger, UserAccountModel, UserPreferencesModel,
AccountSettingsFieldViews, AccountSettingsView, StringUtils, HtmlUtils) {
return function(
fieldsData,
disableOrderHistoryTab,
ordersHistoryData,
authData,
passwordResetSupportUrl,
userAccountsApiUrl,
userPreferencesApiUrl,
accountUserId,
platformName,
contactEmail,
allowEmailChange,
enableCoppaCompliance,
socialPlatforms,
syncLearnerProfileData,
enterpriseName,
enterpriseReadonlyAccountFields,
edxSupportUrl,
extendedProfileFields,
displayAccountDeletion,
isSecondaryEmailFeatureEnabled,
betaLanguage
) {
var $accountSettingsElement, userAccountModel, userPreferencesModel, aboutSectionsData,
accountsSectionData, ordersSectionData, accountSettingsView, showAccountSettingsPage,
showLoadingError, orderNumber, getUserField, userFields, timeZoneDropdownField, countryDropdownField,
emailFieldView, secondaryEmailFieldView, socialFields, accountDeletionFields, platformData,
aboutSectionMessageType, aboutSectionMessage, fullnameFieldView, countryFieldView,
fullNameFieldData, emailFieldData, secondaryEmailFieldData, countryFieldData, additionalFields,
fieldItem, emailFieldViewIndex, focusId, yearOfBirthViewIndex, levelOfEducationFieldData,
tabIndex = 0;
$accountSettingsElement = $('.wrapper-account-settings');
userAccountModel = new UserAccountModel();
userAccountModel.url = userAccountsApiUrl;
userPreferencesModel = new UserPreferencesModel();
userPreferencesModel.url = userPreferencesApiUrl;
if (syncLearnerProfileData && enterpriseName) {
aboutSectionMessageType = 'info';
aboutSectionMessage = HtmlUtils.interpolateHtml(
gettext('Your profile settings are managed by {enterprise_name}. Contact your administrator or {link_start}edX Support{link_end} for help.'), // eslint-disable-line max-len
{
enterprise_name: enterpriseName,
link_start: HtmlUtils.HTML(
StringUtils.interpolate(
'<a href="{edx_support_url}">', {
edx_support_url: edxSupportUrl
}
)
),
link_end: HtmlUtils.HTML('</a>')
}
);
}
emailFieldData = {
model: userAccountModel,
title: gettext('Email Address (Sign In)'),
valueAttribute: 'email',
helpMessage: StringUtils.interpolate(
gettext('You receive messages from {platform_name} and course teams at this address.'), // eslint-disable-line max-len
{platform_name: platformName}
),
persistChanges: true
};
if (!allowEmailChange || (syncLearnerProfileData && enterpriseReadonlyAccountFields.fields.indexOf('email') !== -1)) { // eslint-disable-line max-len
emailFieldView = {
view: new AccountSettingsFieldViews.ReadonlyFieldView(emailFieldData)
};
} else {
emailFieldView = {
view: new AccountSettingsFieldViews.EmailFieldView(emailFieldData)
};
}
secondaryEmailFieldData = {
model: userAccountModel,
title: gettext('Recovery Email Address'),
valueAttribute: 'secondary_email',
helpMessage: gettext('You may access your account with this address if single-sign on or access to your primary email is not available.'), // eslint-disable-line max-len
persistChanges: true
};
fullNameFieldData = {
model: userAccountModel,
title: gettext('Full Name'),
valueAttribute: 'name',
helpMessage: gettext('The name that is used for ID verification and that appears on your certificates.'), // eslint-disable-line max-len,
persistChanges: true
};
if (syncLearnerProfileData && enterpriseReadonlyAccountFields.fields.indexOf('name') !== -1) {
fullnameFieldView = {
view: new AccountSettingsFieldViews.ReadonlyFieldView(fullNameFieldData)
};
} else {
fullnameFieldView = {
view: new AccountSettingsFieldViews.TextFieldView(fullNameFieldData)
};
}
countryFieldData = {
model: userAccountModel,
required: true,
title: gettext('Country or Region of Residence'),
valueAttribute: 'country',
options: fieldsData.country.options,
persistChanges: true,
helpMessage: gettext('The country or region where you live.')
};
if (syncLearnerProfileData && enterpriseReadonlyAccountFields.fields.indexOf('country') !== -1) {
countryFieldData.editable = 'never';
countryFieldView = {
view: new AccountSettingsFieldViews.DropdownFieldView(
countryFieldData
)
};
} else {
countryFieldView = {
view: new AccountSettingsFieldViews.DropdownFieldView(countryFieldData)
};
}
levelOfEducationFieldData = fieldsData.level_of_education.options;
if (enableCoppaCompliance) {
levelOfEducationFieldData = levelOfEducationFieldData.filter(option => option[0] !== 'el');
}
aboutSectionsData = [
{
title: gettext('Basic Account Information'),
subtitle: gettext('These settings include basic information about your account.'),
messageType: aboutSectionMessageType,
message: aboutSectionMessage,
fields: [
{
view: new AccountSettingsFieldViews.ReadonlyFieldView({
model: userAccountModel,
title: gettext('Username'),
valueAttribute: 'username',
helpMessage: StringUtils.interpolate(
gettext('The name that identifies you on {platform_name}. You cannot change your username.'), // eslint-disable-line max-len
{platform_name: platformName}
)
})
},
fullnameFieldView,
emailFieldView,
{
view: new AccountSettingsFieldViews.PasswordFieldView({
model: userAccountModel,
title: gettext('Password'),
screenReaderTitle: gettext('Reset Your Password'),
valueAttribute: 'password',
emailAttribute: 'email',
passwordResetSupportUrl: passwordResetSupportUrl,
linkTitle: gettext('Reset Your Password'),
linkHref: fieldsData.password.url,
helpMessage: gettext('Check your email account for instructions to reset your password.') // eslint-disable-line max-len
})
},
{
view: new AccountSettingsFieldViews.LanguagePreferenceFieldView({
model: userPreferencesModel,
title: gettext('Language'),
valueAttribute: 'pref-lang',
required: true,
refreshPageOnSave: true,
helpMessage: StringUtils.interpolate(
gettext('The language used throughout this site. This site is currently available in a limited number of languages. Changing the value of this field will cause the page to refresh.'), // eslint-disable-line max-len
{platform_name: platformName}
),
options: fieldsData.language.options,
persistChanges: true,
focusNextID: '#u-field-select-country'
})
},
countryFieldView,
{
view: new AccountSettingsFieldViews.TimeZoneFieldView({
model: userPreferencesModel,
required: true,
title: gettext('Time Zone'),
valueAttribute: 'time_zone',
helpMessage: gettext('Select the time zone for displaying course dates. If you do not specify a time zone, course dates, including assignment deadlines, will be displayed in your browser\'s local time zone.'), // eslint-disable-line max-len
groupOptions: [{
groupTitle: gettext('All Time Zones'),
selectOptions: fieldsData.time_zone.options,
nullValueOptionLabel: gettext('Default (Local Time Zone)')
}],
persistChanges: true
})
}
]
},
{
title: gettext('Additional Information'),
fields: [
{
view: new AccountSettingsFieldViews.DropdownFieldView({
model: userAccountModel,
title: gettext('Education Completed'),
valueAttribute: 'level_of_education',
options: levelOfEducationFieldData,
persistChanges: true
})
},
{
view: new AccountSettingsFieldViews.DropdownFieldView({
model: userAccountModel,
title: gettext('Gender'),
valueAttribute: 'gender',
options: fieldsData.gender.options,
persistChanges: true
})
},
{
view: new AccountSettingsFieldViews.DropdownFieldView({
model: userAccountModel,
title: gettext('Year of Birth'),
valueAttribute: 'year_of_birth',
options: fieldsData.year_of_birth.options,
persistChanges: true
})
},
{
view: new AccountSettingsFieldViews.LanguageProficienciesFieldView({
model: userAccountModel,
title: gettext('Preferred Language'),
valueAttribute: 'language_proficiencies',
options: fieldsData.preferred_language.options,
persistChanges: true
})
}
]
}
];
if (enableCoppaCompliance){
yearOfBirthViewIndex = aboutSectionsData[1]['fields'].findIndex(function (field) {
return field['view']['options']['valueAttribute']=== 'year_of_birth';
});
aboutSectionsData[1]['fields'].splice(yearOfBirthViewIndex,1)
}
// Secondary email address
if (isSecondaryEmailFeatureEnabled) {
secondaryEmailFieldView = {
view: new AccountSettingsFieldViews.EmailFieldView(secondaryEmailFieldData),
successMessage: function() {
return HtmlUtils.joinHtml(
this.indicators.success,
StringUtils.interpolate(
gettext('We\'ve sent a confirmation message to {new_secondary_email_address}. Click the link in the message to update your secondary email address.'), // eslint-disable-line max-len
{
new_secondary_email_address: this.fieldValue()
}
)
);}
};
emailFieldViewIndex = aboutSectionsData[0].fields.indexOf(emailFieldView);
// Insert secondary email address after email address field.
aboutSectionsData[0].fields.splice(
emailFieldViewIndex + 1, 0, secondaryEmailFieldView
)
}
// Add the extended profile fields
additionalFields = aboutSectionsData[1];
for (var field in extendedProfileFields) { // eslint-disable-line guard-for-in, no-restricted-syntax, vars-on-top, max-len
fieldItem = extendedProfileFields[field];
if (fieldItem.field_type === 'TextField') {
additionalFields.fields.push({
view: new AccountSettingsFieldViews.ExtendedFieldTextFieldView({
model: userAccountModel,
title: fieldItem.field_label,
fieldName: fieldItem.field_name,
valueAttribute: 'extended_profile',
persistChanges: true
})
});
} else {
if (fieldItem.field_type === 'ListField') {
additionalFields.fields.push({
view: new AccountSettingsFieldViews.ExtendedFieldListFieldView({
model: userAccountModel,
title: fieldItem.field_label,
fieldName: fieldItem.field_name,
options: fieldItem.field_options,
valueAttribute: 'extended_profile',
persistChanges: true
})
});
}
}
}
// Add the social link fields
socialFields = {
title: gettext('Social Media Links'),
subtitle: gettext('Optionally, link your personal accounts to the social media icons on your edX profile.'), // eslint-disable-line max-len
fields: []
};
for (var socialPlatform in socialPlatforms) { // eslint-disable-line guard-for-in, no-restricted-syntax, vars-on-top, max-len
platformData = socialPlatforms[socialPlatform];
socialFields.fields.push(
{
view: new AccountSettingsFieldViews.SocialLinkTextFieldView({
model: userAccountModel,
title: StringUtils.interpolate(
gettext('{platform_display_name} Link'),
{platform_display_name: platformData.display_name}
),
valueAttribute: 'social_links',
helpMessage: StringUtils.interpolate(
gettext('Enter your {platform_display_name} username or the URL to your {platform_display_name} page. Delete the URL to remove the link.'), // eslint-disable-line max-len
{platform_display_name: platformData.display_name}
),
platform: socialPlatform,
persistChanges: true,
placeholder: platformData.example
})
}
);
}
aboutSectionsData.push(socialFields);
// Add account deletion fields
if (displayAccountDeletion) {
accountDeletionFields = {
title: gettext('Delete My Account'),
fields: [],
// Used so content can be rendered external to Backbone
domHookId: 'account-deletion-container'
};
aboutSectionsData.push(accountDeletionFields);
}
// set TimeZoneField to listen to CountryField
getUserField = function(list, search) {
return _.find(list, function(field) {
return field.view.options.valueAttribute === search;
}).view;
};
userFields = _.find(aboutSectionsData, function(section) {
return section.title === gettext('Basic Account Information');
}).fields;
timeZoneDropdownField = getUserField(userFields, 'time_zone');
countryDropdownField = getUserField(userFields, 'country');
timeZoneDropdownField.listenToCountryView(countryDropdownField);
accountsSectionData = [
{
title: gettext('Linked Accounts'),
subtitle: StringUtils.interpolate(
gettext('You can link your social media accounts to simplify signing in to {platform_name}.'),
{platform_name: platformName}
),
fields: _.map(authData.providers, function(provider) {
return {
view: new AccountSettingsFieldViews.AuthFieldView({
title: provider.name,
valueAttribute: 'auth-' + provider.id,
helpMessage: '',
connected: provider.connected,
connectUrl: provider.connect_url,
acceptsLogins: provider.accepts_logins,
disconnectUrl: provider.disconnect_url,
platformName: platformName
})
};
})
}
];
ordersHistoryData.unshift(
{
title: gettext('ORDER NAME'),
order_date: gettext('ORDER PLACED'),
price: gettext('TOTAL'),
number: gettext('ORDER NUMBER')
}
);
ordersSectionData = [
{
title: gettext('My Orders'),
subtitle: StringUtils.interpolate(
gettext('This page contains information about orders that you have placed with {platform_name}.'), // eslint-disable-line max-len
{platform_name: platformName}
),
fields: _.map(ordersHistoryData, function(order) {
orderNumber = order.number;
if (orderNumber === 'ORDER NUMBER') {
orderNumber = 'orderId';
}
return {
view: new AccountSettingsFieldViews.OrderHistoryFieldView({
totalPrice: order.price,
orderId: order.number,
orderDate: order.order_date,
receiptUrl: order.receipt_url,
valueAttribute: 'order-' + orderNumber,
lines: order.lines
})
};
})
}
];
accountSettingsView = new AccountSettingsView({
model: userAccountModel,
accountUserId: accountUserId,
el: $accountSettingsElement,
tabSections: {
aboutTabSections: aboutSectionsData,
accountsTabSections: accountsSectionData,
ordersTabSections: ordersSectionData
},
userPreferencesModel: userPreferencesModel,
disableOrderHistoryTab: disableOrderHistoryTab,
betaLanguage: betaLanguage
});
accountSettingsView.render();
focusId = $.cookie('focus_id');
if (focusId) {
if (~focusId.indexOf('beta-language')) {
tabIndex = -1;
// Scroll to top of selected element
$('html, body').animate({
scrollTop: $(focusId).offset().top
}, 'slow');
}
$(focusId).attr({tabindex: tabIndex}).focus();
// Deleting the cookie
document.cookie = 'focus_id=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/account;';
}
showAccountSettingsPage = function() {
// Record that the account settings page was viewed.
Logger.log('edx.user.settings.viewed', {
page: 'account',
visibility: null,
user_id: accountUserId
});
};
showLoadingError = function() {
accountSettingsView.showLoadingError();
};
userAccountModel.fetch({
success: function() {
// Fetch the user preferences model
userPreferencesModel.fetch({
success: showAccountSettingsPage,
error: showLoadingError
});
},
error: showLoadingError
});
return {
userAccountModel: userAccountModel,
userPreferencesModel: userPreferencesModel,
accountSettingsView: accountSettingsView
};
};
});
}).call(this, define || RequireJS.define);

View File

@@ -1,458 +0,0 @@
(function(define, undefined) {
'use strict';
define([
'gettext',
'jquery',
'underscore',
'backbone',
'js/views/fields',
'text!templates/fields/field_text_account.underscore',
'text!templates/fields/field_readonly_account.underscore',
'text!templates/fields/field_link_account.underscore',
'text!templates/fields/field_dropdown_account.underscore',
'text!templates/fields/field_social_link_account.underscore',
'text!templates/fields/field_order_history.underscore',
'edx-ui-toolkit/js/utils/string-utils',
'edx-ui-toolkit/js/utils/html-utils'
], function(
gettext, $, _, Backbone,
FieldViews,
field_text_account_template,
field_readonly_account_template,
field_link_account_template,
field_dropdown_account_template,
field_social_link_template,
field_order_history_template,
StringUtils,
HtmlUtils
) {
var AccountSettingsFieldViews = {
ReadonlyFieldView: FieldViews.ReadonlyFieldView.extend({
fieldTemplate: field_readonly_account_template
}),
TextFieldView: FieldViews.TextFieldView.extend({
fieldTemplate: field_text_account_template
}),
DropdownFieldView: FieldViews.DropdownFieldView.extend({
fieldTemplate: field_dropdown_account_template
}),
EmailFieldView: FieldViews.TextFieldView.extend({
fieldTemplate: field_text_account_template,
successMessage: function() {
return HtmlUtils.joinHtml(
this.indicators.success,
StringUtils.interpolate(
gettext('We\'ve sent a confirmation message to {new_email_address}. Click the link in the message to update your email address.'), // eslint-disable-line max-len
{new_email_address: this.fieldValue()}
)
);
}
}),
LanguagePreferenceFieldView: FieldViews.DropdownFieldView.extend({
fieldTemplate: field_dropdown_account_template,
initialize: function(options) {
this._super(options); // eslint-disable-line no-underscore-dangle
this.listenTo(this.model, 'revertValue', this.revertValue);
},
revertValue: function(event) {
var attributes = {},
oldPrefLang = $(event.target).data('old-lang-code');
if (oldPrefLang) {
attributes['pref-lang'] = oldPrefLang;
this.saveAttributes(attributes);
}
},
saveSucceeded: function() {
var data = {
language: this.modelValue(),
next: window.location.href
};
var view = this;
$.ajax({
type: 'POST',
url: '/i18n/setlang/',
data: data,
dataType: 'html',
success: function() {
view.showSuccessMessage();
},
error: function() {
view.showNotificationMessage(
HtmlUtils.joinHtml(
view.indicators.error,
gettext('You must sign out and sign back in before your language changes take effect.') // eslint-disable-line max-len
)
);
}
});
}
}),
TimeZoneFieldView: FieldViews.DropdownFieldView.extend({
fieldTemplate: field_dropdown_account_template,
initialize: function(options) {
this.options = _.extend({}, options);
_.bindAll(this, 'listenToCountryView', 'updateCountrySubheader', 'replaceOrAddGroupOption');
this._super(options); // eslint-disable-line no-underscore-dangle
},
listenToCountryView: function(view) {
this.listenTo(view.model, 'change:country', this.updateCountrySubheader);
},
updateCountrySubheader: function(user) {
var view = this;
$.ajax({
type: 'GET',
url: '/api/user/v1/preferences/time_zones/',
data: {country_code: user.attributes.country},
success: function(data) {
var countryTimeZones = $.map(data, function(timeZoneInfo) {
return [[timeZoneInfo.time_zone, timeZoneInfo.description]];
});
view.replaceOrAddGroupOption(
'Country Time Zones',
countryTimeZones
);
view.render();
}
});
},
updateValueInField: function() {
var options;
if (this.modelValue()) {
options = [[this.modelValue(), this.displayValue(this.modelValue())]];
this.replaceOrAddGroupOption(
'Currently Selected Time Zone',
options
);
}
this._super(); // eslint-disable-line no-underscore-dangle
},
replaceOrAddGroupOption: function(title, options) {
var groupOption = {
groupTitle: gettext(title),
selectOptions: options
};
var index = _.findIndex(this.options.groupOptions, function(group) {
return group.groupTitle === gettext(title);
});
if (index >= 0) {
this.options.groupOptions[index] = groupOption;
} else {
this.options.groupOptions.unshift(groupOption);
}
}
}),
PasswordFieldView: FieldViews.LinkFieldView.extend({
fieldType: 'button',
fieldTemplate: field_link_account_template,
events: {
'click button': 'linkClicked'
},
initialize: function(options) {
this.options = _.extend({}, options);
this._super(options);
_.bindAll(this, 'resetPassword');
},
linkClicked: function(event) {
event.preventDefault();
this.toggleDisableButton(true);
this.resetPassword(event);
},
resetPassword: function() {
var data = {};
data[this.options.emailAttribute] = this.model.get(this.options.emailAttribute);
var view = this;
$.ajax({
type: 'POST',
url: view.options.linkHref,
data: data,
success: function() {
view.showSuccessMessage();
view.setMessageTimeout();
},
error: function(xhr) {
view.showErrorMessage(xhr);
view.setMessageTimeout();
view.toggleDisableButton(false);
}
});
},
toggleDisableButton: function(disabled) {
var button = this.$('#u-field-link-' + this.options.valueAttribute);
if (button) {
button.prop('disabled', disabled);
}
},
setMessageTimeout: function() {
var view = this;
setTimeout(function() {
view.showHelpMessage();
}, 6000);
},
successMessage: function() {
return HtmlUtils.joinHtml(
this.indicators.success,
HtmlUtils.interpolateHtml(
gettext('We\'ve sent a message to {email}. Click the link in the message to reset your password. Didn\'t receive the message? Contact {anchorStart}technical support{anchorEnd}.'), // eslint-disable-line max-len
{
email: this.model.get(this.options.emailAttribute),
anchorStart: HtmlUtils.HTML(
StringUtils.interpolate(
'<a href="{passwordResetSupportUrl}">', {
passwordResetSupportUrl: this.options.passwordResetSupportUrl
}
)
),
anchorEnd: HtmlUtils.HTML('</a>')
}
)
);
}
}),
LanguageProficienciesFieldView: FieldViews.DropdownFieldView.extend({
fieldTemplate: field_dropdown_account_template,
modelValue: function() {
var modelValue = this.model.get(this.options.valueAttribute);
if (_.isArray(modelValue) && modelValue.length > 0) {
return modelValue[0].code;
} else {
return null;
}
},
saveValue: function() {
var attributes = {},
value = '';
if (this.persistChanges === true) {
value = this.fieldValue() ? [{code: this.fieldValue()}] : [];
attributes[this.options.valueAttribute] = value;
this.saveAttributes(attributes);
}
}
}),
SocialLinkTextFieldView: FieldViews.TextFieldView.extend({
render: function() {
HtmlUtils.setHtml(this.$el, HtmlUtils.template(field_text_account_template)({
id: this.options.valueAttribute + '_' + this.options.platform,
title: this.options.title,
value: this.modelValue(),
message: this.options.helpMessage,
placeholder: this.options.placeholder || ''
}));
this.delegateEvents();
return this;
},
modelValue: function() {
var socialLinks = this.model.get(this.options.valueAttribute);
for (var i = 0; i < socialLinks.length; i++) { // eslint-disable-line vars-on-top
if (socialLinks[i].platform === this.options.platform) {
return socialLinks[i].social_link;
}
}
return null;
},
saveValue: function() {
var attributes, value;
if (this.persistChanges === true) {
attributes = {};
value = this.fieldValue() != null ? [{platform: this.options.platform,
social_link: this.fieldValue()}] : [];
attributes[this.options.valueAttribute] = value;
this.saveAttributes(attributes);
}
}
}),
ExtendedFieldTextFieldView: FieldViews.TextFieldView.extend({
render: function() {
HtmlUtils.setHtml(this.$el, HtmlUtils.template(field_text_account_template)({
id: this.options.valueAttribute + '_' + this.options.field_name,
title: this.options.title,
value: this.modelValue(),
message: this.options.helpMessage,
placeholder: this.options.placeholder || ''
}));
this.delegateEvents();
return this;
},
modelValue: function() {
var extendedProfileFields = this.model.get(this.options.valueAttribute);
for (var i = 0; i < extendedProfileFields.length; i++) { // eslint-disable-line vars-on-top
if (extendedProfileFields[i].field_name === this.options.fieldName) {
return extendedProfileFields[i].field_value;
}
}
return null;
},
saveValue: function() {
var attributes, value;
if (this.persistChanges === true) {
attributes = {};
value = this.fieldValue() != null ? [{field_name: this.options.fieldName,
field_value: this.fieldValue()}] : [];
attributes[this.options.valueAttribute] = value;
this.saveAttributes(attributes);
}
}
}),
ExtendedFieldListFieldView: FieldViews.DropdownFieldView.extend({
fieldTemplate: field_dropdown_account_template,
modelValue: function() {
var extendedProfileFields = this.model.get(this.options.valueAttribute);
for (var i = 0; i < extendedProfileFields.length; i++) { // eslint-disable-line vars-on-top
if (extendedProfileFields[i].field_name === this.options.fieldName) {
return extendedProfileFields[i].field_value;
}
}
return null;
},
saveValue: function() {
var attributes = {},
value;
if (this.persistChanges === true) {
value = this.fieldValue() ? [{field_name: this.options.fieldName,
field_value: this.fieldValue()}] : [];
attributes[this.options.valueAttribute] = value;
this.saveAttributes(attributes);
}
}
}),
AuthFieldView: FieldViews.LinkFieldView.extend({
fieldTemplate: field_social_link_template,
className: function() {
return 'u-field u-field-social u-field-' + this.options.valueAttribute;
},
initialize: function(options) {
this.options = _.extend({}, options);
this._super(options);
_.bindAll(this, 'redirect_to', 'disconnect', 'successMessage', 'inProgressMessage');
},
render: function() {
var linkTitle = '',
linkClass = '',
subTitle = '',
screenReaderTitle = StringUtils.interpolate(
gettext('Link your {accountName} account'),
{accountName: this.options.title}
);
if (this.options.connected) {
linkTitle = gettext('Unlink This Account');
linkClass = 'social-field-linked';
subTitle = StringUtils.interpolate(
gettext('You can use your {accountName} account to sign in to your {platformName} account.'), // eslint-disable-line max-len
{accountName: this.options.title, platformName: this.options.platformName}
);
screenReaderTitle = StringUtils.interpolate(
gettext('Unlink your {accountName} account'),
{accountName: this.options.title}
);
} else if (this.options.acceptsLogins) {
linkTitle = gettext('Link Your Account');
linkClass = 'social-field-unlinked';
subTitle = StringUtils.interpolate(
gettext('Link your {accountName} account to your {platformName} account and use {accountName} to sign in to {platformName}.'), // eslint-disable-line max-len
{accountName: this.options.title, platformName: this.options.platformName}
);
}
HtmlUtils.setHtml(this.$el, HtmlUtils.template(this.fieldTemplate)({
id: this.options.valueAttribute,
title: this.options.title,
screenReaderTitle: screenReaderTitle,
linkTitle: linkTitle,
subTitle: subTitle,
linkClass: linkClass,
linkHref: '#',
message: this.helpMessage
}));
this.delegateEvents();
return this;
},
linkClicked: function(event) {
event.preventDefault();
this.showInProgressMessage();
if (this.options.connected) {
this.disconnect();
} else {
// Direct the user to the providers site to start the authentication process.
// See python-social-auth docs for more information.
this.redirect_to(this.options.connectUrl);
}
},
redirect_to: function(url) {
window.location.href = url;
},
disconnect: function() {
var data = {};
// Disconnects the provider from the user's edX account.
// See python-social-auth docs for more information.
var view = this;
$.ajax({
type: 'POST',
url: this.options.disconnectUrl,
data: data,
dataType: 'html',
success: function() {
view.options.connected = false;
view.render();
view.showSuccessMessage();
},
error: function(xhr) {
view.showErrorMessage(xhr);
}
});
},
inProgressMessage: function() {
return HtmlUtils.joinHtml(this.indicators.inProgress, (
this.options.connected ? gettext('Unlinking') : gettext('Linking')
));
},
successMessage: function() {
return HtmlUtils.joinHtml(this.indicators.success, gettext('Successfully unlinked.'));
}
}),
OrderHistoryFieldView: FieldViews.ReadonlyFieldView.extend({
fieldType: 'orderHistory',
fieldTemplate: field_order_history_template,
initialize: function(options) {
this.options = options;
this._super(options);
this.template = HtmlUtils.template(this.fieldTemplate);
},
render: function() {
HtmlUtils.setHtml(this.$el, this.template({
totalPrice: this.options.totalPrice,
orderId: this.options.orderId,
orderDate: this.options.orderDate,
receiptUrl: this.options.receiptUrl,
valueAttribute: this.options.valueAttribute,
lines: this.options.lines
}));
this.delegateEvents();
return this;
}
})
};
return AccountSettingsFieldViews;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,155 +0,0 @@
(function(define, undefined) {
'use strict';
define([
'gettext',
'jquery',
'underscore',
'common/js/components/views/tabbed_view',
'edx-ui-toolkit/js/utils/html-utils',
'js/student_account/views/account_section_view',
'text!templates/student_account/account_settings.underscore'
], function(gettext, $, _, TabbedView, HtmlUtils, AccountSectionView, accountSettingsTemplate) {
var AccountSettingsView = TabbedView.extend({
navLink: '.account-nav-link',
activeTab: 'aboutTabSections',
events: {
'click .account-nav-link': 'switchTab',
'keydown .account-nav-link': 'keydownHandler',
'click .btn-alert-primary': 'revertValue'
},
initialize: function(options) {
this.options = options;
_.bindAll(this, 'render', 'switchTab', 'setActiveTab', 'showLoadingError');
},
render: function() {
var tabName, betaLangMessage, helpTranslateText, helpTranslateLink, betaLangCode, oldLangCode,
view = this;
var accountSettingsTabs = [
{
name: 'aboutTabSections',
id: 'about-tab',
label: gettext('Account Information'),
class: 'active',
tabindex: 0,
selected: true,
expanded: true
},
{
name: 'accountsTabSections',
id: 'accounts-tab',
label: gettext('Linked Accounts'),
tabindex: -1,
selected: false,
expanded: false
}
];
if (!view.options.disableOrderHistoryTab) {
accountSettingsTabs.push({
name: 'ordersTabSections',
id: 'orders-tab',
label: gettext('Order History'),
tabindex: -1,
selected: false,
expanded: false
});
}
if (!_.isEmpty(view.options.betaLanguage) && $.cookie('old-pref-lang')) {
betaLangMessage = HtmlUtils.interpolateHtml(
gettext('You have set your language to {beta_language}, which is currently not fully translated. You can help us translate this language fully by joining the Transifex community and adding translations from English for learners that speak {beta_language}.'), // eslint-disable-line max-len
{
beta_language: view.options.betaLanguage.name
}
);
helpTranslateText = HtmlUtils.interpolateHtml(
gettext('Help Translate into {beta_language}'),
{
beta_language: view.options.betaLanguage.name
}
);
betaLangCode = this.options.betaLanguage.code.split('-');
if (betaLangCode.length > 1) {
betaLangCode = betaLangCode[0] + '_' + betaLangCode[1].toUpperCase();
} else {
betaLangCode = betaLangCode[0];
}
helpTranslateLink = 'https://www.transifex.com/open-edx/edx-platform/translate/#' + betaLangCode;
oldLangCode = $.cookie('old-pref-lang');
// Deleting the cookie
document.cookie = 'old-pref-lang=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/account;';
$.cookie('focus_id', '#beta-language-message');
}
HtmlUtils.setHtml(this.$el, HtmlUtils.template(accountSettingsTemplate)({
accountSettingsTabs: accountSettingsTabs,
HtmlUtils: HtmlUtils,
message: betaLangMessage,
helpTranslateText: helpTranslateText,
helpTranslateLink: helpTranslateLink,
oldLangCode: oldLangCode
}));
_.each(accountSettingsTabs, function(tab) {
tabName = tab.name;
view.renderSection(view.options.tabSections[tabName], tabName, tab.label);
});
return this;
},
switchTab: function(e) {
var $currentTab,
$accountNavLink = $('.account-nav-link');
if (e) {
e.preventDefault();
$currentTab = $(e.target);
this.activeTab = $currentTab.data('name');
_.each(this.$('.account-settings-tabpanels'), function(tabPanel) {
$(tabPanel).addClass('hidden');
});
$('#' + this.activeTab + '-tabpanel').removeClass('hidden');
$accountNavLink.attr('tabindex', -1);
$accountNavLink.attr('aria-selected', false);
$accountNavLink.attr('aria-expanded', false);
$currentTab.attr('tabindex', 0);
$currentTab.attr('aria-selected', true);
$currentTab.attr('aria-expanded', true);
$(this.navLink).removeClass('active');
$currentTab.addClass('active');
}
},
setActiveTab: function() {
this.switchTab();
},
renderSection: function(tabSections, tabName, tabLabel) {
var accountSectionView = new AccountSectionView({
tabName: tabName,
tabLabel: tabLabel,
sections: tabSections,
el: '#' + tabName + '-tabpanel'
});
accountSectionView.render();
},
showLoadingError: function() {
this.$('.ui-loading-error').removeClass('is-hidden');
},
revertValue: function(event) {
this.options.userPreferencesModel.trigger('revertValue', event);
}
});
return AccountSettingsView;
});
}).call(this, define || RequireJS.define);

View File

@@ -1 +0,0 @@
../../openedx/features/learner_profile/static/learner_profile

View File

@@ -33,10 +33,8 @@
'js/discussions_management/views/discussions_dashboard_factory',
'js/header_factory',
'js/student_account/logistration_factory',
'js/student_account/views/account_settings_factory',
'js/student_account/views/finish_auth_factory',
'js/views/message_banner',
'learner_profile/js/learner_profile_factory',
'lms/js/preview/preview_factory',
'support/js/certificates_factory',
'support/js/enrollment_factory',

View File

@@ -761,9 +761,6 @@
'js/spec/shoppingcart/shoppingcart_spec.js',
'js/spec/staff_debug_actions_spec.js',
'js/spec/student_account/access_spec.js',
'js/spec/student_account/account_settings_factory_spec.js',
'js/spec/student_account/account_settings_fields_spec.js',
'js/spec/student_account/account_settings_view_spec.js',
'js/spec/student_account/emailoptin_spec.js',
'js/spec/student_account/enrollment_spec.js',
'js/spec/student_account/finish_auth_spec.js',
@@ -787,14 +784,6 @@
'js/spec/views/file_uploader_spec.js',
'js/spec/views/message_banner_spec.js',
'js/spec/views/notification_spec.js',
'learner_profile/js/spec/learner_profile_factory_spec.js',
'learner_profile/js/spec/views/badge_list_container_spec.js',
'learner_profile/js/spec/views/badge_list_view_spec.js',
'learner_profile/js/spec/views/badge_view_spec.js',
'learner_profile/js/spec/views/learner_profile_fields_spec.js',
'learner_profile/js/spec/views/learner_profile_view_spec.js',
'learner_profile/js/spec/views/section_two_tab_spec.js',
'learner_profile/js/spec/views/share_modal_view_spec.js',
'support/js/spec/collections/enrollment_spec.js',
'support/js/spec/models/enrollment_spec.js',
'support/js/spec/views/certificates_spec.js',

View File

@@ -52,7 +52,6 @@
@import 'multicourse/survey-page';
// base - specific views
@import 'views/account-settings';
@import 'views/course-entitlements';
@import 'views/login-register';
@import 'views/verification';
@@ -69,7 +68,6 @@
// features
@import 'features/bookmarks-v1';
@import "features/announcements";
@import 'features/learner-profile';
@import 'features/_unsupported-browser-alert';
@import 'features/content-type-gating';
@import 'features/course-duration-limits';

View File

@@ -1,875 +0,0 @@
// lms - application - learner profile
// ====================
.learner-achievements {
.learner-message {
@extend %no-content;
margin: $baseline*0.75 0;
.message-header,
.message-actions {
text-align: center;
}
.message-actions {
margin-top: $baseline/2;
.btn-brand {
color: $white;
}
}
}
}
.certificate-card {
display: flex;
flex-direction: row;
margin-bottom: $baseline;
padding: $baseline/2;
border: 1px;
border-style: solid;
background-color: $white;
cursor: pointer;
&:hover {
box-shadow: 0 0 1px 1px $gray-l2;
}
.card-logo {
@include margin-right($baseline);
width: 100px;
height: 100px;
@media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
display: none;
}
}
.card-content {
color: $body-color;
margin-top: $baseline/2;
}
.card-supertitle {
@extend %t-title6;
color: $lightest-base-font-color;
}
.card-title {
@extend %t-title5;
@extend %t-strong;
margin-bottom: $baseline/2;
}
.card-text {
@extend %t-title8;
color: $lightest-base-font-color;
}
&.mode-audit {
border-color: $audit-mode-color;
.card-logo {
background-image: url('#{$static-path}/images/certificates/audit.png');
}
}
&.mode-honor {
border-color: $honor-mode-color;
.card-logo {
background-image: url('#{$static-path}/images/certificates/honor.png');
}
}
&.mode-verified {
border-color: $verified-mode-color;
.card-logo {
background-image: url('#{$static-path}/images/certificates/verified.png');
}
}
&.mode-professional {
border-color: $professional-certificate-color;
.card-logo {
background-image: url('#{$static-path}/images/certificates/professional.png');
}
}
}
.view-profile {
$profile-image-dimension: 120px;
.window-wrap,
.content-wrapper {
background-color: $body-bg;
padding: 0;
margin-top: 0;
}
.page-banner {
background-color: $gray-l4;
max-width: none;
.user-messages {
max-width: map-get($container-max-widths, xl);
margin: auto;
padding: $baseline/2;
}
}
.ui-loading-indicator {
@extend .ui-loading-base;
padding-bottom: $baseline;
// center horizontally
@include margin-left(auto);
@include margin-right(auto);
width: ($baseline*5);
}
.profile-image-field {
button {
background: transparent !important;
border: none !important;
padding: 0;
}
.u-field-image {
padding-top: 0;
padding-bottom: ($baseline/4);
}
.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: ($profile-image-dimension/2);
overflow: hidden;
border: 3px solid $gray-l6;
margin-top: $baseline*-0.75;
background: $white;
}
.u-field-upload-button {
position: absolute;
top: 0;
opacity: 0;
width: $profile-image-dimension;
height: $profile-image-dimension;
border-radius: ($profile-image-dimension/2);
border: 2px dashed transparent;
background: rgba(229, 241, 247, 0.8);
color: $link-color;
text-shadow: none;
@include transition(all $tmg-f1 ease-in-out 0s);
z-index: 6;
i {
color: $link-color;
}
&:focus,
&:hover {
@include show-hover-state();
border-color: $link-color;
}
&.in-progress {
opacity: 1;
}
}
.button-visible {
@include show-hover-state();
}
.upload-button-icon,
.upload-button-title {
display: block;
margin-bottom: ($baseline/4);
@include transform(translateY(35px));
line-height: 1.3em;
text-align: center;
z-index: 7;
color: $body-color;
}
.upload-button-input {
position: absolute;
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 {
position: relative;
display: block;
width: $profile-image-dimension;
margin-top: ($baseline / 4);
padding: ($baseline / 5) 0 0;
text-align: center;
opacity: 0;
transition: opacity 0.5s;
}
&:hover,
&:active {
.u-field-remove-button {
opacity: 1;
}
}
}
}
.wrapper-profile {
min-height: 200px;
background-color: $gray-l6;
.ui-loading-indicator {
margin-top: 100px;
}
}
.profile-self {
.wrapper-profile-field-account-privacy {
@include clearfix();
box-sizing: border-box;
width: 100%;
margin: 0 auto;
border-bottom: 1px solid $gray-l3;
background-color: $gray-l4;
padding: ($baseline*0.75) 5%;
display: table;
.wrapper-profile-records {
display: table-row;
button {
@extend %btn-secondary-blue-outline;
margin-top: 1em;
background: $blue;
color: #fff;
}
}
@include media-breakpoint-up(sm) {
.wrapper-profile-records {
display: table-cell;
vertical-align: middle;
white-space: nowrap;
button {
margin-top: 0;
}
}
}
.u-field-account_privacy {
@extend .container;
display: table-cell;
border: none;
box-shadow: none;
padding: 0;
margin: 0;
vertical-align: middle;
@media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
max-width: calc(100% - 40px);
min-width: auto;
}
.btn-change-privacy {
@extend %btn-primary-blue;
padding-top: 4px;
padding-bottom: 5px;
background-image: none;
box-shadow: none;
}
}
.u-field-title {
@extend %t-strong;
width: auto;
color: $body-color;
cursor: text;
text-shadow: none; // override bad lms styles on labels
}
.u-field-value {
width: auto;
@include margin-left($baseline/2);
}
.u-field-message {
@include float(left);
width: 100%;
padding: 0;
color: $body-color;
.u-field-message-notification {
color: $gray-d2;
}
}
}
}
.wrapper-profile-sections {
@extend .container;
@include padding($baseline*1.5, 5%, $baseline*1.5, 5%);
display: flex;
min-width: 0;
max-width: 100%;
@media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
@include margin-left(0);
flex-wrap: wrap;
}
}
.profile-header {
max-width: map-get($container-max-widths, xl);
margin: auto;
padding: $baseline 5% 0;
.header {
@extend %t-title4;
@extend %t-ultrastrong;
display: inline-block;
color: #222;
}
.subheader {
@extend %t-title6;
}
}
.wrapper-profile-section-container-one {
@media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
width: 100%;
}
.wrapper-profile-section-one {
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 {
margin: 0 $baseline/2;
.social-links {
@include padding($baseline/4, 0, 0, $baseline/4);
font-size: 2rem;
& > span {
color: $gray-l4;
}
a {
.fa-facebook-square {
color: $facebook-blue;
}
.fa-twitter-square {
color: $twitter-blue;
}
.fa-linkedin-square {
color: $linkedin-blue;
}
}
}
.u-field {
font-weight: $font-semibold;
@include padding(0, 0, 0, 3px);
color: $body-color;
margin-top: $baseline/5;
.u-field-value,
.u-field-title {
font-weight: 500;
width: calc(100% - 40px);
color: $lightest-base-font-color;
}
.u-field-value-readonly {
font-family: $font-family-sans-serif;
color: $darkest-base-font-color;
}
&.u-field-dropdown {
position: relative;
&:not(.editable-never) {
cursor: pointer;
}
}
&:not(.u-field-readonly) {
&.u-field-value {
@extend %t-weight3;
}
&:not(:last-child) {
padding-bottom: $baseline/4;
border-bottom: 1px solid $border-color;
&:hover.mode-placeholder {
padding-bottom: $baseline/5;
border-bottom: 2px dashed $link-color;
}
}
}
}
& > .u-field {
&:not(:first-child) {
font-size: $body-font-size;
color: $body-color;
font-weight: $font-light;
margin-bottom: 0;
}
&:first-child {
@extend %t-title4;
@extend %t-weight4;
font-size: em(24);
}
}
select {
width: 85%;
}
.u-field-message {
@include right(0);
position: absolute;
top: 0;
width: 20px;
.icon {
vertical-align: baseline;
}
}
}
}
}
.wrapper-profile-section-container-two {
@include float(left);
@include padding-left($baseline);
font-family: $font-family-sans-serif;
flex-grow: 1;
@media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
width: 90%;
margin-top: $baseline;
padding: 0;
}
.u-field-textarea {
@include padding(0, ($baseline*0.75), ($baseline*0.75), 0);
margin-bottom: ($baseline/2);
@media (max-width: $learner-profile-container-flex) { // Switch to map-get($grid-breakpoints,md) for bootstrap
@include padding-left($baseline/4);
}
.u-field-header {
position: relative;
.u-field-message {
@include right(0);
top: $baseline/4;
position: absolute;
}
}
&.editable-toggle {
cursor: pointer;
}
}
.u-field-title {
@extend %t-title6;
display: inline-block;
margin-top: 0;
margin-bottom: ($baseline/4);
color: $gray-d3;
width: 100%;
font: $font-semibold 1.4em/1.4em $font-family-sans-serif;
}
.u-field-value {
@extend %t-copy-base;
width: 100%;
overflow: auto;
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 {
color: inherit;
}
}
.u-field-message {
@include float(right);
width: auto;
.message-can-edit {
position: absolute;
}
}
.u-field.mode-placeholder {
padding: $baseline;
margin: $baseline*0.75 0;
border: 2px dashed $gray-l3;
i {
font-size: 12px;
@include padding-right(5px);
vertical-align: middle;
color: $body-color;
}
.u-field-title {
width: 100%;
text-align: center;
}
.u-field-value {
text-align: center;
line-height: 1.5em;
@extend %t-copy-sub1;
color: $body-color;
}
&:hover {
border: 2px dashed $link-color;
.u-field-title,
i {
color: $link-color;
}
}
}
.wrapper-u-field {
font-size: $body-font-size;
color: $body-color;
.u-field-header .u-field-title {
color: $body-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: 3em;
}
}
.badge-paging-header {
padding-top: $baseline;
}
.page-content-nav {
@extend %page-content-nav;
}
.badge-set-display {
@extend .container;
padding: 0;
.badge-list {
// We're using a div instead of ul for accessibility, so we have to match the style
// used by ul.
margin: 1em 0;
padding: 0 0 0 40px;
}
.badge-display {
width: 50%;
display: inline-block;
vertical-align: top;
padding: 2em 0;
.badge-image-container {
padding-right: $baseline;
margin-left: 1em;
width: 20%;
vertical-align: top;
display: inline-block;
img.badge {
width: 100%;
}
.accomplishment-placeholder {
border: 4px dotted $gray-l4;
border-radius: 50%;
display: block;
width: 100%;
padding-bottom: 100%;
}
}
.badge-details {
@extend %t-copy-sub1;
@extend %t-regular;
max-width: 70%;
display: inline-block;
color: $gray-d1;
.badge-name {
@extend %t-strong;
@extend %t-copy-base;
color: $gray-d3;
}
.badge-description {
padding-bottom: $baseline;
line-height: 1.5em;
}
.badge-date-stamp {
@extend %t-copy-sub1;
}
.find-button-container {
border: 1px solid $blue-l1;
padding: ($baseline / 2) $baseline ($baseline / 2) $baseline;
display: inline-block;
border-radius: 5px;
font-weight: bold;
color: $blue-s3;
}
.share-button {
@extend %t-action3;
@extend %button-reset;
background: $gray-l6;
color: $gray-d1;
padding: ($baseline / 4) ($baseline / 2);
margin-bottom: ($baseline / 2);
display: inline-block;
border-radius: 5px;
border: 2px solid $gray-d1;
cursor: pointer;
transition: background 0.5s;
.share-prefix {
display: inline-block;
vertical-align: middle;
}
.share-icon-container {
display: inline-block;
img.icon-mozillaopenbadges {
max-width: 1.5em;
margin-right: 0.25em;
}
}
&:hover {
background: $gray-l4;
}
&:active {
box-shadow: inset 0 4px 15px 0 $black-t2;
transition: none;
}
}
}
}
.badge-placeholder {
background-color: $gray-l7;
box-shadow: inset 0 0 4px 0 $gray-l4;
}
}
// ------------------------------
// #BADGES MODAL
// ------------------------------
.badges-overlay {
@extend %ui-depth1;
position: fixed;
top: 0;
left: 0;
background-color: $dark-trans-bg; /* dim the background */
width: 100%;
height: 100%;
vertical-align: middle;
.badges-modal {
@extend %t-copy-lead1;
@extend %ui-depth2;
color: $lighter-base-font-color;
box-sizing: content-box;
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 80%;
max-width: 700px;
max-height: calc(100% - 100px);
margin-right: auto;
margin-left: auto;
border-top: rem(10) solid $blue-l2;
background: $light-gray3;
padding-right: ($baseline * 2);
padding-left: ($baseline * 2);
padding-bottom: ($baseline);
overflow-x: hidden;
.modal-header {
margin-top: ($baseline / 2);
margin-bottom: ($baseline / 2);
}
.close {
@extend %button-reset;
@extend %t-strong;
color: $lighter-base-font-color;
position: absolute;
right: ($baseline);
top: $baseline;
cursor: pointer;
padding: ($baseline / 4) ($baseline / 2);
@include transition(all $tmg-f2 ease-in-out 0s);
&:focus,
&:hover {
background-color: $blue-d2;
border-radius: 3px;
color: $white;
}
}
.badges-steps {
display: table;
}
.image-container {
// Lines the image up with the content of the above list.
@include ltr {
@include padding-left(2em);
}
@include rtl {
@include padding-right(1em);
float: right;
}
}
.backpack-logo {
@include float(right);
@include margin-left($baseline);
}
}
}
.modal-hr {
display: block;
border: none;
background-color: $light-gray;
height: rem(2);
width: 100%;
}
}

View File

@@ -527,9 +527,6 @@ $palette-success-border: #b9edb9;
$palette-success-back: #ecfaec;
$palette-success-text: #008100;
// learner profile elements
$learner-profile-container-flex: 768px;
// course elements
$course-bg-color: $uxpl-grayscale-x-back !default;
$account-content-wrapper-bg: shade($body-bg, 2%) !default;

View File

@@ -1,683 +0,0 @@
// lms - application - account settings
// ====================
// Table of Contents
// * +Container - Account Settings
// * +Main - Header
// * +Settings Section
// * +Alert Messages
// +Container - Account Settings
.wrapper-account-settings {
background: $white;
width: 100%;
.account-settings-container {
max-width: grid-width(12);
padding: 10px;
margin: 0 auto;
}
.ui-loading-indicator,
.ui-loading-error {
@extend .ui-loading-base;
// center horizontally
@include margin-left(auto);
@include margin-right(auto);
padding: ($baseline*3);
text-align: center;
.message-error {
color: $alert-color;
}
}
}
// +Main - Header
.wrapper-account-settings {
.wrapper-header {
max-width: grid-width(12);
height: 139px;
border-bottom: 4px solid $m-gray-l4;
.header-title {
@extend %t-title4;
margin-bottom: ($baseline/2);
padding-top: ($baseline*2);
}
.header-subtitle {
color: $gray-l2;
}
.account-nav {
@include float(left);
margin: ($baseline/2) 0;
padding: 0;
list-style: none;
.account-nav-link {
@include float(left);
font-size: em(14);
color: $gray;
padding: $baseline/4 $baseline*1.25 $baseline;
display: inline-block;
box-shadow: none;
border-bottom: 4px solid transparent;
border-radius: 0;
background: transparent none;
}
button {
@extend %ui-clear-button;
@extend %btn-no-style;
@include appearance(none);
display: block;
padding: ($baseline/4);
&:hover,
&:focus {
text-decoration: none;
border-bottom-color: $courseware-border-bottom-color;
}
&.active {
border-bottom-color: theme-color("dark");
}
}
}
@include media-breakpoint-down(md) {
border-bottom-color: transparent;
.account-nav {
display: flex;
border-bottom: none;
.account-nav-link {
border-bottom: 4px solid theme-color("light");
}
}
}
}
}
// +Settings Section
.account-settings-sections {
.section-header {
@extend %t-title5;
@extend %t-strong;
padding-top: ($baseline/2)*3;
color: $dark-gray1;
}
.section {
background-color: $white;
margin: $baseline 5% 0;
border-bottom: 4px solid $m-gray-l4;
.account-settings-header-subtitle {
font-size: em(14);
line-height: normal;
color: $dark-gray;
padding-bottom: 10px;
}
.account-settings-header-subtitle-warning {
@extend .account-settings-header-subtitle;
color: $alert-color;
}
.account-settings-section-body {
.u-field {
border-bottom: 2px solid $m-gray-l4;
padding: $baseline*0.75 0;
.field {
width: 30%;
vertical-align: top;
display: inline-block;
position: relative;
select {
@include appearance(none);
padding: 14px 30px 14px 15px;
border: 1px solid $gray58-border;
background-color: transparent;
border-radius: 2px;
position: relative;
z-index: 10;
&::-ms-expand {
display: none;
}
~ .icon-caret-down {
&::after {
content: "";
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-top: 7px solid $blue;
position: absolute;
right: 10px;
bottom: 20px;
z-index: 0;
}
}
}
.field-label {
display: block;
width: auto;
margin-bottom: 0.625rem;
font-size: 1rem;
line-height: 1;
color: $dark-gray;
white-space: nowrap;
}
.field-input {
@include transition(all 0.125s ease-in-out 0s);
display: inline-block;
padding: 0.625rem;
border: 1px solid $gray58-border;
border-radius: 2px;
background: $white;
font-size: $body-font-size;
color: $dark-gray;
width: 100%;
height: 48px;
box-shadow: none;
}
.u-field-link {
@extend %ui-clear-button;
// set styles
@extend %btn-pl-default-base;
@include font-size(18);
width: 100%;
border: 1px solid $blue;
color: $blue;
padding: 11px 14px;
line-height: normal;
}
}
.u-field-order {
display: flex;
align-items: center;
font-size: em(16);
font-weight: 600;
color: $dark-gray;
width: 100%;
padding-top: $baseline;
padding-bottom: $baseline;
line-height: normal;
flex-flow: row wrap;
span {
padding: $baseline;
}
.u-field-order-number {
@include float(left);
width: 30%;
}
.u-field-order-date {
@include float(left);
padding-left: 30px;
width: 20%;
}
.u-field-order-price {
@include float(left);
width: 15%;
}
.u-field-order-link {
width: 10%;
padding: 0;
.u-field-link {
@extend %ui-clear-button;
@extend %btn-pl-default-base;
@include font-size(14);
border: 1px solid $blue;
color: $blue;
line-height: normal;
padding: 10px;
width: 110px;
}
}
}
.u-field-order-lines {
@extend .u-field-order;
padding: 5px 0 0;
font-weight: 100;
.u-field-order-number {
padding: 20px 10px 20px 30px;
}
}
.social-field-linked {
background: $m-gray-l4;
box-shadow: 0 1px 2px 1px $shadow-l2;
padding: 1.25rem;
box-sizing: border-box;
margin: 10px;
width: 100%;
.field-label {
@include font-size(24);
}
.u-field-social-help {
display: inline-block;
padding: 20px 0 6px;
}
.u-field-link {
@include font-size(14);
@include text-align(left);
border: none;
margin-top: $baseline;
font-weight: $font-semibold;
padding: 0;
&:focus,
&:hover,
&:active {
background-color: transparent;
color: $m-blue-d3;
border: none;
}
}
}
.social-field-unlinked {
background: $m-gray-l4;
box-shadow: 0 1px 2px 1px $shadow-l2;
padding: 1.25rem;
box-sizing: border-box;
text-align: center;
margin: 10px;
width: 100%;
.field-label {
@include font-size(24);
text-align: center;
}
.u-field-link {
@include font-size(14);
margin-top: $baseline;
font-weight: $font-semibold;
}
}
.u-field-message {
position: relative;
padding: $baseline*0.75 0 0 ($baseline*4);
width: 60%;
.u-field-message-notification {
position: absolute;
left: 0;
top: 0;
bottom: 0;
margin: auto;
padding: 38px 0 0 ($baseline*5);
}
}
&:last-child {
border-bottom: none;
margin-bottom: ($baseline*2);
}
// Responsive behavior
@include media-breakpoint-down(md) {
.u-field-value {
width: 100%;
}
.u-field-message {
width: 100%;
padding: $baseline/2 0;
.u-field-message-notification {
position: relative;
padding: 0;
}
}
.u-field-order {
display: flex;
flex-wrap: nowrap;
.u-field-order-number,
.u-field-order-date,
.u-field-order-price,
.u-field-order-link {
width: auto;
float: none;
flex-grow: 1;
&:first-of-type {
flex-grow: 2;
}
}
}
}
}
.u-field {
&.u-field-dropdown,
&.editable-never &.mode-display {
.u-field-value {
margin-bottom: ($baseline);
.u-field-title {
font-size: 16px;
line-height: 22px;
margin-bottom: 18px;
}
.u-field-value-readonly {
font-size: 22px;
color: #636c72;
line-height: 30px;
white-space: nowrap;
}
}
}
}
.u-field-readonly .u-field-title {
font-size: 16px;
color: #636c72;
line-height: 22px;
padding-top: ($baseline/2);
padding-bottom: 0;
margin-bottom: 8px !important;
}
.u-field-readonly .u-field-value {
font-size: 22px;
color: #636c72;
line-height: 30px;
padding-top: 8px;
padding-bottom: ($baseline);
white-space: nowrap;
}
.u-field-orderHistory {
border-bottom: none;
border: 1px solid $m-gray-l4;
margin-bottom: $baseline;
padding: 0;
&:last-child {
border-bottom: 1px solid $m-gray-l4;
}
&:hover,
&:focus {
background-color: $light-gray4;
}
}
.u-field-order-orderId {
border: none;
margin-top: $baseline;
margin-bottom: 0;
padding-bottom: 0;
&:hover,
&:focus {
background-color: transparent;
}
.u-field-order {
font-weight: $font-semibold;
padding-top: 0;
padding-bottom: 0;
.u-field-order-title {
font-size: em(16);
}
}
}
.u-field-social {
border-bottom: none;
margin-right: 20px;
width: 30%;
display: inline-block;
vertical-align: top;
.u-field-social-help {
@include font-size(12);
color: $m-gray-d1;
}
}
}
.account-deletion-details {
.btn-outline-primary {
@extend %ui-clear-button;
// set styles
@extend %btn-pl-default-base;
@include font-size(18);
border: 1px solid $blue;
color: $blue;
padding: 11px 14px;
line-height: normal;
margin: 20px 0;
}
.paragon__modal-open {
overflow-y: scroll;
color: $dark-gray;
.paragon__modal-title {
font-weight: $font-semibold;
}
.paragon__modal-body {
line-height: 1.5;
.alert-title {
line-height: 1.5;
}
}
.paragon__alert-warning {
color: $dark-gray;
}
.next-steps {
margin-bottom: 10px;
font-weight: $font-semibold;
}
.confirm-password-input {
width: 50%;
}
.paragon__btn:not(.cancel-btn) {
@extend %btn-primary-blue;
}
}
.modal-alert {
display: flex;
.icon-wrapper {
padding-right: 15px;
}
.alert-content {
.alert-title {
color: $dark-gray;
margin-bottom: 10px;
font: {
size: 1rem;
weight: $font-semibold;
}
}
a {
color: $blue-u1;
}
}
}
.delete-confirmation-wrapper {
.paragon__modal-footer {
.paragon__btn-outline-primary {
@extend %ui-clear-button;
// set styles
@extend %btn-pl-default-base;
@include margin-left(25px);
border-color: $blue;
color: $blue;
padding: 11px 14px;
line-height: normal;
}
}
}
}
&:last-child {
border-bottom: none;
}
}
}
// * +Alert Messages
.account-settings-message,
.account-settings-section-message {
font-size: 16px;
line-height: 22px;
margin-top: 15px;
margin-bottom: 30px;
.alert-message {
color: #292b2c;
font-family: $font-family-sans-serif;
position: relative;
padding: 10px 10px 10px 35px;
border: 1px solid transparent;
border-radius: 0;
box-shadow: none;
margin-bottom: 8px;
& > .fa {
position: absolute;
left: 11px;
top: 13px;
font-size: 16px;
}
span {
display: block;
a {
text-decoration: underline;
}
}
}
.success {
background-color: #ecfaec;
border-color: #b9edb9;
}
.info {
background-color: #d8edf8;
border-color: #bbdff2;
}
.warning {
background-color: #fcf8e3;
border-color: #faebcc;
}
.error {
background-color: #f2dede;
border-color: #ebccd1;
}
}
.account-settings-message {
margin-bottom: 0;
.alert-message {
padding: 10px;
.alert-actions {
margin-top: 10px;
.btn-alert-primary {
@extend %btn-primary-blue;
@include font-size(18);
border: 1px solid $m-blue-d3;
border-radius: 3px;
box-shadow: none;
padding: 11px 14px;
line-height: normal;
}
.btn-alert-secondary {
@extend %ui-clear-button;
// set styles
@extend %btn-pl-default-base;
@include font-size(18);
background-color: white;
border: 1px solid $blue;
color: $blue;
padding: 11px 14px;
line-height: normal;
}
}
}
}

View File

@@ -1,14 +0,0 @@
<%page expression_filter="h"/>
<%! from django.utils.translation import ugettext as _ %>
<div class="wrapper-msg urgency-high">
<div class="msg">
<div class="msg-content">
<h2 class="sr">${_("Could Not Link Accounts")}</h2>
<div class="copy">
## Translators: this message is displayed when a user tries to link their account with a third-party authentication provider (for example, Google or LinkedIn) with a given edX account, but their third-party account is already associated with another edX account. provider_name is the name of the third-party authentication provider, and platform_name is the name of the edX deployment.
<p>${_("The {provider_name} account you selected is already linked to another {platform_name} account.").format(provider_name=duplicate_provider, platform_name=platform_name)}</p>
</div>
</div>
</div>
</div>

View File

@@ -4,13 +4,13 @@
<%!
import json
from urllib.parse import urljoin
from django.conf import settings
from django.urls import reverse
from django.utils.translation import gettext as _
from openedx.core.djangoapps.user_api.accounts.image_helpers import get_profile_image_urls_for_user
from openedx.core.djangoapps.user_api.accounts.toggles import should_redirect_to_order_history_microfrontend
from openedx.core.djangoapps.user_api.accounts.utils import retrieve_last_sitewide_block_completed
from openedx.features.enterprise_support.utils import get_enterprise_learner_generic_name, get_enterprise_learner_portal
%>
@@ -25,7 +25,7 @@ displayname = get_enterprise_learner_generic_name(request) or username
enterprise_customer_portal = get_enterprise_learner_portal(request)
## Enterprises with the learner portal enabled should not show order history, as it does
## not apply to the learner's method of purchasing content.
should_show_order_history = should_redirect_to_order_history_microfrontend() and not enterprise_customer_portal
should_show_order_history = not enterprise_customer_portal
%>
<div class="nav-item hidden-mobile">
@@ -49,8 +49,8 @@ should_show_order_history = should_redirect_to_order_history_microfrontend() and
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${settings.ENTERPRISE_LEARNER_PORTAL_BASE_URL}/${enterprise_customer_portal.get('slug')}" role="menuitem">${_("Dashboard")}</a></div>
% endif
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('learner_profile', kwargs={'username': username})}" role="menuitem">${_("Profile")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${reverse('account_settings')}" role="menuitem">${_("Account")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${urljoin(settings.PROFILE_MICROFRONTEND_URL, f'/u/{user.username}')}" role="menuitem">${_("Profile")}</a></div>
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${settings.ACCOUNT_MICROFRONTEND_URL}" role="menuitem">${_("Account")}</a></div>
% if should_show_order_history:
<div class="mobile-nav-item dropdown-item dropdown-nav-item"><a href="${settings.ORDER_HISTORY_MICROFRONTEND_URL}" role="menuitem">${_("Order History")}</a></div>
% endif

View File

@@ -1,91 +0,0 @@
<%page expression_filter="h"/>
<%!
import json
from django.urls import reverse
from django.conf import settings
from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
from openedx.core.djangolib.markup import HTML
from webpack_loader.templatetags.webpack_loader import render_bundle
from openedx.core.djangoapps.user_api.accounts.utils import is_secondary_email_feature_enabled
%>
<%inherit file="/main.html" />
<%def name="online_help_token()"><% return "learneraccountsettings" %></%def>
<%namespace name='static' file='/static_content.html'/>
<%block name="pagetitle">${_("Account Settings")}</%block>
% if duplicate_provider:
<section>
<%include file='/dashboard/_dashboard_third_party_error.html' />
</section>
% endif
<div class="wrapper-account-settings"></div>
<%block name="headextra">
<%static:css group='style-course'/>
<link type="text/css" rel="stylesheet" href="${STATIC_URL}paragon/static/paragon.min.css">
</%block>
<%block name="js_extra">
<%static:require_module module_name="js/student_account/views/account_settings_factory" class_name="AccountSettingsFactory">
var fieldsData = ${ fields | n, dump_js_escaped_json },
ordersHistoryData = ${ order_history | n, dump_js_escaped_json },
authData = ${ auth | n, dump_js_escaped_json },
platformName = '${ static.get_platform_name() | n, js_escaped_string }',
contactEmail = '${ static.get_contact_email_address() | n, js_escaped_string }',
allowEmailChange = ${ bool(settings.FEATURES['ALLOW_EMAIL_ADDRESS_CHANGE']) | n, dump_js_escaped_json },
socialPlatforms = ${ settings.SOCIAL_PLATFORMS | n, dump_js_escaped_json },
syncLearnerProfileData = ${ bool(sync_learner_profile_data) | n, dump_js_escaped_json },
enterpriseName = '${ enterprise_name | n, js_escaped_string }',
enterpriseReadonlyAccountFields = ${ enterprise_readonly_account_fields | n, dump_js_escaped_json },
edxSupportUrl = '${ edx_support_url | n, js_escaped_string }',
extendedProfileFields = ${ extended_profile_fields | n, dump_js_escaped_json },
displayAccountDeletion = ${ enable_account_deletion | n, dump_js_escaped_json};
isSecondaryEmailFeatureEnabled = ${ bool(is_secondary_email_feature_enabled()) | n, dump_js_escaped_json },
enableCoppaCompliance = ${ bool(enable_coppa_compliance) | n, dump_js_escaped_json },
AccountSettingsFactory(
fieldsData,
${ disable_order_history_tab | n, dump_js_escaped_json },
ordersHistoryData,
authData,
'${ password_reset_support_link | n, js_escaped_string }',
'${ user_accounts_api_url | n, js_escaped_string }',
'${ user_preferences_api_url | n, js_escaped_string }',
${ user.id | n, dump_js_escaped_json },
platformName,
contactEmail,
allowEmailChange,
enableCoppaCompliance,
socialPlatforms,
syncLearnerProfileData,
enterpriseName,
enterpriseReadonlyAccountFields,
edxSupportUrl,
extendedProfileFields,
displayAccountDeletion,
isSecondaryEmailFeatureEnabled,
${ beta_language | n, dump_js_escaped_json },
);
</%static:require_module>
<script type="text/javascript">
window.auth = ${ auth | n, dump_js_escaped_json };
window.isActive = ${ user.is_active | n, dump_js_escaped_json };
window.additionalSiteSpecificDeletionText = "${ static.get_value('SITE_SPECIFIC_DELETION_TEXT', _(' and access to private sites offered by MIT Open Learning, Wharton Executive Education, and Harvard Medical School')) | n, js_escaped_string }";
window.mktgRootLink = "${ static.marketing_link('ROOT') | n, js_escaped_string }";
window.platformName = "${ platform_name | n, js_escaped_string }";
window.siteName = "${ static.get_value('SITE_NAME', settings.SITE_NAME) | n, js_escaped_string }";
window.mktgEmailOptIn = ${ settings.MARKETING_EMAILS_OPT_IN | n, dump_js_escaped_json };;
</script>
<%static:webpack entry="StudentAccountDeletionInitializer">
</%static:webpack>
</%block>

View File

@@ -1,30 +0,0 @@
<main id="main" aria-label="Content" tabindex="-1">
<div class="account-settings-container">
<% if (message) { %>
<div class="account-settings-message">
<div id="beta-language-message" class="alert-message warning" aria-live="assertive" role="alert">
<span><%= HtmlUtils.ensureHtml(message) %></span>
<div class="alert-actions">
<button class="btn-alert-primary" data-old-lang-code="<%- oldLangCode %>"><%- gettext('Switch Language Back') %></button>
<a href="<%- helpTranslateLink %>" rel="noopener" target="_blank" class="btn-alert-secondary"><%= HtmlUtils.ensureHtml(helpTranslateText) %></a>
</div>
</div>
</div>
<% } %>
<div class="wrapper-header">
<h2 class="header-title"><%- gettext("Account Settings") %></h2>
<div class="left list-inline account-nav" role="tablist">
<% _.each(accountSettingsTabs, function(tab) { %>
<button id="<%- tab.id %>" aria-controls="<%- tab.name %>-tabpanel" tabindex="<%- tab.tabindex %>" aria-selected="<%- tab.selected %>" aria-expanded="<%- tab.expanded %>" data-name="<%- tab.name %>" aria-describedby="header-subtitle-<%- tab.name %>" class="tab account-nav-link <%- tab.class %>" role="tab">
<%- tab.label %>
</button>
<% }); %>
</div>
</div>
<div class="account-settings-sections">
<% _.each(accountSettingsTabs, function(tab) { %>
<div id="<%- tab.name %>-tabpanel" class="account-settings-tabpanels <% if (!tab.class) { %> hidden <% } %>" aria-label="<%- tab.label %>" role="tabpanel"></div>
<% }); %>
</div>
</div>
</main>

View File

@@ -1,31 +0,0 @@
<h2 class="sr" id="header-subtitle-<%- tabName %>">
<%- tabLabel %>
</h2>
<% _.each(sections, function(section) { %>
<div class="section">
<h3 class="section-header"><%- gettext(section.title) %></h3>
<% if (section.subtitle && _.isUndefined(section.message)) { %>
<p class="account-settings-header-subtitle"><%- section.subtitle %></p>
<% } %>
<% if (section.message) { %>
<div class="account-settings-section-message">
<div class="alert-message <%- section.messageType%>" aria-live="polite">
<i class="fa fa-info-circle message-icon <%- section.messageType %>" aria-hidden="true"></i>
<span><%= HtmlUtils.ensureHtml(section.message) %></span>
</div>
</div>
<% } %>
<% if (section.domHookId) { %>
<div id="<%- section.domHookId %>"></div>
<% } %>
<div class="account-settings-section-body <%- tabName %>-section-body">
<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. Please reload the page.") %></span>
</div>
</div>
</div>
<% }); %>

View File

@@ -28,7 +28,7 @@ profile_image_url = get_profile_image_urls_for_user(self.real_user)['medium']
<a data-hj-suppress class="dropdown-toggle" id="navbarDropdownMenuLink" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">${username}</a>
<ul role="menu" class="dropdown-menu dropdown-menu-right" id="${_("Usermenu")}" aria-labelledby="dropdownMenuLink" tabindex="-1">
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${reverse('dashboard')}">${_("Dashboard")}</a></li>
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${reverse('account_settings')}">${_("Account")}</a></li>
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${settings.ACCOUNT_MICROFRONTEND_URL}">${_("Account")}</a></li>
<li role="presentation"><a role="menuitem" class="dropdown-item" href="${reverse('logout')}">${_("Sign Out")}</a></li>
</ul>
</div>
@@ -36,7 +36,7 @@ profile_image_url = get_profile_image_urls_for_user(self.real_user)['medium']
</div>
<ul role="menu" class="nav flex-column align-items-center">
<li role="presentation" class="nav-item nav-item-open-collapsed-only collapse"><a role="menuitem" href="${reverse('dashboard')}">${_("Dashboard")}</a></li>
<li role="presentation" class="nav-item nav-item-open-collapsed-only"><a role="menuitem" href="${reverse('account_settings')}">${_("Account")}</a></li>
<li role="presentation" class="nav-item nav-item-open-collapsed-only"><a role="menuitem" href="${settings.ACCOUNT_MICROFRONTEND_URL}">${_("Account")}</a></li>
<li role="presentation" class="nav-item nav-item-open-collapsed-only"><a role="menuitem" href="${reverse('logout')}">${_("Sign Out")}</a></li>
</ul>
% else:

View File

@@ -667,12 +667,6 @@ urlpatterns += [
include('openedx.features.calendar_sync.urls'),
),
# Learner profile
path(
'u/',
include('openedx.features.learner_profile.urls'),
),
# Survey Report
re_path(
fr'^survey_report/',

View File

@@ -43,21 +43,6 @@ class TestComprehensiveThemeLMS(TestCase):
# This string comes from header.html of test-theme
self.assertContains(resp, "This is a footer for test-theme.")
@with_comprehensive_theme("edx.org")
def test_account_settings_hide_nav(self):
"""
Test that theme header doesn't show marketing site links for Account Settings page.
"""
self._login()
account_settings_url = reverse('account_settings')
response = self.client.get(account_settings_url)
# Verify that the header navigation links are hidden for the edx.org version
self.assertNotContains(response, "How it Works")
self.assertNotContains(response, "Find courses")
self.assertNotContains(response, "Schools & Partners")
@with_comprehensive_theme("test-theme")
def test_logo_image(self):
"""

View File

@@ -1,279 +0,0 @@
""" Views related to Account Settings. """
import logging
import urllib
from datetime import datetime
from django.conf import settings
from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.shortcuts import redirect
from django.urls import reverse
from django.utils.translation import gettext as _
from django.views.decorators.http import require_http_methods
from django_countries import countries
from common.djangoapps import third_party_auth
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.student.models import UserProfile
from common.djangoapps.third_party_auth import pipeline
from common.djangoapps.util.date_utils import strftime_localized
from lms.djangoapps.commerce.models import CommerceConfiguration
from lms.djangoapps.commerce.utils import EcommerceService
from openedx.core.djangoapps.commerce.utils import get_ecommerce_api_base_url, get_ecommerce_api_client
from openedx.core.djangoapps.dark_lang.models import DarkLangConfig
from openedx.core.djangoapps.lang_pref.api import all_languages, released_languages
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api.accounts.toggles import (
should_redirect_to_account_microfrontend,
should_redirect_to_order_history_microfrontend
)
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences
from openedx.core.lib.edx_api_utils import get_api_data
from openedx.core.lib.time_zone_utils import TIME_ZONE_CHOICES
from openedx.features.enterprise_support.api import enterprise_customer_for_request
from openedx.features.enterprise_support.utils import update_account_settings_context_for_enterprise
log = logging.getLogger(__name__)
@login_required
@require_http_methods(['GET'])
def account_settings(request):
"""Render the current user's account settings page.
Args:
request (HttpRequest)
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/settings
"""
if should_redirect_to_account_microfrontend():
url = settings.ACCOUNT_MICROFRONTEND_URL
duplicate_provider = pipeline.get_duplicate_provider(messages.get_messages(request))
if duplicate_provider:
url = '{url}?{params}'.format(
url=url,
params=urllib.parse.urlencode({
'duplicate_provider': duplicate_provider,
}),
)
return redirect(url)
context = account_settings_context(request)
return render_to_response('student_account/account_settings.html', context)
def account_settings_context(request):
""" Context for the account settings page.
Args:
request: The request object.
Returns:
dict
"""
user = request.user
year_of_birth_options = [(str(year), str(year)) for year in UserProfile.VALID_YEARS]
try:
user_orders = get_user_orders(user)
except: # pylint: disable=bare-except
log.exception('Error fetching order history from Otto.')
# Return empty order list as account settings page expect a list and
# it will be broken if exception raised
user_orders = []
beta_language = {}
dark_lang_config = DarkLangConfig.current()
if dark_lang_config.enable_beta_languages:
user_preferences = get_user_preferences(user)
pref_language = user_preferences.get('pref-lang')
if pref_language in dark_lang_config.beta_languages_list:
beta_language['code'] = pref_language
beta_language['name'] = settings.LANGUAGE_DICT.get(pref_language)
context = {
'auth': {},
'duplicate_provider': None,
'nav_hidden': True,
'fields': {
'country': {
'options': list(countries),
}, 'gender': {
'options': [(choice[0], _(choice[1])) for choice in UserProfile.GENDER_CHOICES], # lint-amnesty, pylint: disable=translation-of-non-string
}, 'language': {
'options': released_languages(),
}, 'level_of_education': {
'options': [(choice[0], _(choice[1])) for choice in UserProfile.LEVEL_OF_EDUCATION_CHOICES], # lint-amnesty, pylint: disable=translation-of-non-string
}, 'password': {
'url': reverse('password_reset'),
}, 'year_of_birth': {
'options': year_of_birth_options,
}, 'preferred_language': {
'options': all_languages(),
}, 'time_zone': {
'options': TIME_ZONE_CHOICES,
}
},
'platform_name': configuration_helpers.get_value('PLATFORM_NAME', settings.PLATFORM_NAME),
'password_reset_support_link': configuration_helpers.get_value(
'PASSWORD_RESET_SUPPORT_LINK', settings.PASSWORD_RESET_SUPPORT_LINK
) or settings.SUPPORT_SITE_LINK,
'user_accounts_api_url': reverse("accounts_api", kwargs={'username': user.username}),
'user_preferences_api_url': reverse('preferences_api', kwargs={'username': user.username}),
'disable_courseware_js': True,
'show_program_listing': ProgramsApiConfig.is_enabled(),
'show_dashboard_tabs': True,
'order_history': user_orders,
'disable_order_history_tab': should_redirect_to_order_history_microfrontend(),
'enable_account_deletion': configuration_helpers.get_value(
'ENABLE_ACCOUNT_DELETION', settings.FEATURES.get('ENABLE_ACCOUNT_DELETION', False)
),
'extended_profile_fields': _get_extended_profile_fields(),
'beta_language': beta_language,
'enable_coppa_compliance': settings.ENABLE_COPPA_COMPLIANCE,
}
enterprise_customer = enterprise_customer_for_request(request)
update_account_settings_context_for_enterprise(context, enterprise_customer, user)
if third_party_auth.is_enabled():
# If the account on the third party provider is already connected with another edX account,
# we display a message to the user.
context['duplicate_provider'] = pipeline.get_duplicate_provider(messages.get_messages(request))
auth_states = pipeline.get_provider_user_states(user)
context['auth']['providers'] = [{
'id': state.provider.provider_id,
'name': state.provider.name, # The name of the provider e.g. Facebook
'connected': state.has_account, # Whether the user's edX account is connected with the provider.
# If the user is not connected, they should be directed to this page to authenticate
# with the particular provider, as long as the provider supports initiating a login.
'connect_url': pipeline.get_login_url(
state.provider.provider_id,
pipeline.AUTH_ENTRY_ACCOUNT_SETTINGS,
# The url the user should be directed to after the auth process has completed.
redirect_url=reverse('account_settings'),
),
'accepts_logins': state.provider.accepts_logins,
# If the user is connected, sending a POST request to this url removes the connection
# information for this provider from their edX account.
'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id),
# We only want to include providers if they are either currently available to be logged
# in with, or if the user is already authenticated with them.
} for state in auth_states if state.provider.display_for_login or state.has_account]
return context
def get_user_orders(user):
"""Given a user, get the detail of all the orders from the Ecommerce service.
Args:
user (User): The user to authenticate as when requesting ecommerce.
Returns:
list of dict, representing orders returned by the Ecommerce service.
"""
user_orders = []
commerce_configuration = CommerceConfiguration.current()
user_query = {'username': user.username}
use_cache = commerce_configuration.is_cache_enabled
cache_key = commerce_configuration.CACHE_KEY + '.' + str(user.id) if use_cache else None
commerce_user_orders = get_api_data(
commerce_configuration,
'orders',
api_client=get_ecommerce_api_client(user),
base_api_url=get_ecommerce_api_base_url(),
querystring=user_query,
cache_key=cache_key
)
for order in commerce_user_orders:
if order['status'].lower() == 'complete':
date_placed = datetime.strptime(order['date_placed'], "%Y-%m-%dT%H:%M:%SZ")
order_data = {
'number': order['number'],
'price': order['total_excl_tax'],
'order_date': strftime_localized(date_placed, 'SHORT_DATE'),
'receipt_url': EcommerceService().get_receipt_page_url(order['number']),
'lines': order['lines'],
}
user_orders.append(order_data)
return user_orders
def _get_extended_profile_fields():
"""Retrieve the extended profile fields from site configuration to be shown on the
Account Settings page
Returns:
A list of dicts. Each dict corresponds to a single field. The keys per field are:
"field_name" : name of the field stored in user_profile.meta
"field_label" : The label of the field.
"field_type" : TextField or ListField
"field_options": a list of tuples for options in the dropdown in case of ListField
"""
extended_profile_fields = []
fields_already_showing = ['username', 'name', 'email', 'pref-lang', 'country', 'time_zone', 'level_of_education',
'gender', 'year_of_birth', 'language_proficiencies', 'social_links']
field_labels_map = {
"first_name": _("First Name"),
"last_name": _("Last Name"),
"city": _("City"),
"state": _("State/Province/Region"),
"company": _("Company"),
"title": _("Title"),
"job_title": _("Job Title"),
"mailing_address": _("Mailing address"),
"goals": _("Tell us why you're interested in {platform_name}").format(
platform_name=configuration_helpers.get_value("PLATFORM_NAME", settings.PLATFORM_NAME)
),
"profession": _("Profession"),
"specialty": _("Specialty")
}
extended_profile_field_names = configuration_helpers.get_value('extended_profile_fields', [])
for field_to_exclude in fields_already_showing:
if field_to_exclude in extended_profile_field_names:
extended_profile_field_names.remove(field_to_exclude)
extended_profile_field_options = configuration_helpers.get_value('EXTRA_FIELD_OPTIONS', [])
extended_profile_field_option_tuples = {}
for field in extended_profile_field_options.keys():
field_options = extended_profile_field_options[field]
extended_profile_field_option_tuples[field] = [(option.lower(), option) for option in field_options]
for field in extended_profile_field_names:
field_dict = {
"field_name": field,
"field_label": field_labels_map.get(field, field),
}
field_options = extended_profile_field_option_tuples.get(field)
if field_options:
field_dict["field_type"] = "ListField"
field_dict["field_options"] = field_options
else:
field_dict["field_type"] = "TextField"
extended_profile_fields.append(field_dict)
return extended_profile_fields

View File

@@ -1,271 +0,0 @@
""" Tests for views related to account settings. """
from unittest import mock
from django.conf import settings
from django.contrib import messages
from django.contrib.messages.middleware import MessageMiddleware
from django.http import HttpRequest
from django.test import TestCase
from django.test.utils import override_settings
from django.urls import reverse
from edx_rest_api_client import exceptions
from edx_toggles.toggles.testutils import override_waffle_flag
from lms.djangoapps.commerce.models import CommerceConfiguration
from lms.djangoapps.commerce.tests import factories
from lms.djangoapps.commerce.tests.mocks import mock_get_orders
from openedx.core.djangoapps.dark_lang.models import DarkLangConfig
from openedx.core.djangoapps.lang_pref.tests.test_api import EN, LT_LT
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from openedx.core.djangoapps.user_api.accounts.settings_views import account_settings_context, get_user_orders
from openedx.core.djangoapps.user_api.accounts.toggles import REDIRECT_TO_ACCOUNT_MICROFRONTEND
from openedx.core.djangoapps.user_api.tests.factories import UserPreferenceFactory
from openedx.core.djangolib.testing.utils import skip_unless_lms
from openedx.features.enterprise_support.utils import get_enterprise_readonly_account_fields
from common.djangoapps.student.tests.factories import UserFactory
from common.djangoapps.third_party_auth.tests.testutil import ThirdPartyAuthTestMixin
@skip_unless_lms
class AccountSettingsViewTest(ThirdPartyAuthTestMixin, SiteMixin, ProgramsApiConfigMixin, TestCase):
""" Tests for the account settings view. """
USERNAME = 'student'
PASSWORD = 'password'
FIELDS = [
'country',
'gender',
'language',
'level_of_education',
'password',
'year_of_birth',
'preferred_language',
'time_zone',
]
@mock.patch("django.conf.settings.MESSAGE_STORAGE", 'django.contrib.messages.storage.cookie.CookieStorage')
def setUp(self): # pylint: disable=arguments-differ
super().setUp()
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
CommerceConfiguration.objects.create(cache_ttl=10, enabled=True)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
self.request = HttpRequest()
self.request.user = self.user
# For these tests, two third party auth providers are enabled by default:
self.configure_google_provider(enabled=True, visible=True)
self.configure_facebook_provider(enabled=True, visible=True)
# Python-social saves auth failure notifcations in Django messages.
# See pipeline.get_duplicate_provider() for details.
self.request.COOKIES = {}
MessageMiddleware().process_request(self.request)
messages.error(self.request, 'Facebook is already in use.', extra_tags='Auth facebook')
@mock.patch('openedx.features.enterprise_support.api.enterprise_customer_for_request')
def test_context(self, mock_enterprise_customer_for_request):
self.request.site = SiteFactory.create()
UserPreferenceFactory(user=self.user, key='pref-lang', value='lt-lt')
DarkLangConfig(
released_languages='en',
changed_by=self.user,
enabled=True,
beta_languages='lt-lt',
enable_beta_languages=True
).save()
mock_enterprise_customer_for_request.return_value = {}
with override_settings(LANGUAGES=[EN, LT_LT], LANGUAGE_CODE='en'):
context = account_settings_context(self.request)
user_accounts_api_url = reverse("accounts_api", kwargs={'username': self.user.username})
assert context['user_accounts_api_url'] == user_accounts_api_url
user_preferences_api_url = reverse('preferences_api', kwargs={'username': self.user.username})
assert context['user_preferences_api_url'] == user_preferences_api_url
for attribute in self.FIELDS:
assert attribute in context['fields']
assert context['user_accounts_api_url'] == reverse('accounts_api', kwargs={'username': self.user.username})
assert context['user_preferences_api_url'] ==\
reverse('preferences_api', kwargs={'username': self.user.username})
assert context['duplicate_provider'] == 'facebook'
assert context['auth']['providers'][0]['name'] == 'Facebook'
assert context['auth']['providers'][1]['name'] == 'Google'
assert context['sync_learner_profile_data'] is False
assert context['edx_support_url'] == settings.SUPPORT_SITE_LINK
assert context['enterprise_name'] is None
assert context['enterprise_readonly_account_fields'] ==\
{'fields': list(get_enterprise_readonly_account_fields(self.user))}
expected_beta_language = {'code': 'lt-lt', 'name': settings.LANGUAGE_DICT.get('lt-lt')}
assert context['beta_language'] == expected_beta_language
@mock.patch('openedx.core.djangoapps.user_api.accounts.settings_views.enterprise_customer_for_request')
@mock.patch('openedx.features.enterprise_support.utils.third_party_auth.provider.Registry.get')
def test_context_for_enterprise_learner(
self, mock_get_auth_provider, mock_enterprise_customer_for_request
):
dummy_enterprise_customer = {
'uuid': 'real-ent-uuid',
'name': 'Dummy Enterprise',
'identity_provider': 'saml-ubc'
}
mock_enterprise_customer_for_request.return_value = dummy_enterprise_customer
self.request.site = SiteFactory.create()
mock_get_auth_provider.return_value.sync_learner_profile_data = True
context = account_settings_context(self.request)
user_accounts_api_url = reverse("accounts_api", kwargs={'username': self.user.username})
assert context['user_accounts_api_url'] == user_accounts_api_url
user_preferences_api_url = reverse('preferences_api', kwargs={'username': self.user.username})
assert context['user_preferences_api_url'] == user_preferences_api_url
for attribute in self.FIELDS:
assert attribute in context['fields']
assert context['user_accounts_api_url'] == reverse('accounts_api', kwargs={'username': self.user.username})
assert context['user_preferences_api_url'] ==\
reverse('preferences_api', kwargs={'username': self.user.username})
assert context['duplicate_provider'] == 'facebook'
assert context['auth']['providers'][0]['name'] == 'Facebook'
assert context['auth']['providers'][1]['name'] == 'Google'
assert context['sync_learner_profile_data'] == mock_get_auth_provider.return_value.sync_learner_profile_data
assert context['edx_support_url'] == settings.SUPPORT_SITE_LINK
assert context['enterprise_name'] == dummy_enterprise_customer['name']
assert context['enterprise_readonly_account_fields'] ==\
{'fields': list(get_enterprise_readonly_account_fields(self.user))}
def test_view(self):
"""
Test that all fields are visible
"""
view_path = reverse('account_settings')
response = self.client.get(path=view_path)
for attribute in self.FIELDS:
self.assertContains(response, attribute)
def test_header_with_programs_listing_enabled(self):
"""
Verify that tabs header will be shown while program listing is enabled.
"""
self.create_programs_config()
view_path = reverse('account_settings')
response = self.client.get(path=view_path)
self.assertContains(response, 'global-header')
def test_header_with_programs_listing_disabled(self):
"""
Verify that nav header will be shown while program listing is disabled.
"""
self.create_programs_config(enabled=False)
view_path = reverse('account_settings')
response = self.client.get(path=view_path)
self.assertContains(response, 'global-header')
def test_commerce_order_detail(self):
"""
Verify that get_user_orders returns the correct order data.
"""
with mock_get_orders():
order_detail = get_user_orders(self.user)
for i, order in enumerate(mock_get_orders.default_response['results']):
expected = {
'number': order['number'],
'price': order['total_excl_tax'],
'order_date': 'Jan 01, 2016',
'receipt_url': '/checkout/receipt/?order_number=' + order['number'],
'lines': order['lines'],
}
assert order_detail[i] == expected
def test_commerce_order_detail_exception(self):
with mock_get_orders(exception=exceptions.HttpNotFoundError):
order_detail = get_user_orders(self.user)
assert not order_detail
def test_incomplete_order_detail(self):
response = {
'results': [
factories.OrderFactory(
status='Incomplete',
lines=[
factories.OrderLineFactory(
product=factories.ProductFactory(attribute_values=[factories.ProductAttributeFactory()])
)
]
)
]
}
with mock_get_orders(response=response):
order_detail = get_user_orders(self.user)
assert not order_detail
def test_order_history_with_no_product(self):
response = {
'results': [
factories.OrderFactory(
lines=[
factories.OrderLineFactory(
product=None
),
factories.OrderLineFactory(
product=factories.ProductFactory(attribute_values=[factories.ProductAttributeFactory(
name='certificate_type',
value='verified'
)])
)
]
)
]
}
with mock_get_orders(response=response):
order_detail = get_user_orders(self.user)
assert len(order_detail) == 1
def test_redirect_view(self):
old_url_path = reverse('account_settings')
with override_waffle_flag(REDIRECT_TO_ACCOUNT_MICROFRONTEND, active=True):
# Test with waffle flag active and none site setting, redirects to microfrontend
response = self.client.get(path=old_url_path)
self.assertRedirects(response, settings.ACCOUNT_MICROFRONTEND_URL, fetch_redirect_response=False)
# Test with waffle flag disabled and site setting disabled, does not redirect
response = self.client.get(path=old_url_path)
for attribute in self.FIELDS:
self.assertContains(response, attribute)
# Test with site setting disabled, does not redirect
site_domain = 'othersite.example.com'
site = self.set_up_site(site_domain, {
'SITE_NAME': site_domain,
'ENABLE_ACCOUNT_MICROFRONTEND': False
})
self.client.login(username=self.USERNAME, password=self.PASSWORD)
response = self.client.get(path=old_url_path)
for attribute in self.FIELDS:
self.assertContains(response, attribute)
# Test with site setting enabled, redirects to microfrontend
site.configuration.site_values['ENABLE_ACCOUNT_MICROFRONTEND'] = True
site.configuration.save()
site.__class__.objects.clear_cache()
response = self.client.get(path=old_url_path)
self.assertRedirects(response, settings.ACCOUNT_MICROFRONTEND_URL, fetch_redirect_response=False)

View File

@@ -1,44 +0,0 @@
"""
Toggles for accounts related code.
"""
from edx_toggles.toggles import WaffleFlag
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
# .. toggle_name: order_history.redirect_to_microfrontend
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Supports staged rollout of a new micro-frontend-based implementation of the order history page.
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 2019-04-11
# .. toggle_target_removal_date: 2020-12-31
# .. toggle_warning: Also set settings.ORDER_HISTORY_MICROFRONTEND_URL and site's
# ENABLE_ORDER_HISTORY_MICROFRONTEND.
# .. toggle_tickets: DEPR-17
REDIRECT_TO_ORDER_HISTORY_MICROFRONTEND = WaffleFlag('order_history.redirect_to_microfrontend', __name__)
def should_redirect_to_order_history_microfrontend():
return (
configuration_helpers.get_value('ENABLE_ORDER_HISTORY_MICROFRONTEND') and
REDIRECT_TO_ORDER_HISTORY_MICROFRONTEND.is_enabled()
)
# .. toggle_name: account.redirect_to_microfrontend
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Supports staged rollout of a new micro-frontend-based implementation of the account page.
# Its action can be overridden using site's ENABLE_ACCOUNT_MICROFRONTEND setting.
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 2019-04-30
# .. toggle_target_removal_date: 2021-12-31
# .. toggle_warning: Also set settings.ACCOUNT_MICROFRONTEND_URL.
# .. toggle_tickets: DEPR-17
REDIRECT_TO_ACCOUNT_MICROFRONTEND = WaffleFlag('account.redirect_to_microfrontend', __name__)
def should_redirect_to_account_microfrontend():
return configuration_helpers.get_value('ENABLE_ACCOUNT_MICROFRONTEND',
REDIRECT_TO_ACCOUNT_MICROFRONTEND.is_enabled())

View File

@@ -5,7 +5,6 @@ from django.urls import path, re_path, include
from rest_framework import routers
from . import views as user_api_views
from .accounts.settings_views import account_settings
from .models import UserPreference
USER_API_ROUTER = routers.DefaultRouter()
@@ -13,7 +12,6 @@ USER_API_ROUTER.register(r'users', user_api_views.UserViewSet)
USER_API_ROUTER.register(r'user_prefs', user_api_views.UserPreferenceViewSet)
urlpatterns = [
path('account/settings', account_settings, name='account_settings'),
path('user_api/v1/', include(USER_API_ROUTER.urls)),
re_path(
fr'^user_api/v1/preferences/(?P<pref_key>{UserPreference.KEY_REGEX})/users/$',

View File

@@ -6,6 +6,7 @@ Utility functions for setting "logged in" cookies used by subdomains.
import json
import logging
import time
from urllib.parse import urljoin
from django.conf import settings
from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user
@@ -244,8 +245,8 @@ def _get_user_info_cookie_data(request, user):
# External sites will need to have fallback mechanisms to handle this case
# (most likely just hiding the links).
try:
header_urls['account_settings'] = reverse('account_settings')
header_urls['learner_profile'] = reverse('learner_profile', kwargs={'username': user.username})
header_urls['account_settings'] = settings.ACCOUNT_MICROFRONTEND_URL
header_urls['learner_profile'] = urljoin(settings.PROFILE_MICROFRONTEND_URL, f'/u/{user.username}')
except NoReverseMatch:
pass

View File

@@ -4,6 +4,7 @@
from datetime import date
import json
from unittest.mock import MagicMock, patch
from urllib.parse import urljoin
from django.conf import settings
from django.http import HttpResponse
from django.test import RequestFactory, TestCase
@@ -57,8 +58,8 @@ class CookieTests(TestCase):
def _get_expected_header_urls(self):
expected_header_urls = {
'logout': reverse('logout'),
'account_settings': reverse('account_settings'),
'learner_profile': reverse('learner_profile', kwargs={'username': self.user.username}),
'account_settings': settings.ACCOUNT_MICROFRONTEND_URL,
'learner_profile': urljoin(settings.PROFILE_MICROFRONTEND_URL, f'/u/{self.user.username}'),
}
block_url = retrieve_last_sitewide_block_completed(self.user)
if block_url:

View File

@@ -492,7 +492,7 @@ class LoginTest(SiteMixin, CacheIsolationTestCase, OpenEdxEventsTestMixin):
# Check that the URLs are absolute
for url in user_info["header_urls"].values():
assert 'http://testserver/' in url
assert 'http://' in url
def test_logout_deletes_mktg_cookies(self):
response, _ = self._login_response(self.user_email, self.password)

View File

@@ -1,8 +0,0 @@
Learner Profile
---------------
This directory contains a Django application that provides a view to render
a profile for any Open edX learner. See `Exploring Your Dashboard and Profile`_
for more details.
.. _Exploring Your Dashboard and Profile: https://edx.readthedocs.io/projects/open-edx-learner-guide/en/latest/SFD_dashboard_profile_SectionHead.html?highlight=profile

View File

@@ -1,40 +0,0 @@
<div class="message-banner" aria-live="polite"></div>
<div class="wrapper-profile">
<div class="profile profile-other">
<div class="wrapper-profile-field-account-privacy"></div>
<div class="wrapper-profile-sections account-settings-container">
<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">An error occurred. Try loading the page again.</span>
</div>
</div>
<div class="wrapper-profile-section-container-two">
<div class="wrapper-profile-bio">
</div>
</div>
</div>
</div>
<div class="ui-loading-indicator">
<p>
<span class="spin">
<span class="icon fa fa-refresh" aria-hidden="true"></span>
</span>
<span class="copy">
Loading
</span>
</p>
</div>
<div class="ui-loading-error is-hidden">
<span class="fa fa-exclamation-triangle message-error" aria-hidden="true"></span>
<span class="copy">
An error occurred. Please reload the page.
</span>
</div>
</div>

View File

@@ -1,246 +0,0 @@
(function (define){
'use strict';
define([
'gettext',
'jquery',
'underscore',
'backbone',
'logger',
'edx-ui-toolkit/js/utils/string-utils',
'edx-ui-toolkit/js/pagination/paging-collection',
'js/student_account/models/user_account_model',
'js/student_account/models/user_preferences_model',
'js/views/fields',
'learner_profile/js/views/learner_profile_fields',
'learner_profile/js/views/learner_profile_view',
'learner_profile/js/models/badges_model',
'learner_profile/js/views/badge_list_container',
'js/student_account/views/account_settings_fields',
'js/views/message_banner',
'string_utils'
], function (gettext, $, _, Backbone, Logger, StringUtils, PagingCollection, AccountSettingsModel,
AccountPreferencesModel, FieldsView, LearnerProfileFieldsView, LearnerProfileView, BadgeModel,
BadgeListContainer, AccountSettingsFieldViews, MessageBannerView){
return function (options) {
var $learnerProfileElement = $('.wrapper-profile');
var accountSettingsModel = new AccountSettingsModel(
_.extend(
options.account_settings_data,
{
default_public_account_fields: options.default_public_account_fields,
parental_consent_age_limit: options.parental_consent_age_limit,
enable_coppa_compliance: options.enable_coppa_compliance
}
),
{parse: true}
);
var AccountPreferencesModelWithDefaults = AccountPreferencesModel.extend({
defaults: {
account_privacy: options.default_visibility
}
});
var accountPreferencesModel = new AccountPreferencesModelWithDefaults(options.preferences_data);
var editable = options.own_profile ? 'toggle' : 'never';
var messageView = new MessageBannerView({
el: $('.message-banner')
});
var accountPrivacyFieldView,
profileImageFieldView,
usernameFieldView,
nameFieldView,
sectionOneFieldViews,
sectionTwoFieldViews,
BadgeCollection,
badgeCollection,
badgeListContainer,
learnerProfileView,
getProfileVisibility,
showLearnerProfileView;
accountSettingsModel.url = options.accounts_api_url;
accountPreferencesModel.url = options.preferences_api_url;
accountPrivacyFieldView = new LearnerProfileFieldsView.AccountPrivacyFieldView({
model: accountPreferencesModel,
required: true,
editable: 'always',
showMessages: false,
title: gettext('Profile Visibility:'),
valueAttribute: 'account_privacy',
options: [
['private', gettext('Limited Profile')],
['all_users', gettext('Full Profile')]
],
helpMessage: '',
accountSettingsPageUrl: options.account_settings_page_url,
persistChanges: true
});
profileImageFieldView = new LearnerProfileFieldsView.ProfileImageFieldView({
model: accountSettingsModel,
valueAttribute: 'profile_image',
editable: editable === 'toggle',
messageView: messageView,
imageMaxBytes: options.profile_image_max_bytes,
imageMinBytes: options.profile_image_min_bytes,
imageUploadUrl: options.profile_image_upload_url,
imageRemoveUrl: options.profile_image_remove_url
});
usernameFieldView = new FieldsView.ReadonlyFieldView({
model: accountSettingsModel,
screenReaderTitle: gettext('Username'),
valueAttribute: 'username',
helpMessage: ''
});
nameFieldView = new FieldsView.ReadonlyFieldView({
model: accountSettingsModel,
screenReaderTitle: gettext('Full Name'),
valueAttribute: 'name',
helpMessage: ''
});
sectionOneFieldViews = [
new LearnerProfileFieldsView.SocialLinkIconsView({
model: accountSettingsModel,
socialPlatforms: options.social_platforms,
ownProfile: options.own_profile
}),
new FieldsView.DateFieldView({
title: gettext('Joined'),
titleVisible: true,
model: accountSettingsModel,
screenReaderTitle: gettext('Joined Date'),
valueAttribute: 'date_joined',
helpMessage: '',
userLanguage: accountSettingsModel.get('language'),
userTimezone: accountPreferencesModel.get('time_zone'),
dateFormat: 'MMMM YYYY' // not localized, but hopefully ok.
}),
new FieldsView.DropdownFieldView({
title: gettext('Location'),
titleVisible: true,
model: accountSettingsModel,
screenReaderTitle: gettext('Country'),
required: true,
editable: editable,
showMessages: false,
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'),
required: false,
editable: editable,
showMessages: false,
placeholderValue: gettext('Add language'),
valueAttribute: 'language_proficiencies',
options: options.language_options,
helpMessage: '',
persistChanges: true
})
];
sectionTwoFieldViews = [
new FieldsView.TextareaFieldView({
model: accountSettingsModel,
editable: editable,
showMessages: false,
title: gettext('About me'),
// eslint-disable-next-line max-len
placeholderValue: gettext("Tell other learners a little about yourself: where you live, what your interests are, why you're taking courses, or what you hope to learn."),
valueAttribute: 'bio',
helpMessage: '',
persistChanges: true,
messagePosition: 'header',
maxCharacters: 300
})
];
BadgeCollection = PagingCollection.extend({
queryParams: {
currentPage: 'current_page'
}
});
badgeCollection = new BadgeCollection();
badgeCollection.url = options.badges_api_url;
badgeListContainer = new BadgeListContainer({
attributes: {class: 'badge-set-display'},
collection: badgeCollection,
find_courses_url: options.find_courses_url,
ownProfile: options.own_profile,
badgeMeta: {
badges_logo: options.badges_logo,
backpack_ui_img: options.backpack_ui_img,
badges_icon: options.badges_icon
}
});
learnerProfileView = new LearnerProfileView({
el: $learnerProfileElement,
ownProfile: options.own_profile,
has_preferences_access: options.has_preferences_access,
accountSettingsModel: accountSettingsModel,
preferencesModel: accountPreferencesModel,
accountPrivacyFieldView: accountPrivacyFieldView,
profileImageFieldView: profileImageFieldView,
usernameFieldView: usernameFieldView,
nameFieldView: nameFieldView,
sectionOneFieldViews: sectionOneFieldViews,
sectionTwoFieldViews: sectionTwoFieldViews,
badgeListContainer: badgeListContainer,
platformName: options.platform_name
});
getProfileVisibility = function (){
if (options.has_preferences_access) {
return accountPreferencesModel.get('account_privacy');
} else {
return accountSettingsModel.get('profile_is_public') ? 'all_users' : 'private';
}
};
showLearnerProfileView = function (){
// Record that the profile page was viewed
Logger.log('edx.user.settings.viewed', {
page: 'profile',
visibility: getProfileVisibility(),
user_id: options.profile_user_id
});
// Render the view for the first time
learnerProfileView.render();
};
if (options.has_preferences_access) {
if (accountSettingsModel.get('requires_parental_consent')) {
accountPreferencesModel.set('account_privacy', 'private');
}
}
showLearnerProfileView();
return {
accountSettingsModel: accountSettingsModel,
accountPreferencesModel: accountPreferencesModel,
learnerProfileView: learnerProfileView,
badgeListContainer: badgeListContainer
};
};
});
}).call(this, define || RequireJS.define);

View File

@@ -1,8 +0,0 @@
(function(define) {
'use strict';
define(['backbone'], function(Backbone) {
var BadgesModel = Backbone.Model.extend({});
return BadgesModel;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,223 +0,0 @@
define(
[
'backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/spec/student_account/helpers',
'learner_profile/js/spec_helpers/helpers',
'js/views/fields',
'js/student_account/models/user_account_model',
'js/student_account/models/user_preferences_model',
'learner_profile/js/views/learner_profile_view',
'learner_profile/js/views/learner_profile_fields',
'learner_profile/js/learner_profile_factory',
'js/views/message_banner'
],
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers, FieldViews,
UserAccountModel, UserPreferencesModel, LearnerProfileView, LearnerProfileFields, LearnerProfilePage) {
'use strict';
describe('edx.user.LearnerProfileFactory', function() {
var createProfilePage;
beforeEach(function() {
loadFixtures('learner_profile/fixtures/learner_profile.html');
});
afterEach(function() {
Backbone.history.stop();
});
createProfilePage = function(ownProfile, options) {
return new LearnerProfilePage({
accounts_api_url: Helpers.USER_ACCOUNTS_API_URL,
preferences_api_url: Helpers.USER_PREFERENCES_API_URL,
badges_api_url: Helpers.BADGES_API_URL,
own_profile: ownProfile,
account_settings_page_url: Helpers.USER_ACCOUNTS_API_URL,
country_options: Helpers.FIELD_OPTIONS,
language_options: Helpers.FIELD_OPTIONS,
has_preferences_access: true,
profile_image_max_bytes: Helpers.IMAGE_MAX_BYTES,
profile_image_min_bytes: Helpers.IMAGE_MIN_BYTES,
profile_image_upload_url: Helpers.IMAGE_UPLOAD_API_URL,
profile_image_remove_url: Helpers.IMAGE_REMOVE_API_URL,
default_visibility: 'all_users',
platform_name: 'edX',
find_courses_url: '/courses/',
account_settings_data: Helpers.createAccountSettingsData(options),
preferences_data: Helpers.createUserPreferencesData()
});
};
it('renders the full profile for a user', function() {
var context,
learnerProfileView;
AjaxHelpers.requests(this);
context = createProfilePage(true);
learnerProfileView = context.learnerProfileView;
// sets the profile for full view.
context.accountPreferencesModel.set({account_privacy: 'all_users'});
LearnerProfileHelpers.expectProfileSectionsAndFieldsToBeRendered(learnerProfileView, false);
});
it("renders the limited profile for undefined 'year_of_birth'", function() {
var context = createProfilePage(true, {year_of_birth: '', requires_parental_consent: true}),
learnerProfileView = context.learnerProfileView;
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
});
it("doesn't show the mode toggle if badges are disabled", function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: false}),
tabbedView = context.learnerProfileView.tabbedView,
learnerProfileView = context.learnerProfileView;
LearnerProfileHelpers.expectTabbedViewToBeUndefined(requests, tabbedView);
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
});
it("doesn't show the mode toggle if badges fail to fetch", function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: false}),
tabbedView = context.learnerProfileView.tabbedView,
learnerProfileView = context.learnerProfileView;
LearnerProfileHelpers.expectTabbedViewToBeUndefined(requests, tabbedView);
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
});
it('renders the mode toggle if there are badges', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
tabbedView = context.learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
LearnerProfileHelpers.expectTabbedViewToBeShown(tabbedView);
});
it('renders the mode toggle if badges enabled but none exist', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
tabbedView = context.learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.emptyBadges);
LearnerProfileHelpers.expectTabbedViewToBeShown(tabbedView);
});
it('displays the badges when the accomplishments toggle is selected', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
learnerProfileView = context.learnerProfileView,
tabbedView = learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.secondPageBadges);
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
tabbedView.$el.find('[data-url="accomplishments"]').click();
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, false);
tabbedView.$el.find('[data-url="about_me"]').click();
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
});
it('displays a placeholder on the last page of badges', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
learnerProfileView = context.learnerProfileView,
tabbedView = learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.thirdPageBadges);
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
tabbedView.$el.find('[data-url="accomplishments"]').click();
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, true);
tabbedView.$el.find('[data-url="about_me"]').click();
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
});
it('displays a placeholder when the accomplishments toggle is selected and no badges exist', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
learnerProfileView = context.learnerProfileView,
tabbedView = learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.emptyBadges);
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
tabbedView.$el.find('[data-url="accomplishments"]').click();
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 0, true);
tabbedView.$el.find('[data-url="about_me"]').click();
LearnerProfileHelpers.expectBadgesHidden(learnerProfileView);
});
it('shows a paginated list of badges', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
learnerProfileView = context.learnerProfileView,
tabbedView = learnerProfileView.tabbedView;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
tabbedView.$el.find('[data-url="accomplishments"]').click();
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, false);
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.firstPageBadges);
});
it('allows forward and backward navigation of badges', function() {
var requests = AjaxHelpers.requests(this),
context = createProfilePage(true, {accomplishments_shared: true}),
learnerProfileView = context.learnerProfileView,
tabbedView = learnerProfileView.tabbedView,
badgeListContainer = context.badgeListContainer;
AjaxHelpers.expectRequest(requests, 'POST', '/event');
AjaxHelpers.respondWithError(requests, 404);
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
tabbedView.$el.find('[data-url="accomplishments"]').click();
badgeListContainer.$el.find('.next-page-link').click();
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.secondPageBadges);
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.secondPageBadges);
badgeListContainer.$el.find('.next-page-link').click();
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.thirdPageBadges);
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, true);
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.thirdPageBadges);
badgeListContainer.$el.find('.previous-page-link').click();
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.secondPageBadges);
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.secondPageBadges);
LearnerProfileHelpers.expectBadgesDisplayed(learnerProfileView, 10, false);
badgeListContainer.$el.find('.previous-page-link').click();
AjaxHelpers.respondWithJson(requests, LearnerProfileHelpers.firstPageBadges);
LearnerProfileHelpers.expectPage(learnerProfileView, LearnerProfileHelpers.firstPageBadges);
});
it('renders the limited profile for under 13 users', function() {
var context = createProfilePage(
true,
{year_of_birth: new Date().getFullYear() - 10, requires_parental_consent: true}
);
var learnerProfileView = context.learnerProfileView;
LearnerProfileHelpers.expectLimitedProfileSectionsAndFieldsToBeRendered(learnerProfileView);
});
});
});

View File

@@ -1,99 +0,0 @@
define([
'backbone',
'jquery',
'underscore',
'URI',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'edx-ui-toolkit/js/pagination/paging-collection',
'learner_profile/js/spec_helpers/helpers',
'learner_profile/js/views/badge_list_container'
],
function(Backbone, $, _, URI, AjaxHelpers, PagingCollection, LearnerProfileHelpers, BadgeListContainer) {
'use strict';
describe('edx.user.BadgeListContainer', function() {
var view;
var createView = function(requests, pageNum, badgeListObject) {
var BadgeCollection = PagingCollection.extend({
queryParams: {
currentPage: 'current_page'
}
});
var badgeCollection = new BadgeCollection();
var models = [];
var badgeListContainer;
var request;
var path;
badgeCollection.url = '/api/badges/v1/assertions/user/staff/';
_.each(_.range(badgeListObject.count), function(idx) {
models.push(LearnerProfileHelpers.makeBadge(idx));
});
badgeListObject.results = models; // eslint-disable-line no-param-reassign
badgeCollection.setPage(pageNum);
request = AjaxHelpers.currentRequest(requests);
path = new URI(request.url).path();
expect(path).toBe('/api/badges/v1/assertions/user/staff/');
AjaxHelpers.respondWithJson(requests, badgeListObject);
badgeListContainer = new BadgeListContainer({
collection: badgeCollection
});
badgeListContainer.render();
return badgeListContainer;
};
afterEach(function() {
view.$el.remove();
});
it('displays all badges', function() {
var requests = AjaxHelpers.requests(this),
badges;
view = createView(requests, 1, {
count: 30,
previous: '/arbitrary/url',
num_pages: 3,
next: null,
start: 20,
current_page: 1,
results: []
});
badges = view.$el.find('div.badge-display');
expect(badges.length).toBe(30);
});
it('displays placeholder on last page', function() {
var requests = AjaxHelpers.requests(this),
placeholder;
view = createView(requests, 3, {
count: 30,
previous: '/arbitrary/url',
num_pages: 3,
next: null,
start: 20,
current_page: 3,
results: []
});
placeholder = view.$el.find('span.accomplishment-placeholder');
expect(placeholder.length).toBe(1);
});
it('does not display placeholder on first page', function() {
var requests = AjaxHelpers.requests(this),
placeholder;
view = createView(requests, 1, {
count: 30,
previous: '/arbitrary/url',
num_pages: 3,
next: null,
start: 0,
current_page: 1,
results: []
});
placeholder = view.$el.find('span.accomplishment-placeholder');
expect(placeholder.length).toBe(0);
});
});
}
);

View File

@@ -1,82 +0,0 @@
define([
'backbone',
'jquery',
'underscore',
'edx-ui-toolkit/js/pagination/paging-collection',
'learner_profile/js/spec_helpers/helpers',
'learner_profile/js/views/badge_list_view'
],
function(Backbone, $, _, PagingCollection, LearnerProfileHelpers, BadgeListView) {
'use strict';
describe('edx.user.BadgeListView', function() {
var view;
var createView = function(badges, pages, page, hasNextPage) {
var badgeCollection = new PagingCollection();
var models = [];
var badgeList;
badgeCollection.url = '/api/badges/v1/assertions/user/staff/';
_.each(badges, function(element) {
models.push(new Backbone.Model(element));
});
badgeCollection.models = models;
badgeCollection.length = badges.length;
badgeCollection.currentPage = page;
badgeCollection.totalPages = pages;
badgeCollection.hasNextPage = function() {
return hasNextPage;
};
badgeList = new BadgeListView({
collection: badgeCollection
});
return badgeList;
};
afterEach(function() {
view.$el.remove();
});
it('there is a single row if there is only one badge', function() {
var rows;
view = createView([LearnerProfileHelpers.makeBadge(1)], 1, 1, false);
view.render();
rows = view.$el.find('div.row');
expect(rows.length).toBe(1);
});
it('accomplishments placeholder is visible on a last page', function() {
var placeholder;
view = createView([LearnerProfileHelpers.makeBadge(1)], 2, 2, false);
view.render();
placeholder = view.$el.find('span.accomplishment-placeholder');
expect(placeholder.length).toBe(1);
});
it('accomplishments placeholder to be not visible on a first page', function() {
var placeholder;
view = createView([LearnerProfileHelpers.makeBadge(1)], 1, 2, true);
view.render();
placeholder = view.$el.find('span.accomplishment-placeholder');
expect(placeholder.length).toBe(0);
});
it('badges are in two columns (checked by counting rows for a known number of badges)', function() {
var badges = [];
var placeholder;
var rows;
_.each(_.range(4), function(item) {
badges.push(LearnerProfileHelpers.makeBadge(item));
});
view = createView(badges, 1, 2, true);
view.render();
placeholder = view.$el.find('span.accomplishment-placeholder');
expect(placeholder.length).toBe(0);
rows = view.$el.find('div.row');
expect(rows.length).toBe(2);
});
});
}
);

View File

@@ -1,114 +0,0 @@
define([
'backbone', 'jquery', 'underscore',
'learner_profile/js/spec_helpers/helpers',
'learner_profile/js/views/badge_view'
],
function(Backbone, $, _, LearnerProfileHelpers, BadgeView) {
'use strict';
describe('edx.user.BadgeView', function() {
var view,
badge,
testBadgeNameIsDisplayed,
testBadgeIconIsDisplayed;
var createView = function(ownProfile) {
var options,
testView;
badge = LearnerProfileHelpers.makeBadge(1);
options = {
model: new Backbone.Model(badge),
ownProfile: ownProfile,
badgeMeta: {}
};
testView = new BadgeView(options);
testView.render();
$('body').append(testView.$el);
testView.$el.show();
expect(testView.$el.is(':visible')).toBe(true);
return testView;
};
afterEach(function() {
view.$el.remove();
$('.badges-modal').remove();
});
it('profile of other has no share button', function() {
view = createView(false);
expect(view.context.ownProfile).toBeFalsy();
expect(view.$el.find('button.share-button').length).toBe(0);
});
it('own profile has share button', function() {
view = createView(true);
expect(view.context.ownProfile).toBeTruthy();
expect(view.$el.find('button.share-button').length).toBe(1);
});
it('click on share button calls createModal function', function() {
var shareButton;
view = createView(true);
spyOn(view, 'createModal');
view.delegateEvents();
expect(view.context.ownProfile).toBeTruthy();
shareButton = view.$el.find('button.share-button');
expect(shareButton.length).toBe(1);
expect(view.createModal).not.toHaveBeenCalled();
shareButton.click();
expect(view.createModal).toHaveBeenCalled();
});
it('click on share button calls shows the dialog', function(done) {
var shareButton,
$modalElement;
view = createView(true);
expect(view.context.ownProfile).toBeTruthy();
shareButton = view.$el.find('button.share-button');
expect(shareButton.length).toBe(1);
$modalElement = $('.badges-modal');
expect($modalElement.length).toBe(0);
expect($modalElement.is(':visible')).toBeFalsy();
shareButton.click();
// Note: this element should have appeared in the dom during: shareButton.click();
$modalElement = $('.badges-modal');
jasmine.waitUntil(function() {
return $modalElement.is(':visible');
}).always(done);
});
testBadgeNameIsDisplayed = function(ownProfile) {
var badgeDiv;
view = createView(ownProfile);
badgeDiv = view.$el.find('.badge-name');
expect(badgeDiv.length).toBeTruthy();
expect(badgeDiv.is(':visible')).toBe(true);
expect(_.count(badgeDiv.html(), badge.badge_class.display_name)).toBeTruthy();
};
it('test badge name is displayed for own profile', function() {
testBadgeNameIsDisplayed(true);
});
it('test badge name is displayed for other profile', function() {
testBadgeNameIsDisplayed(false);
});
testBadgeIconIsDisplayed = function(ownProfile) {
var badgeImg;
view = createView(ownProfile);
badgeImg = view.$el.find('img.badge');
expect(badgeImg.length).toBe(1);
expect(badgeImg.attr('src')).toEqual(badge.image_url);
};
it('test badge icon is displayed for own profile', function() {
testBadgeIconIsDisplayed(true);
});
it('test badge icon is displayed for other profile', function() {
testBadgeIconIsDisplayed(false);
});
});
}
);

View File

@@ -1,378 +0,0 @@
define(
[
'backbone',
'jquery',
'underscore',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/spec/student_account/helpers',
'js/student_account/models/user_account_model',
'learner_profile/js/views/learner_profile_fields',
'js/views/message_banner'
],
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, Helpers, UserAccountModel, LearnerProfileFields,
MessageBannerView) {
'use strict';
describe('edx.user.LearnerProfileFields', function() {
var MOCK_YEAR_OF_BIRTH = 1989;
var MOCK_IMAGE_MAX_BYTES = 64;
var MOCK_IMAGE_MIN_BYTES = 16;
var createImageView = function(options) {
var yearOfBirth = _.isUndefined(options.yearOfBirth) ? MOCK_YEAR_OF_BIRTH : options.yearOfBirth;
var imageMaxBytes = _.isUndefined(options.imageMaxBytes) ? MOCK_IMAGE_MAX_BYTES : options.imageMaxBytes;
var imageMinBytes = _.isUndefined(options.imageMinBytes) ? MOCK_IMAGE_MIN_BYTES : options.imageMinBytes;
var messageView;
var imageData = {
image_url_large: '/media/profile-images/default.jpg',
has_image: !!options.hasImage
};
var accountSettingsModel = new UserAccountModel();
accountSettingsModel.set({profile_image: imageData});
accountSettingsModel.set({year_of_birth: yearOfBirth});
accountSettingsModel.set({requires_parental_consent: !!_.isEmpty(yearOfBirth)});
accountSettingsModel.url = Helpers.USER_ACCOUNTS_API_URL;
messageView = new MessageBannerView({
el: $('.message-banner')
});
return new LearnerProfileFields.ProfileImageFieldView({
model: accountSettingsModel,
valueAttribute: 'profile_image',
editable: options.ownProfile,
messageView: messageView,
imageMaxBytes: imageMaxBytes,
imageMinBytes: imageMinBytes,
imageUploadUrl: Helpers.IMAGE_UPLOAD_API_URL,
imageRemoveUrl: Helpers.IMAGE_REMOVE_API_URL
});
};
var createSocialLinksView = function(ownProfile, socialPlatformLinks) {
var accountSettingsModel = new UserAccountModel();
accountSettingsModel.set({social_platforms: socialPlatformLinks});
return new LearnerProfileFields.SocialLinkIconsView({
model: accountSettingsModel,
socialPlatforms: ['twitter', 'facebook', 'linkedin'],
ownProfile: ownProfile
});
};
var createFakeImageFile = function(size) {
var fileFakeData = 'i63ljc6giwoskyb9x5sw0169bdcmcxr3cdz8boqv0lik971972cmd6yknvcxr5sw0nvc169bdcmcxsdf';
return new Blob(
[fileFakeData.substr(0, size)],
{type: 'image/jpg'}
);
};
var initializeUploader = function(view) {
view.$('.upload-button-input').fileupload({
url: Helpers.IMAGE_UPLOAD_API_URL,
type: 'POST',
add: view.fileSelected,
done: view.imageChangeSucceeded,
fail: view.imageChangeFailed
});
};
beforeEach(function() {
loadFixtures('learner_profile/fixtures/learner_profile.html');
TemplateHelpers.installTemplate('templates/fields/field_image');
TemplateHelpers.installTemplate('templates/fields/message_banner');
TemplateHelpers.installTemplate('learner_profile/templates/social_icons');
});
afterEach(function() {
// image_field.js's window.onBeforeUnload breaks Karma in Chrome, clean it up after each test
$(window).off('beforeunload');
});
describe('ProfileImageFieldView', function() {
var verifyImageUploadButtonMessage = function(view, inProgress) {
var iconName = inProgress ? 'fa-spinner' : 'fa-camera';
var message = inProgress ? view.titleUploading : view.uploadButtonTitle();
expect(view.$('.upload-button-icon span').attr('class')).toContain(iconName);
expect(view.$('.upload-button-title').text().trim()).toBe(message);
};
var verifyImageRemoveButtonMessage = function(view, inProgress) {
var iconName = inProgress ? 'fa-spinner' : 'fa-remove';
var message = inProgress ? view.titleRemoving : view.removeButtonTitle();
expect(view.$('.remove-button-icon span').attr('class')).toContain(iconName);
expect(view.$('.remove-button-title').text().trim()).toBe(message);
};
it('can upload profile image', function() {
var requests = AjaxHelpers.requests(this);
var imageName = 'profile_image.jpg';
var imageView = createImageView({ownProfile: true, hasImage: false});
var data;
imageView.render();
initializeUploader(imageView);
// Remove button should not be present for default image
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
// For default image, image title should be `Upload an image`
verifyImageUploadButtonMessage(imageView, false);
// Add image to upload queue. Validate the image size and send POST request to upload image
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]});
// Verify image upload progress message
verifyImageUploadButtonMessage(imageView, true);
// Verify if POST request received for image upload
AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_UPLOAD_API_URL, new FormData());
// Send 204 NO CONTENT to confirm the image upload success
AjaxHelpers.respondWithNoContent(requests);
// Upon successful image upload, account settings model will be fetched to
// get the url for newly uploaded image, So we need to send the response for that GET
data = {profile_image: {
image_url_large: '/media/profile-images/' + imageName,
has_image: true
}};
AjaxHelpers.respondWithJson(requests, data);
// Verify uploaded image name
expect(imageView.$('.image-frame').attr('src')).toContain(imageName);
// Remove button should be present after successful image upload
expect(imageView.$('.u-field-remove-button').css('display') !== 'none').toBeTruthy();
// After image upload, image title should be `Change image`
verifyImageUploadButtonMessage(imageView, false);
});
it('can remove profile image', function() {
var requests = AjaxHelpers.requests(this);
var imageView = createImageView({ownProfile: true, hasImage: false});
var data;
imageView.render();
// Verify image remove title
verifyImageRemoveButtonMessage(imageView, false);
imageView.$('.u-field-remove-button').click();
// Verify image remove progress message
verifyImageRemoveButtonMessage(imageView, true);
// Verify if POST request received for image remove
AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_REMOVE_API_URL, null);
// Send 204 NO CONTENT to confirm the image removal success
AjaxHelpers.respondWithNoContent(requests);
// Upon successful image removal, account settings model will be fetched to get default image url
// So we need to send the response for that GET
data = {profile_image: {
image_url_large: '/media/profile-images/default.jpg',
has_image: false
}};
AjaxHelpers.respondWithJson(requests, data);
// Remove button should not be present for default image
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
});
it("can't remove default profile image", function() {
var imageView = createImageView({ownProfile: true, hasImage: false});
imageView.render();
spyOn(imageView, 'clickedRemoveButton');
// Remove button should not be present for default image
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
imageView.$('.u-field-remove-button').click();
// Remove button click handler should not be called
expect(imageView.clickedRemoveButton).not.toHaveBeenCalled();
});
it("can't upload image having size greater than max size", function() {
var imageView = createImageView({ownProfile: true, hasImage: false});
imageView.render();
initializeUploader(imageView);
// Add image to upload queue, this will validate the image size
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(70)]});
// Verify error message
expect($('.message-banner').text().trim())
.toBe('The file must be smaller than 64 bytes in size.');
});
it("can't upload image having size less than min size", function() {
var imageView = createImageView({ownProfile: true, hasImage: false});
imageView.render();
initializeUploader(imageView);
// Add image to upload queue, this will validate the image size
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(10)]});
// Verify error message
expect($('.message-banner').text().trim()).toBe('The file must be at least 16 bytes in size.');
});
it("can't upload and remove image if parental consent required", function() {
var imageView = createImageView({ownProfile: true, hasImage: false, yearOfBirth: ''});
imageView.render();
spyOn(imageView, 'clickedUploadButton');
spyOn(imageView, 'clickedRemoveButton');
expect(imageView.$('.u-field-upload-button').css('display') === 'none').toBeTruthy();
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
imageView.$('.u-field-upload-button').click();
imageView.$('.u-field-remove-button').click();
expect(imageView.clickedUploadButton).not.toHaveBeenCalled();
expect(imageView.clickedRemoveButton).not.toHaveBeenCalled();
});
it("can't upload and remove image on others profile", function() {
var imageView = createImageView({ownProfile: false});
imageView.render();
spyOn(imageView, 'clickedUploadButton');
spyOn(imageView, 'clickedRemoveButton');
expect(imageView.$('.u-field-upload-button').css('display') === 'none').toBeTruthy();
expect(imageView.$('.u-field-remove-button').css('display') === 'none').toBeTruthy();
imageView.$('.u-field-upload-button').click();
imageView.$('.u-field-remove-button').click();
expect(imageView.clickedUploadButton).not.toHaveBeenCalled();
expect(imageView.clickedRemoveButton).not.toHaveBeenCalled();
});
it('shows message if we try to navigate away during image upload/remove', function() {
var imageView = createImageView({ownProfile: true, hasImage: false});
spyOn(imageView, 'onBeforeUnload');
imageView.render();
initializeUploader(imageView);
// Add image to upload queue, this will validate image size and send POST request to upload image
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]});
// Verify image upload progress message
verifyImageUploadButtonMessage(imageView, true);
window.onbeforeunload = null;
$(window).trigger('beforeunload');
expect(imageView.onBeforeUnload).toHaveBeenCalled();
});
it('shows error message for HTTP 500', function() {
var requests = AjaxHelpers.requests(this);
var imageView = createImageView({ownProfile: true, hasImage: false});
imageView.render();
initializeUploader(imageView);
// Add image to upload queue. Validate the image size and send POST request to upload image
imageView.$('.upload-button-input').fileupload('add', {files: [createFakeImageFile(60)]});
// Verify image upload progress message
verifyImageUploadButtonMessage(imageView, true);
// Verify if POST request received for image upload
AjaxHelpers.expectRequest(requests, 'POST', Helpers.IMAGE_UPLOAD_API_URL, new FormData());
// Send HTTP 500
AjaxHelpers.respondWithError(requests);
expect($('.message-banner').text().trim()).toBe(imageView.errorMessage);
});
});
describe('SocialLinkIconsView', function() {
var socialPlatformLinks,
socialLinkData,
socialLinksView,
socialPlatform,
$icon;
it('icons are visible and links to social profile if added in account settings', function() {
socialPlatformLinks = {
twitter: {
platform: 'twitter',
social_link: 'https://www.twitter.com/edX'
},
facebook: {
platform: 'facebook',
social_link: 'https://www.facebook.com/edX'
},
linkedin: {
platform: 'linkedin',
social_link: ''
}
};
socialLinksView = createSocialLinksView(true, socialPlatformLinks);
// Icons should be present and contain links if defined
for (var i = 0; i < Object.keys(socialPlatformLinks); i++) { // eslint-disable-line vars-on-top
socialPlatform = Object.keys(socialPlatformLinks)[i];
socialLinkData = socialPlatformLinks[socialPlatform];
if (socialLinkData.social_link) {
// Icons with a social_link value should be displayed with a surrounding link
$icon = socialLinksView.$('span.fa-' + socialPlatform + '-square');
expect($icon).toExist();
expect($icon.parent().is('a'));
} else {
// Icons without a social_link value should be displayed without a surrounding link
$icon = socialLinksView.$('span.fa-' + socialPlatform + '-square');
expect($icon).toExist();
expect(!$icon.parent().is('a'));
}
}
});
it('icons are not visible on a profile with no links', function() {
socialPlatformLinks = {
twitter: {
platform: 'twitter',
social_link: ''
},
facebook: {
platform: 'facebook',
social_link: ''
},
linkedin: {
platform: 'linkedin',
social_link: ''
}
};
socialLinksView = createSocialLinksView(false, socialPlatformLinks);
// Icons should not be present if not defined on another user's profile
for (var i = 0; i < Object.keys(socialPlatformLinks); i++) { // eslint-disable-line vars-on-top
socialPlatform = Object.keys(socialPlatformLinks)[i];
socialLinkData = socialPlatformLinks[socialPlatform];
$icon = socialLinksView.$('span.fa-' + socialPlatform + '-square');
expect($icon).toBe(null);
}
});
});
});
});

View File

@@ -1,240 +0,0 @@
/* eslint-disable vars-on-top */
define(
[
'gettext',
'backbone',
'jquery',
'underscore',
'edx-ui-toolkit/js/pagination/paging-collection',
'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers',
'js/spec/student_account/helpers',
'learner_profile/js/spec_helpers/helpers',
'js/views/fields',
'js/student_account/models/user_account_model',
'js/student_account/models/user_preferences_model',
'learner_profile/js/views/learner_profile_fields',
'learner_profile/js/views/learner_profile_view',
'learner_profile/js/views/badge_list_container',
'js/student_account/views/account_settings_fields',
'js/views/message_banner'
],
function(gettext, Backbone, $, _, PagingCollection, AjaxHelpers, TemplateHelpers, Helpers, LearnerProfileHelpers,
FieldViews, UserAccountModel, AccountPreferencesModel, LearnerProfileFields, LearnerProfileView,
BadgeListContainer, AccountSettingsFieldViews, MessageBannerView) {
'use strict';
describe('edx.user.LearnerProfileView', function() {
var createLearnerProfileView = function(ownProfile, accountPrivacy, profileIsPublic) {
var accountSettingsModel = new UserAccountModel();
accountSettingsModel.set(Helpers.createAccountSettingsData());
accountSettingsModel.set({profile_is_public: profileIsPublic});
accountSettingsModel.set({profile_image: Helpers.PROFILE_IMAGE});
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 messageView = new MessageBannerView({
el: $('.message-banner')
});
var profileImageFieldView = new LearnerProfileFields.ProfileImageFieldView({
model: accountSettingsModel,
valueAttribute: 'profile_image',
editable: editable,
messageView: messageView,
imageMaxBytes: Helpers.IMAGE_MAX_BYTES,
imageMinBytes: Helpers.IMAGE_MIN_BYTES,
imageUploadUrl: Helpers.IMAGE_UPLOAD_API_URL,
imageRemoveUrl: Helpers.IMAGE_REMOVE_API_URL
});
var usernameFieldView = new FieldViews.ReadonlyFieldView({
model: accountSettingsModel,
valueAttribute: 'username',
helpMessage: ''
});
var nameFieldView = new FieldViews.ReadonlyFieldView({
model: accountSettingsModel,
valueAttribute: 'name',
helpMessage: ''
});
var sectionOneFieldViews = [
new LearnerProfileFields.SocialLinkIconsView({
model: accountSettingsModel,
socialPlatforms: Helpers.SOCIAL_PLATFORMS,
ownProfile: true
}),
new FieldViews.DropdownFieldView({
title: gettext('Location'),
model: accountSettingsModel,
required: false,
editable: editable,
showMessages: false,
placeholderValue: '',
valueAttribute: 'country',
options: Helpers.FIELD_OPTIONS,
helpMessage: ''
}),
new AccountSettingsFieldViews.LanguageProficienciesFieldView({
title: gettext('Language'),
model: accountSettingsModel,
required: false,
editable: editable,
showMessages: false,
placeholderValue: 'Add language',
valueAttribute: 'language_proficiencies',
options: Helpers.FIELD_OPTIONS,
helpMessage: ''
}),
new FieldViews.DateFieldView({
model: accountSettingsModel,
valueAttribute: 'date_joined',
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: '',
messagePosition: 'header'
})
];
var badgeCollection = new PagingCollection();
badgeCollection.url = Helpers.BADGES_API_URL;
var badgeListContainer = new BadgeListContainer({
attributes: {class: 'badge-set-display'},
collection: badgeCollection,
find_courses_url: Helpers.FIND_COURSES_URL
});
return new LearnerProfileView(
{
el: $('.wrapper-profile'),
ownProfile: ownProfile,
hasPreferencesAccess: true,
accountSettingsModel: accountSettingsModel,
preferencesModel: accountPreferencesModel,
accountPrivacyFieldView: accountPrivacyFieldView,
usernameFieldView: usernameFieldView,
nameFieldView: nameFieldView,
profileImageFieldView: profileImageFieldView,
sectionOneFieldViews: sectionOneFieldViews,
sectionTwoFieldViews: sectionTwoFieldViews,
badgeListContainer: badgeListContainer
});
};
beforeEach(function() {
loadFixtures('learner_profile/fixtures/learner_profile.html');
});
afterEach(function() {
Backbone.history.stop();
});
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);
});
it("renders an error if the badges can't be fetched", function() {
var learnerProfileView = createLearnerProfileView(false, 'all_users', true);
learnerProfileView.options.accountSettingsModel.set({accomplishments_shared: true});
var requests = AjaxHelpers.requests(this);
learnerProfileView.render();
LearnerProfileHelpers.breakBadgeLoading(learnerProfileView, requests);
LearnerProfileHelpers.expectBadgeLoadingErrorIsRendered(learnerProfileView);
});
});
});

View File

@@ -1,113 +0,0 @@
/* eslint-disable vars-on-top */
define(
[
'backbone', 'jquery', 'underscore',
'js/spec/student_account/helpers',
'learner_profile/js/views/section_two_tab',
'js/views/fields',
'js/student_account/models/user_account_model'
],
function(Backbone, $, _, Helpers, SectionTwoTabView, FieldViews, UserAccountModel) {
'use strict';
describe('edx.user.SectionTwoTab', function() {
var createSectionTwoView = function(ownProfile, profileIsPublic) {
var accountSettingsModel = new UserAccountModel();
accountSettingsModel.set(Helpers.createAccountSettingsData());
accountSettingsModel.set({profile_is_public: profileIsPublic});
accountSettingsModel.set({profile_image: Helpers.PROFILE_IMAGE});
var editable = ownProfile ? 'toggle' : 'never';
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: '',
messagePosition: 'header'
})
];
return new SectionTwoTabView({
viewList: sectionTwoFieldViews,
showFullProfile: function() {
return profileIsPublic;
},
ownProfile: ownProfile
});
};
it('full profile displayed for public profile', function() {
var view = createSectionTwoView(false, true);
view.render();
var bio = view.$el.find('.u-field-bio');
expect(bio.length).toBe(1);
});
it('profile field parts are actually rendered for public profile', function() {
var view = createSectionTwoView(false, true);
_.each(view.options.viewList, function(fieldView) {
spyOn(fieldView, 'render').and.callThrough();
});
view.render();
_.each(view.options.viewList, function(fieldView) {
expect(fieldView.render).toHaveBeenCalled();
});
});
var testPrivateProfile = function(ownProfile, messageString) {
var view = createSectionTwoView(ownProfile, false);
view.render();
var bio = view.$el.find('.u-field-bio');
expect(bio.length).toBe(0);
var msg = view.$el.find('span.profile-private-message');
expect(msg.length).toBe(1);
expect(_.count(msg.html(), messageString)).toBeTruthy();
};
it('no profile when profile is private for other people', function() {
testPrivateProfile(false, 'This learner is currently sharing a limited profile');
});
it('no profile when profile is private for the user herself', function() {
testPrivateProfile(true, 'You are currently sharing a limited profile');
});
var testProfilePrivatePartsDoNotRender = function(ownProfile) {
var view = createSectionTwoView(ownProfile, false);
_.each(view.options.viewList, function(fieldView) {
spyOn(fieldView, 'render');
});
view.render();
_.each(view.options.viewList, function(fieldView) {
expect(fieldView.render).not.toHaveBeenCalled();
});
};
it('profile field parts are not rendered for private profile for owner', function() {
testProfilePrivatePartsDoNotRender(true);
});
it('profile field parts are not rendered for private profile for other people', function() {
testProfilePrivatePartsDoNotRender(false);
});
it('does not allow fields to be edited when visiting a profile for other people', function() {
var view = createSectionTwoView(false, true);
var bio = view.options.viewList[0];
expect(bio.editable).toBe('never');
});
it("allows fields to be edited when visiting one's own profile", function() {
var view = createSectionTwoView(true, true);
var bio = view.options.viewList[0];
expect(bio.editable).toBe('toggle');
});
});
}
);

View File

@@ -1,63 +0,0 @@
define(
[
'backbone', 'jquery', 'underscore', 'moment',
'js/spec/student_account/helpers',
'learner_profile/js/spec_helpers/helpers',
'learner_profile/js/views/share_modal_view',
'jquery.simulate'
],
function(Backbone, $, _, Moment, Helpers, LearnerProfileHelpers, ShareModalView) {
'use strict';
describe('edx.user.ShareModalView', function() {
var keys = $.simulate.keyCode;
var view;
var createModalView = function() {
var badge = LearnerProfileHelpers.makeBadge(1);
var context = _.extend(badge, {
created: new Moment(badge.created),
ownProfile: true,
badgeMeta: {}
});
return new ShareModalView({
model: new Backbone.Model(context),
shareButton: $('<button/>')
});
};
beforeEach(function() {
view = createModalView();
// Attach view to document, otherwise click won't work
view.render();
$('body').append(view.$el);
view.$el.show();
expect(view.$el.is(':visible')).toBe(true);
});
afterEach(function() {
view.$el.remove();
});
it('modal view closes on escape', function() {
spyOn(view, 'close');
view.delegateEvents();
expect(view.close).not.toHaveBeenCalled();
$(view.$el).simulate('keydown', {keyCode: keys.ESCAPE});
expect(view.close).toHaveBeenCalled();
});
it('modal view closes click on close', function() {
var $closeButton;
spyOn(view, 'close');
view.delegateEvents();
$closeButton = view.$el.find('button.close');
expect($closeButton.length).toBe(1);
expect(view.close).not.toHaveBeenCalled();
$closeButton.trigger('click');
expect(view.close).toHaveBeenCalled();
});
});
}
);

View File

@@ -1,259 +0,0 @@
define(['underscore', 'URI', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers'], function(_, URI, AjaxHelpers) {
'use strict';
var expectProfileElementContainsField = function(element, view) {
var titleElement, fieldTitle;
var $element = $(element);
// Avoid testing for elements without titles
titleElement = $element.find('.u-field-title');
if (titleElement.length === 0) {
return;
}
fieldTitle = titleElement.text().trim();
if (!_.isUndefined(view.options.title) && !_.isUndefined(fieldTitle)) {
expect(fieldTitle).toBe(view.options.title);
}
if ('fieldValue' in view || 'imageUrl' in view) {
if ('imageUrl' in view) {
expect($($element.find('.image-frame')[0]).attr('src')).toBe(view.imageUrl());
} else if (view.fieldType === 'date') {
expect(view.fieldValue()).toBe(view.timezoneFormattedDate());
} else if (view.fieldValue()) {
expect(view.fieldValue()).toBe(view.modelValue());
} else if ('optionForValue' in view) {
expect($($element.find('.u-field-value .u-field-value-readonly')[0]).text()).toBe(
view.displayValue(view.modelValue())
);
} else {
expect($($element.find('.u-field-value .u-field-value-readonly')[0]).text()).toBe(view.modelValue());
}
} else {
throw new Error('Unexpected field type: ' + view.fieldType);
}
};
var expectProfilePrivacyFieldTobeRendered = function(learnerProfileView, othersProfile) {
var $accountPrivacyElement = $('.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, .social-links');
expect(sectionOneFieldElements.length).toBe(7);
expectProfileElementContainsField(sectionOneFieldElements[0], learnerProfileView.options.profileImageFieldView);
expectProfileElementContainsField(sectionOneFieldElements[1], learnerProfileView.options.usernameFieldView);
expectProfileElementContainsField(sectionOneFieldElements[2], learnerProfileView.options.nameFieldView);
_.each(_.rest(sectionOneFieldElements, 3), function(sectionFieldElement, fieldIndex) {
expectProfileElementContainsField(
sectionFieldElement,
learnerProfileView.options.sectionOneFieldViews[fieldIndex]
);
});
};
var expectSectionTwoTobeRendered = function(learnerProfileView) {
var $sectionTwoElement = $('.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) {
var sectionOneFieldElements = $('.wrapper-profile-section-one').find('.u-field');
expectProfilePrivacyFieldTobeRendered(learnerProfileView, othersProfile);
expect(sectionOneFieldElements.length).toBe(2);
expectProfileElementContainsField(
sectionOneFieldElements[0],
learnerProfileView.options.profileImageFieldView
);
expectProfileElementContainsField(
sectionOneFieldElements[1],
learnerProfileView.options.usernameFieldView
);
if (othersProfile) {
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.');
}
};
var expectProfileSectionsNotToBeRendered = function() {
expect($('.wrapper-profile-field-account-privacy').length).toBe(0);
expect($('.wrapper-profile-section-one').length).toBe(0);
expect($('.wrapper-profile-section-two').length).toBe(0);
};
var expectTabbedViewToBeUndefined = function(requests, tabbedViewView) {
// Unrelated initial request, no badge request
expect(requests.length).toBe(1);
expect(tabbedViewView).toBe(undefined);
};
var expectTabbedViewToBeShown = function(tabbedViewView) {
expect(tabbedViewView.$el.find('.page-content-nav').is(':visible')).toBe(true);
};
var expectBadgesDisplayed = function(learnerProfileView, length, lastPage) {
var $badgeListingView = $('#tabpanel-accomplishments'),
updatedLength = length,
placeholder;
expect($('#tabpanel-about_me').hasClass('is-hidden')).toBe(true);
expect($badgeListingView.hasClass('is-hidden')).toBe(false);
if (lastPage) {
updatedLength += 1;
placeholder = $badgeListingView.find('.find-course');
expect(placeholder.length).toBe(1);
expect(placeholder.attr('href')).toBe('/courses/');
}
expect($badgeListingView.find('.badge-display').length).toBe(updatedLength);
};
var expectBadgesHidden = function() {
var $accomplishmentsTab = $('#tabpanel-accomplishments');
if ($accomplishmentsTab.length) {
// Nonexistence counts as hidden.
expect($('#tabpanel-accomplishments').hasClass('is-hidden')).toBe(true);
}
expect($('#tabpanel-about_me').hasClass('is-hidden')).toBe(false);
};
var expectPage = function(learnerProfileView, pageData) {
var $badgeListContainer = $('#tabpanel-accomplishments');
var index = $badgeListContainer.find('span.search-count').text().trim();
expect(index).toBe('Showing ' + (pageData.start + 1) + '-' + (pageData.start + pageData.results.length) +
' out of ' + pageData.count + ' total');
expect($badgeListContainer.find('.current-page').text()).toBe('' + pageData.current_page);
_.each(pageData.results, function(badge) {
expect($('.badge-display:contains(' + badge.badge_class.display_name + ')').length).toBe(1);
});
};
var expectBadgeLoadingErrorIsRendered = function() {
var errorMessage = $('.badge-set-display').text();
expect(errorMessage).toBe(
'Your request could not be completed. Reload the page and try again. If the issue persists, click the ' +
'Help tab to report the problem.'
);
};
var breakBadgeLoading = function(learnerProfileView, requests) {
var request = AjaxHelpers.currentRequest(requests);
var path = new URI(request.url).path();
expect(path).toBe('/api/badges/v1/assertions/user/student/');
AjaxHelpers.respondWithError(requests, 500);
};
var firstPageBadges = {
count: 30,
previous: null,
next: '/arbitrary/url',
num_pages: 3,
start: 0,
current_page: 1,
results: []
};
var secondPageBadges = {
count: 30,
previous: '/arbitrary/url',
next: '/arbitrary/url',
num_pages: 3,
start: 10,
current_page: 2,
results: []
};
var thirdPageBadges = {
count: 30,
previous: '/arbitrary/url',
num_pages: 3,
next: null,
start: 20,
current_page: 3,
results: []
};
var emptyBadges = {
count: 0,
previous: null,
num_pages: 1,
results: []
};
function makeBadge(num) {
return {
badge_class: {
slug: 'test_slug_' + num,
issuing_component: 'test_component',
display_name: 'Test Badge ' + num,
course_id: null,
description: "Yay! It's a test badge.",
criteria: 'https://example.com/syllabus',
image_url: 'http://localhost:8000/media/badge_classes/test_lMB9bRw.png'
},
image_url: 'http://example.com/image.png',
assertion_url: 'http://example.com/example.json',
created_at: '2015-12-03T16:25:57.676113Z'
};
}
_.each(_.range(0, 10), function(i) {
firstPageBadges.results.push(makeBadge(i));
});
_.each(_.range(10, 20), function(i) {
secondPageBadges.results.push(makeBadge(i));
});
_.each(_.range(20, 30), function(i) {
thirdPageBadges.results.push(makeBadge(i));
});
return {
expectLimitedProfileSectionsAndFieldsToBeRendered: expectLimitedProfileSectionsAndFieldsToBeRendered,
expectProfileSectionsAndFieldsToBeRendered: expectProfileSectionsAndFieldsToBeRendered,
expectProfileSectionsNotToBeRendered: expectProfileSectionsNotToBeRendered,
expectTabbedViewToBeUndefined: expectTabbedViewToBeUndefined,
expectTabbedViewToBeShown: expectTabbedViewToBeShown,
expectBadgesDisplayed: expectBadgesDisplayed,
expectBadgesHidden: expectBadgesHidden,
expectBadgeLoadingErrorIsRendered: expectBadgeLoadingErrorIsRendered,
breakBadgeLoading: breakBadgeLoading,
firstPageBadges: firstPageBadges,
secondPageBadges: secondPageBadges,
thirdPageBadges: thirdPageBadges,
emptyBadges: emptyBadges,
expectPage: expectPage,
makeBadge: makeBadge
};
});

View File

@@ -1,35 +0,0 @@
/* eslint-disable no-underscore-dangle */
(function(define) {
'use strict';
define(
[
'gettext', 'jquery', 'underscore', 'common/js/components/views/paginated_view',
'learner_profile/js/views/badge_view', 'learner_profile/js/views/badge_list_view',
'text!learner_profile/templates/badge_list.underscore'
],
function(gettext, $, _, PaginatedView, BadgeView, BadgeListView, BadgeListTemplate) {
var BadgeListContainer = PaginatedView.extend({
type: 'badge',
itemViewClass: BadgeView,
listViewClass: BadgeListView,
viewTemplate: BadgeListTemplate,
isZeroIndexed: true,
paginationLabel: gettext('Accomplishments Pagination'),
initialize: function(options) {
BadgeListContainer.__super__.initialize.call(this, options);
this.listView.find_courses_url = options.find_courses_url;
this.listView.badgeMeta = options.badgeMeta;
this.listView.ownProfile = options.ownProfile;
}
});
return BadgeListContainer;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,65 +0,0 @@
(function(define) {
'use strict';
define([
'gettext',
'jquery',
'underscore',
'edx-ui-toolkit/js/utils/html-utils',
'common/js/components/views/list',
'learner_profile/js/views/badge_view',
'text!learner_profile/templates/badge_placeholder.underscore'
],
function(gettext, $, _, HtmlUtils, ListView, BadgeView, badgePlaceholder) {
var BadgeListView = ListView.extend({
tagName: 'div',
template: HtmlUtils.template(badgePlaceholder),
renderCollection: function() {
var self = this,
$row;
this.$el.empty();
// Split into two columns.
this.collection.each(function(badge, index) {
var $item;
if (index % 2 === 0) {
$row = $('<div class="row">');
this.$el.append($row);
}
$item = new BadgeView({
model: badge,
badgeMeta: this.badgeMeta,
ownProfile: this.ownProfile
}).render().el;
if ($row) {
$row.append($item);
}
this.itemViews.push($item);
}, this);
// Placeholder must always be at the end, and may need a new row.
if (!this.collection.hasNextPage()) {
// find_courses_url set by BadgeListContainer during initialization.
if (this.collection.length % 2 === 0) {
$row = $('<div class="row">');
this.$el.append($row);
}
if ($row) {
HtmlUtils.append(
$row,
this.template({find_courses_url: self.find_courses_url})
);
}
}
return this;
}
});
return BadgeListView;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,47 +0,0 @@
(function(define) {
'use strict';
define(
[
'gettext', 'jquery', 'underscore', 'backbone', 'moment',
'text!learner_profile/templates/badge.underscore',
'learner_profile/js/views/share_modal_view',
'edx-ui-toolkit/js/utils/html-utils'
],
function(gettext, $, _, Backbone, Moment, badgeTemplate, ShareModalView, HtmlUtils) {
var BadgeView = Backbone.View.extend({
initialize: function(options) {
this.options = _.extend({}, options);
this.context = _.extend(this.options.model.toJSON(), {
created: new Moment(this.options.model.toJSON().created),
ownProfile: options.ownProfile,
badgeMeta: options.badgeMeta
});
},
attributes: {
class: 'badge-display'
},
template: _.template(badgeTemplate),
events: {
'click .share-button': 'createModal'
},
createModal: function() {
var modal = new ShareModalView({
model: new Backbone.Model(this.context),
shareButton: this.shareButton
});
modal.$el.hide();
modal.render();
$('body').append(modal.$el);
modal.$el.fadeIn('short', 'swing', _.bind(modal.ready, modal));
},
render: function() {
this.$el.html(HtmlUtils.HTML(this.template(this.context)).toString());
this.shareButton = this.$el.find('.share-button');
return this;
}
});
return BadgeView;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,169 +0,0 @@
/* eslint-disable no-underscore-dangle */
(function(define) {
'use strict';
define([
'gettext',
'jquery',
'underscore',
'backbone',
'edx-ui-toolkit/js/utils/string-utils',
'edx-ui-toolkit/js/utils/html-utils',
'js/views/fields',
'js/views/image_field',
'text!learner_profile/templates/social_icons.underscore',
'backbone-super'
], function(gettext, $, _, Backbone, StringUtils, HtmlUtils, FieldViews, ImageFieldView, socialIconsTemplate) {
var LearnerProfileFieldViews = {};
LearnerProfileFieldViews.AccountPrivacyFieldView = FieldViews.DropdownFieldView.extend({
events: {
'click button.btn-change-privacy': 'finishEditing',
'change select': 'showSaveButton'
},
render: function() {
this._super();
this.showNotificationMessage();
this.updateFieldValue();
return this;
},
showNotificationMessage: function() {
var accountSettingsLink = HtmlUtils.joinHtml(
HtmlUtils.interpolateHtml(
HtmlUtils.HTML('<a href="{settings_url}">'), {settings_url: this.options.accountSettingsPageUrl}
),
gettext('Account Settings page.'),
HtmlUtils.HTML('</a>')
);
if (this.profileIsPrivate) {
this._super(
HtmlUtils.interpolateHtml(
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}'), // eslint-disable-line max-len
{account_settings_page_link: accountSettingsLink}
)
);
} else if (this.requiresParentalConsent) {
this._super(
HtmlUtils.interpolateHtml(
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}'), // eslint-disable-line max-len
{account_settings_page_link: accountSettingsLink}
)
);
} else {
this._super('');
}
},
updateFieldValue: function() {
if (!this.isAboveMinimumAge) {
this.$('.u-field-value select').val('private');
this.disableField(true);
}
},
showSaveButton: function() {
$('.btn-change-privacy').removeClass('hidden');
}
});
LearnerProfileFieldViews.ProfileImageFieldView = ImageFieldView.extend({
screenReaderTitle: gettext('Profile Image'),
imageUrl: function() {
return this.model.profileImageUrl();
},
imageAltText: function() {
return StringUtils.interpolate(
gettext('Profile image for {username}'),
{username: this.model.get('username')}
);
},
imageChangeSucceeded: function() {
var view = this;
// Update model to get the latest urls of profile image.
this.model.fetch().done(function() {
view.setCurrentStatus('');
view.render();
view.$('.u-field-upload-button').focus();
}).fail(function() {
view.setCurrentStatus('');
view.showErrorMessage(view.errorMessage);
});
},
imageChangeFailed: function(e, data) {
this.setCurrentStatus('');
this.showImageChangeFailedMessage(data.jqXHR.status, data.jqXHR.responseText);
},
showImageChangeFailedMessage: function(status, responseText) {
var errors;
if (_.contains([400, 404], status)) {
try {
errors = JSON.parse(responseText);
this.showErrorMessage(errors.user_message);
} catch (error) {
this.showErrorMessage(this.errorMessage);
}
} else {
this.showErrorMessage(this.errorMessage);
}
},
showErrorMessage: function(message) {
this.options.messageView.showMessage(message);
},
isEditingAllowed: function() {
return this.model.isAboveMinimumAge();
},
isShowingPlaceholder: function() {
return !this.model.hasProfileImage();
},
clickedRemoveButton: function(e, data) {
this.options.messageView.hideMessage();
this._super(e, data);
},
fileSelected: function(e, data) {
this.options.messageView.hideMessage();
this._super(e, data);
}
});
LearnerProfileFieldViews.SocialLinkIconsView = Backbone.View.extend({
initialize: function(options) {
this.options = _.extend({}, options);
},
render: function() {
var socialLinks = {};
for (var platformName in this.options.socialPlatforms) { // eslint-disable-line no-restricted-syntax, guard-for-in, vars-on-top, max-len
socialLinks[platformName] = null;
for (var link in this.model.get('social_links')) { // eslint-disable-line no-restricted-syntax, vars-on-top, max-len
if (platformName === this.model.get('social_links')[link].platform) {
socialLinks[platformName] = this.model.get('social_links')[link].social_link;
}
}
}
HtmlUtils.setHtml(this.$el, HtmlUtils.template(socialIconsTemplate)({
socialLinks: socialLinks,
ownProfile: this.options.ownProfile
}));
return this;
}
});
return LearnerProfileFieldViews;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,168 +0,0 @@
(function (define){
'use strict';
define(
[
'gettext', 'jquery', 'underscore', 'backbone', 'edx-ui-toolkit/js/utils/html-utils',
'common/js/components/views/tabbed_view',
'learner_profile/js/views/section_two_tab'
],
function (gettext, $, _, Backbone, HtmlUtils, TabbedView, SectionTwoTab){
var LearnerProfileView = Backbone.View.extend({
initialize: function (options){
var Router;
this.options = _.extend({}, options);
_.bindAll(this, 'showFullProfile', 'render', 'renderFields', 'showLoadingError');
this.listenTo(this.options.preferencesModel, 'change:account_privacy', this.render);
Router = Backbone.Router.extend({
routes: {':about_me': 'loadTab', ':accomplishments': 'loadTab'}
});
this.router = new Router();
this.firstRender = true;
},
showFullProfile: function () {
var isAboveMinimumAge = this.options.accountSettingsModel.isAboveMinimumAge();
if (this.options.ownProfile) {
return isAboveMinimumAge
&& this.options.preferencesModel.get('account_privacy') === 'all_users';
} else {
return this.options.accountSettingsModel.get('profile_is_public');
}
},
setActiveTab: function (tab){
// This tab may not actually exist.
if (this.tabbedView.getTabMeta(tab).tab) {
this.tabbedView.setActiveTab(tab);
}
},
render: function (){
var tabs,
$tabbedViewElement,
$wrapperProfileBioElement = this.$el.find('.wrapper-profile-bio'),
self = this;
this.sectionTwoView = new SectionTwoTab({
viewList: this.options.sectionTwoFieldViews,
showFullProfile: this.showFullProfile,
ownProfile: this.options.ownProfile
});
this.renderFields();
// Reveal the profile and hide the loading indicator
$('.ui-loading-indicator').addClass('is-hidden');
$('.wrapper-profile-section-container-one').removeClass('is-hidden');
$('.wrapper-profile-section-container-two').removeClass('is-hidden');
// Only show accomplishments if this is a full profile
if (this.showFullProfile()) {
$('.learner-achievements').removeClass('is-hidden');
} else {
$('.learner-achievements').addClass('is-hidden');
}
if (this.showFullProfile() && (this.options.accountSettingsModel.get('accomplishments_shared'))) {
tabs = [
{view: this.sectionTwoView, title: gettext('About Me'), url: 'about_me'},
{
view: this.options.badgeListContainer,
title: gettext('Accomplishments'),
url: 'accomplishments'
}
];
// Build the accomplishments Tab and fill with data
this.options.badgeListContainer.collection.fetch().done(function (){
self.options.badgeListContainer.render();
}).error(function (){
self.options.badgeListContainer.renderError();
});
this.tabbedView = new TabbedView({
tabs: tabs,
router: this.router,
viewLabel: gettext('Profile')
});
$tabbedViewElement = this.tabbedView.render().el;
HtmlUtils.setHtml(
$wrapperProfileBioElement,
HtmlUtils.HTML($tabbedViewElement)
);
if (this.firstRender) {
this.router.on('route:loadTab', _.bind(this.setActiveTab, this));
Backbone.history.start();
this.firstRender = false;
// Load from history.
this.router.navigate((Backbone.history.getFragment() || 'about_me'), {trigger: true});
} else {
// Restart the router so the tab will be brought up anew.
Backbone.history.stop();
Backbone.history.start();
}
} else {
if (this.isCoppaCompliant()) {
// xss-lint: disable=javascript-jquery-html
$wrapperProfileBioElement.html(this.sectionTwoView.render().el);
}
}
return this;
},
isCoppaCompliant: function (){
var enableCoppaCompliance = this.options.accountSettingsModel.get('enable_coppa_compliance'),
isAboveAge = this.options.accountSettingsModel.isAboveMinimumAge();
return !enableCoppaCompliance || (enableCoppaCompliance && isAboveAge);
},
renderFields: function (){
var view = this,
fieldView,
imageView,
settings;
if (this.options.ownProfile && this.isCoppaCompliant()) {
fieldView = this.options.accountPrivacyFieldView;
settings = this.options.accountSettingsModel;
fieldView.profileIsPrivate = !settings.get('year_of_birth');
fieldView.requiresParentalConsent = settings.get('requires_parental_consent');
fieldView.isAboveMinimumAge = settings.isAboveMinimumAge();
fieldView.undelegateEvents();
this.$('.wrapper-profile-field-account-privacy').prepend(fieldView.render().el);
fieldView.delegateEvents();
}
// Clear existing content in user profile card
this.$('.profile-section-one-fields').html('');
// 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;
this.$('.profile-image-field').append(imageView.render().el);
if (this.showFullProfile()){
_.each(this.options.sectionOneFieldViews, function (childFieldView) {
view.$('.profile-section-one-fields').append(childFieldView.render().el);
});
}
},
showLoadingError: function (){
this.$('.ui-loading-indicator').addClass('is-hidden');
this.$('.ui-loading-error').removeClass('is-hidden');
}
});
return LearnerProfileView;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,33 +0,0 @@
(function(define) {
'use strict';
define(
[
'gettext', 'jquery', 'underscore', 'backbone', 'text!learner_profile/templates/section_two.underscore',
'edx-ui-toolkit/js/utils/html-utils'
],
function(gettext, $, _, Backbone, sectionTwoTemplate, HtmlUtils) {
var SectionTwoTab = Backbone.View.extend({
attributes: {
class: 'wrapper-profile-section-two'
},
template: _.template(sectionTwoTemplate),
initialize: function(options) {
this.options = _.extend({}, options);
},
render: function() {
var self = this;
var showFullProfile = this.options.showFullProfile();
this.$el.html(HtmlUtils.HTML(this.template({ownProfile: self.options.ownProfile, showFullProfile: showFullProfile})).toString()); // eslint-disable-line max-len
if (showFullProfile) {
_.each(this.options.viewList, function(fieldView) {
self.$el.find('.field-container').append(fieldView.render().el);
});
}
return this;
}
});
return SectionTwoTab;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,56 +0,0 @@
(function(define) {
'use strict';
define(
[
'gettext', 'jquery', 'underscore', 'backbone', 'moment',
'text!learner_profile/templates/share_modal.underscore',
'edx-ui-toolkit/js/utils/html-utils'
],
function(gettext, $, _, Backbone, Moment, badgeModalTemplate, HtmlUtils) {
var ShareModalView = Backbone.View.extend({
attributes: {
class: 'badges-overlay'
},
template: _.template(badgeModalTemplate),
events: {
'click .badges-modal': function(event) { event.stopPropagation(); },
'click .badges-modal .close': 'close',
'click .badges-overlay': 'close',
keydown: 'keyAction',
'focus .focusguard-start': 'focusGuardStart',
'focus .focusguard-end': 'focusGuardEnd'
},
initialize: function(options) {
this.options = _.extend({}, options);
},
focusGuardStart: function() {
// Should only be selected directly if shift-tabbing from the start, so grab last item.
this.$el.find('a').last().focus();
},
focusGuardEnd: function() {
this.$el.find('.badges-modal').focus();
},
close: function() {
this.$el.fadeOut('short', 'swing', _.bind(this.remove, this));
this.options.shareButton.focus();
},
keyAction: function(event) {
if (event.keyCode === $.ui.keyCode.ESCAPE) {
this.close();
}
},
ready: function() {
// Focusing on the modal background directly doesn't work, probably due
// to its positioning.
this.$el.find('.badges-modal').focus();
},
render: function() {
this.$el.html(HtmlUtils.HTML(this.template(this.model.toJSON())).toString());
return this;
}
});
return ShareModalView;
});
}).call(this, define || RequireJS.define);

View File

@@ -1,30 +0,0 @@
<div class="badge-image-container">
<img class="badge" src="<%- image_url %>" alt=""/>
</div>
<div class="badge-details">
<div class="badge-name"><%- badge_class.display_name %></div>
<p class="badge-description"><%- badge_class.description %></p>
<% if (ownProfile) { %>
<button class="share-button">
<div class="share-icon-container">
<img class="icon icon-mozillaopenbadges" src="<%- badgeMeta.badges_icon %>" alt="<%-
interpolate(
// Translators: display_name is the name of an OpenBadges award.
gettext('Share your "%(display_name)s" award'),
{'display_name': badge_class.display_name},
true
)%>">
</div>
<div class="share-prefix" aria-hidden="true"><%- gettext("Share") %></div>
</div>
<% } %>
<div class="badge-date-stamp">
<%-
interpolate(
// Translators: Date stamp for earned badges. Example: Earned December 3, 2015.
gettext('Earned %(created)s.'),
{created: created.format('LL')},
true
)
%></div>
</div>

View File

@@ -1,4 +0,0 @@
<div class="sr-is-focusable sr-<%- type %>-view" tabindex="-1"></div>
<div class="<%- type %>-paging-header"></div>
<div class="<%- type %>-list cards-list"></div>
<div class="<%- type %>-paging-footer"></div>

View File

@@ -1,10 +0,0 @@
<div class="badge-display badge-placeholder">
<div class="badge-image-container">
<span class="accomplishment-placeholder" aria-hidden="true">
</div>
<div class="badge-details">
<div class="badge-name"><%- gettext("What's Your Next Accomplishment?") %></div>
<p class="badge-description"><%- gettext('Start working toward your next learning goal.') %></p>
<a class="find-course" href="<%- find_courses_url %>"><span class="find-button-container"><%- gettext('Find a course') %></span></a>
</div>
</div>

View File

@@ -1,10 +0,0 @@
<div class="profile-section-two-fields">
<div class="field-container"></div>
<% 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 learner is currently sharing a limited profile.") %></span>
<% } %>
<% } %>
</div>

View File

@@ -1,41 +0,0 @@
<div class="focusguard focusguard-start" tabindex="0"></div>
<div class="badges-modal" tabindex="0">
<button class="close"><span class="fa fa-close" aria-hidden="true"></span><span class="sr"><%- gettext("Close") %></span></button>
<h1 class="modal-header"><%- gettext("Share on Mozilla Backpack") %></h1>
<p class="explanation"><%- gettext("To share your certificate on Mozilla Backpack, you must first have a Backpack account. Complete the following steps to add your certificate to Backpack.") %>
</p>
<hr class="modal-hr"/>
<img class="backpack-logo" src="<%- badgeMeta.badges_logo %>" alt="">
<ol class="badges-steps">
<li class="step">
<%= edx.HtmlUtils.interpolateHtml(
gettext("Create a {link_start}Mozilla Backpack{link_end} account, or log in to your existing account"),
{
link_start: edx.HtmlUtils.HTML('<a href="https://backpack.openbadges.org/" rel="noopener" target="_blank">'),
link_end: edx.HtmlUtils.HTML('</a>')
}
)
%>
</li>
<li class="step">
<%= edx.HtmlUtils.interpolateHtml(
gettext("{download_link_start}Download this image (right-click or option-click, save as){link_end} and then {upload_link_start}upload{link_end} it to your backpack."),
{
download_link_start: edx.HtmlUtils.joinHtml(
edx.HtmlUtils.HTML('<a class="badge-link" href="'),
image_url,
edx.HtmlUtils.HTML('" rel="noopener" target="_blank">'),
),
link_end: edx.HtmlUtils.HTML('</a>'),
upload_link_start: edx.HtmlUtils.HTML('<a href="https://backpack.openbadges.org/backpack/add" rel="noopener" target="_blank">')
}
)
%>
</li>
</ol>
<div class="image-container">
<img class="badges-backpack-example" src="<%- badgeMeta.backpack_ui_img %>" alt="">
</div>
</div>
<div class="focusguard focusguard-end" tabindex="0"></div>

View File

@@ -1,9 +0,0 @@
<div class="social-links">
<% for (var platform in socialLinks) { %>
<% if (socialLinks[platform]) { %>
<a rel="noopener" target="_blank" href= <%-socialLinks[platform]%>>
<span class="icon fa fa-<%-platform%>-square" data-platform=<%-platform%> aria-hidden="true"></span>
</a>
<% } %>
<% } %>
</div>

View File

@@ -1,47 +0,0 @@
<%page expression_filter="h"/>
<%!
from django.utils.translation import ugettext as _
from common.djangoapps.third_party_auth import pipeline
%>
<li class="controls--account">
<span class="title">
## Translators: this section lists all the third-party authentication providers (for example, Google and LinkedIn) the user can link with or unlink from their edX account.
${_("Connected Accounts")}
</span>
<span class="data">
<span class="third-party-auth">
% for state in provider_user_states:
<div class="auth-provider">
<div class="status">
% if state.has_account:
<span class="icon fa fa-link" aria-hidden="true"></span> <span class="copy">${_('Linked')}</span>
% else:
<span class="icon fa fa-unlink" aria-hidden="true"></span><span class="copy">${_('Not Linked')}</span>
% endif
</div>
<span class="provider">${state.provider.name}</span>
<span class="control">
<form
action="${pipeline.get_disconnect_url(state.provider.provider_id, state.association_id)}"
method="post"
name="${state.get_unlink_form_name()}">
% if state.has_account:
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<button type="button" onclick="document.${state.get_unlink_form_name()}.submit()">
## Translators: clicking on this removes the link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
${_("Unlink")}
</button>
% elif state.provider.display_for_login:
<a href="${pipeline.get_login_url(state.provider.provider_id, pipeline.AUTH_ENTRY_PROFILE)}">
## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
${_("Link")}
</a>
% endif
</form>
</span>
</div>
% endfor
</span>
</li>

View File

@@ -1,69 +0,0 @@
## mako
<%page expression_filter="h"/>
<%namespace name='static' file='/static_content.html'/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML, Text
%>
<div class="learner-achievements">
% if course_certificates or own_profile:
<h3 class="u-field-title">Course Certificates</h3>
% if course_certificates:
% for certificate in course_certificates:
<%
course = certificate['course']
completion_date_message_html = Text(_('Completed {completion_date_html}')).format(
completion_date_html=HTML(
'<span'
' class="localized-datetime start-date"'
' data-datetime="{completion_date}"'
' data-format="shortDate"'
' data-timezone="{user_timezone}"'
' data-language="{user_language}"'
'></span>'
).format(
completion_date=certificate['created'],
user_timezone=user_timezone,
user_language=user_language,
),
)
%>
<div class="card certificate-card mode-${certificate['type']}">
<div class="card-logo">
<h4 class="sr-only">
${_('{course_mode} certificate').format(
course_mode=certificate['type'],
)}
</h4>
</div>
<div class="card-content">
<div class="card-supertitle">${course.display_org_with_default}</div>
<div class="card-title">${course.display_name_with_default}</div>
<p class="card-text">${completion_date_message_html}</p>
</div>
</div>
% endfor
% elif own_profile:
<div class="learner-message">
<h4 class="message-header">${_("You haven't earned any certificates yet.")}</h4>
% if settings.FEATURES.get('COURSES_ARE_BROWSABLE'):
<p class="message-actions">
<a class="btn btn-brand" href="${marketing_link('COURSES')}">
<span class="icon fa fa-search" aria-hidden="true"></span>
${_('Explore New Courses')}
</a>
</p>
% endif
</div>
% endif
% endif
</div>
<%static:require_module_async module_name="js/dateutil_factory" class_name="DateUtilFactory">
DateUtilFactory.transform('.localized-datetime');
</%static:require_module_async>

View File

@@ -1,79 +0,0 @@
## mako
<%page expression_filter="h"/>
<%inherit file="/main.html" />
<%def name="online_help_token()"><% return "profile" %></%def>
<%namespace name='static' file='/static_content.html'/>
<%!
import json
from django.urls import reverse
from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import dump_js_escaped_json
from openedx.core.djangolib.markup import HTML
%>
<%block name="pagetitle">${_("Learner Profile")}</%block>
<%block name="bodyclass">view-profile</%block>
<%block name="headextra">
<%static:css group='style-course'/>
</%block>
<div class="message-banner" aria-live="polite"></div>
<main id="main" aria-label="Content" tabindex="-1">
<div class="wrapper-profile">
<div class="profile ${'profile-self' if own_profile else 'profile-other'}">
<div class="wrapper-profile-field-account-privacy">
% if own_profile and records_url:
<div class="wrapper-profile-records">
<a href="${records_url}">
<button class="btn profile-records-button">${_("View My Records")}</button>
</a>
</div>
% endif
</div>
% if own_profile:
<div class="profile-header">
<h2 class="header">${_("My Profile")}</h2>
<div class="subheader">
${_('Build out your profile to personalize your identity on {platform_name}.').format(
platform_name=platform_name,
)}
</div>
</div>
% endif
<div class="wrapper-profile-sections account-settings-container">
<div class="ui-loading-indicator">
<p><span class="spin"><span class="icon fa fa-refresh" aria-hidden="true"></span></span> <span class="copy">${_("Loading")}</span></p>
</div>
<div class="wrapper-profile-section-container-one is-hidden">
<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">${_("An error occurred. Try loading the page again.")}</span>
</div>
</div>
<div class="wrapper-profile-section-container-two is-hidden">
<div class="wrapper-profile-bio"></div>
% if achievements_fragment:
${HTML(achievements_fragment.body_html())}
% endif
</div>
</div>
</div>
</div>
</main>
<%block name="js_extra">
<%static:require_module module_name="learner_profile/js/learner_profile_factory" class_name="LearnerProfileFactory">
var options = ${data | n, dump_js_escaped_json};
LearnerProfileFactory(options);
</%static:require_module>
</%block>

View File

@@ -1,281 +0,0 @@
""" Tests for student profile views. """
import datetime
from unittest import mock
import ddt
from django.conf import settings
from django.test.client import RequestFactory
from django.urls import reverse
from edx_toggles.toggles.testutils import override_waffle_flag
from opaque_keys.edx.locator import CourseLocator
from common.djangoapps.course_modes.models import CourseMode
from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory
from common.djangoapps.util.testing import UrlResetMixin
from lms.djangoapps.certificates.data import CertificateStatuses
from lms.djangoapps.certificates.tests.factories import GeneratedCertificateFactory
from lms.envs.test import CREDENTIALS_PUBLIC_SERVICE_URL
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.site_configuration.tests.mixins import SiteMixin
from openedx.features.learner_profile.toggles import REDIRECT_TO_PROFILE_MICROFRONTEND
from openedx.features.learner_profile.views.learner_profile import learner_profile_context
from xmodule.data import CertificatesDisplayBehaviors # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@ddt.ddt
class LearnerProfileViewTest(SiteMixin, UrlResetMixin, ModuleStoreTestCase):
""" Tests for the student profile view. """
USERNAME = "username"
OTHER_USERNAME = "other_user"
PASSWORD = "password"
DOWNLOAD_URL = "http://www.example.com/certificate.pdf"
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',
'account_settings_data',
'preferences_data',
]
def setUp(self):
super().setUp()
self.user = UserFactory.create(username=self.USERNAME, password=self.PASSWORD)
self.other_user = UserFactory.create(username=self.OTHER_USERNAME, password=self.PASSWORD)
self.client.login(username=self.USERNAME, password=self.PASSWORD)
self.course = CourseFactory.create(
start=datetime.datetime(2013, 9, 16, 7, 17, 28),
end=datetime.datetime.now(),
certificate_available_date=datetime.datetime.now(),
)
def test_context(self):
"""
Verify learner profile page context data.
"""
request = RequestFactory().get('/url')
request.user = self.user
context = learner_profile_context(request, self.USERNAME, self.user.is_staff)
assert context['data']['default_public_account_fields'] == \
settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields']
assert context['data']['accounts_api_url'] == \
reverse('accounts_api', kwargs={'username': self.user.username})
assert context['data']['preferences_api_url'] == \
reverse('preferences_api', kwargs={'username': self.user.username})
assert context['data']['profile_image_upload_url'] == \
reverse('profile_image_upload', kwargs={'username': self.user.username})
assert context['data']['profile_image_remove_url'] == \
reverse('profile_image_remove', kwargs={'username': self.user.username})
assert context['data']['profile_image_max_bytes'] == settings.PROFILE_IMAGE_MAX_BYTES
assert context['data']['profile_image_min_bytes'] == settings.PROFILE_IMAGE_MIN_BYTES
assert context['data']['account_settings_page_url'] == reverse('account_settings')
for attribute in self.CONTEXT_DATA:
assert attribute in 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.assertContains(response, attribute)
def test_redirect_view(self):
with override_waffle_flag(REDIRECT_TO_PROFILE_MICROFRONTEND, active=True):
profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME})
# Test with waffle flag active and site setting disabled, does not redirect
response = self.client.get(path=profile_path)
for attribute in self.CONTEXT_DATA:
self.assertContains(response, attribute)
# Test with waffle flag active and site setting enabled, redirects to microfrontend
site_domain = 'othersite.example.com'
self.set_up_site(site_domain, {
'SITE_NAME': site_domain,
'ENABLE_PROFILE_MICROFRONTEND': True
})
self.client.login(username=self.USERNAME, password=self.PASSWORD)
response = self.client.get(path=profile_path)
profile_url = settings.PROFILE_MICROFRONTEND_URL
self.assertRedirects(response, profile_url + self.USERNAME, fetch_redirect_response=False)
def test_records_link(self):
profile_path = reverse('learner_profile', kwargs={'username': self.USERNAME})
response = self.client.get(path=profile_path)
self.assertContains(response, f'<a href="{CREDENTIALS_PUBLIC_SERVICE_URL}/records/">')
def test_undefined_profile_page(self):
"""
Verify that a 404 is returned for a non-existent profile page.
"""
profile_path = reverse('learner_profile', kwargs={'username': "no_such_user"})
response = self.client.get(path=profile_path)
assert 404 == response.status_code
def _create_certificate(self, course_key=None, enrollment_mode=CourseMode.HONOR, status='downloadable'):
"""Simulate that the user has a generated certificate. """
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, mode=enrollment_mode)
return GeneratedCertificateFactory(
user=self.user,
course_id=course_key or self.course.id,
mode=enrollment_mode,
download_url=self.DOWNLOAD_URL,
status=status,
)
@ddt.data(CourseMode.HONOR, CourseMode.PROFESSIONAL, CourseMode.VERIFIED)
def test_certificate_visibility(self, cert_mode):
"""
Verify that certificates are displayed with the correct card mode.
"""
# Add new certificate
cert = self._create_certificate(enrollment_mode=cert_mode)
cert.save()
response = self.client.get(f'/u/{self.user.username}')
self.assertContains(response, f'card certificate-card mode-{cert_mode}')
@ddt.data(
['downloadable', True],
['notpassing', False],
)
@ddt.unpack
def test_certificate_status_visibility(self, status, is_passed_status):
"""
Verify that certificates are only displayed for passing status.
"""
# Add new certificate
cert = self._create_certificate(status=status)
cert.save()
# Ensure that this test is actually using both passing and non-passing certs.
assert CertificateStatuses.is_passing_status(cert.status) == is_passed_status
response = self.client.get(f'/u/{self.user.username}')
if is_passed_status:
self.assertContains(response, f'card certificate-card mode-{cert.mode}')
else:
self.assertNotContains(response, f'card certificate-card mode-{cert.mode}')
def test_certificate_for_missing_course(self):
"""
Verify that a certificate is not shown for a missing course.
"""
# Add new certificate
cert = self._create_certificate(course_key=CourseLocator.from_string('course-v1:edX+INVALID+1'))
cert.save()
response = self.client.get(f'/u/{self.user.username}')
self.assertNotContains(response, f'card certificate-card mode-{cert.mode}')
@ddt.data(True, False)
def test_no_certificate_visibility(self, own_profile):
"""
Verify that the 'You haven't earned any certificates yet.' well appears on the user's
own profile when they do not have certificates and does not appear when viewing
another user that does not have any certificates.
"""
profile_username = self.user.username if own_profile else self.other_user.username
response = self.client.get(f'/u/{profile_username}')
if own_profile:
self.assertContains(response, 'You haven&#39;t earned any certificates yet.')
else:
self.assertNotContains(response, 'You haven&#39;t earned any certificates yet.')
@ddt.data(True, False)
def test_explore_courses_visibility(self, courses_browsable):
with mock.patch.dict('django.conf.settings.FEATURES', {'COURSES_ARE_BROWSABLE': courses_browsable}):
response = self.client.get(f'/u/{self.user.username}')
if courses_browsable:
self.assertContains(response, 'Explore New Courses')
else:
self.assertNotContains(response, 'Explore New Courses')
def test_certificate_for_visibility_for_not_viewable_course(self):
"""
Verify that a certificate is not shown if certificate are not viewable to users.
"""
# add new course with certificate_available_date is future date.
course = CourseFactory.create(
certificate_available_date=datetime.datetime.now() + datetime.timedelta(days=5),
certificates_display_behavior=CertificatesDisplayBehaviors.END_WITH_DATE
)
cert = self._create_certificate(course_key=course.id)
cert.save()
response = self.client.get(f'/u/{self.user.username}')
self.assertNotContains(response, f'card certificate-card mode-{cert.mode}')
def test_certificates_visible_only_for_staff_and_profile_user(self):
"""
Verify that certificates data are passed to template only in case of staff user
and profile user.
"""
request = RequestFactory().get('/url')
request.user = self.user
profile_username = self.other_user.username
user_is_staff = True
context = learner_profile_context(request, profile_username, user_is_staff)
assert 'achievements_fragment' in context
user_is_staff = False
context = learner_profile_context(request, profile_username, user_is_staff)
assert 'achievements_fragment' not in context
profile_username = self.user.username
context = learner_profile_context(request, profile_username, user_is_staff)
assert 'achievements_fragment' in context
@mock.patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
def test_certificate_visibility_with_no_cert_config(self):
"""
Verify that certificates are not displayed until there is an active
certificate configuration.
"""
# Add new certificate
cert = self._create_certificate(enrollment_mode=CourseMode.VERIFIED)
cert.download_url = ''
cert.save()
response = self.client.get(f'/u/{self.user.username}')
self.assertNotContains(
response, f'card certificate-card mode-{CourseMode.VERIFIED}'
)
course_overview = CourseOverview.get_from_id(self.course.id)
course_overview.has_any_active_web_certificate = True
course_overview.save()
response = self.client.get(f'/u/{self.user.username}')
self.assertContains(
response, f'card certificate-card mode-{CourseMode.VERIFIED}'
)

View File

@@ -1,29 +0,0 @@
"""
Toggles for Learner Profile page.
"""
from edx_toggles.toggles import WaffleFlag
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
# Namespace for learner profile waffle flags.
WAFFLE_FLAG_NAMESPACE = 'learner_profile'
# Waffle flag to redirect to another learner profile experience.
# .. toggle_name: learner_profile.redirect_to_microfrontend
# .. toggle_implementation: WaffleFlag
# .. toggle_default: False
# .. toggle_description: Supports staged rollout of a new micro-frontend-based implementation of the profile page.
# .. toggle_use_cases: temporary, open_edx
# .. toggle_creation_date: 2019-02-19
# .. toggle_target_removal_date: 2020-12-31
# .. toggle_warning: Also set settings.PROFILE_MICROFRONTEND_URL and site's ENABLE_PROFILE_MICROFRONTEND.
# .. toggle_tickets: DEPR-17
REDIRECT_TO_PROFILE_MICROFRONTEND = WaffleFlag(f'{WAFFLE_FLAG_NAMESPACE}.redirect_to_microfrontend', __name__)
def should_redirect_to_profile_microfrontend():
return (
configuration_helpers.get_value('ENABLE_PROFILE_MICROFRONTEND') and
REDIRECT_TO_PROFILE_MICROFRONTEND.is_enabled()
)

View File

@@ -1,24 +0,0 @@
"""
Defines URLs for the learner profile.
"""
from django.conf import settings
from django.urls import path, re_path
from openedx.features.learner_profile.views.learner_profile import learner_profile
from .views.learner_achievements import LearnerAchievementsFragmentView
urlpatterns = [
re_path(
r'^{username_pattern}$'.format(
username_pattern=settings.USERNAME_PATTERN,
),
learner_profile,
name='learner_profile',
),
path('achievements', LearnerAchievementsFragmentView.as_view(),
name='openedx.learner_profile.learner_achievements_fragment_view',
),
]

View File

@@ -1,58 +0,0 @@
"""
Views to render a learner's achievements.
"""
from django.template.loader import render_to_string
from web_fragments.fragment import Fragment
from lms.djangoapps.certificates import api as certificate_api
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.plugin_api.views import EdxFragmentView
class LearnerAchievementsFragmentView(EdxFragmentView):
"""
A fragment to render a learner's achievements.
"""
def render_to_fragment(self, request, username=None, own_profile=False, **kwargs): # lint-amnesty, pylint: disable=arguments-differ
"""
Renders the current learner's achievements.
"""
course_certificates = self._get_ordered_certificates_for_user(request, username)
context = {
'course_certificates': course_certificates,
'own_profile': own_profile,
'disable_courseware_js': True,
}
if course_certificates or own_profile:
html = render_to_string('learner_profile/learner-achievements-fragment.html', context)
return Fragment(html)
else:
return None
def _get_ordered_certificates_for_user(self, request, username):
"""
Returns a user's certificates sorted by course name.
"""
course_certificates = certificate_api.get_certificates_for_user(username)
passing_certificates = []
for course_certificate in course_certificates:
if course_certificate.get('is_passing', False):
course_key = course_certificate['course_key']
try:
course_overview = CourseOverview.get_from_id(course_key)
course_certificate['course'] = course_overview
if certificate_api.certificates_viewable_for_course(course_overview):
# add certificate into passing certificate list only if it's a PDF certificate
# or there is an active certificate configuration.
if course_certificate['is_pdf_certificate'] or course_overview.has_any_active_web_certificate:
passing_certificates.append(course_certificate)
except CourseOverview.DoesNotExist:
# This is unlikely to fail as the course should exist.
# Ideally the cert should have all the information that
# it needs. This might be solved by the Credentials API.
pass
passing_certificates.sort(key=lambda certificate: certificate['course'].display_name_with_default)
return passing_certificates

View File

@@ -1,134 +0,0 @@
""" Views for a student's profile information. """
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.contrib.staticfiles.storage import staticfiles_storage
from django.core.exceptions import ObjectDoesNotExist
from django.http import Http404
from django.shortcuts import redirect, render
from django.urls import reverse
from django.views.decorators.http import require_http_methods
from django_countries import countries
from lms.djangoapps.badges.utils import badges_enabled
from common.djangoapps.edxmako.shortcuts import marketing_link
from openedx.core.djangoapps.credentials.utils import get_credentials_records_url
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
from openedx.core.djangoapps.user_api.errors import UserNotAuthorized, UserNotFound
from openedx.core.djangoapps.user_api.preferences.api import get_user_preferences
from openedx.features.learner_profile.toggles import should_redirect_to_profile_microfrontend
from openedx.features.learner_profile.views.learner_achievements import LearnerAchievementsFragmentView
from common.djangoapps.student.models import User
@login_required
@require_http_methods(['GET'])
def learner_profile(request, username):
"""Render the profile page for the specified username.
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
Raises:
Http404: 404 if the specified user is not authorized or does not exist
Example usage:
GET /account/profile
"""
if should_redirect_to_profile_microfrontend():
profile_microfrontend_url = f"{settings.PROFILE_MICROFRONTEND_URL}{username}"
if request.GET:
profile_microfrontend_url += f'?{request.GET.urlencode()}'
return redirect(profile_microfrontend_url)
try:
context = learner_profile_context(request, username, request.user.is_staff)
return render(
request=request,
template_name='learner_profile/learner_profile.html',
context=context
)
except (UserNotAuthorized, UserNotFound, ObjectDoesNotExist):
raise Http404 # lint-amnesty, pylint: disable=raise-missing-from
def learner_profile_context(request, profile_username, user_is_staff):
"""Context for the learner profile page.
Args:
logged_in_user (object): Logged In user.
profile_username (str): username of user whose profile is requested.
user_is_staff (bool): Logged In user has staff access.
build_absolute_uri_func ():
Returns:
dict
Raises:
ObjectDoesNotExist: the specified profile_username does not exist.
"""
profile_user = User.objects.get(username=profile_username)
logged_in_user = request.user
own_profile = (logged_in_user.username == profile_username)
account_settings_data = get_account_settings(request, [profile_username])[0]
preferences_data = get_user_preferences(profile_user, profile_username)
context = {
'own_profile': own_profile,
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
'data': {
'profile_user_id': profile_user.id,
'default_public_account_fields': settings.ACCOUNT_VISIBILITY_CONFIGURATION['public_fields'],
'default_visibility': settings.ACCOUNT_VISIBILITY_CONFIGURATION['default_visibility'],
'accounts_api_url': reverse("accounts_api", kwargs={'username': profile_username}),
'preferences_api_url': reverse('preferences_api', kwargs={'username': profile_username}),
'preferences_data': preferences_data,
'account_settings_data': account_settings_data,
'profile_image_upload_url': reverse('profile_image_upload', kwargs={'username': profile_username}),
'profile_image_remove_url': reverse('profile_image_remove', kwargs={'username': profile_username}),
'profile_image_max_bytes': settings.PROFILE_IMAGE_MAX_BYTES,
'profile_image_min_bytes': settings.PROFILE_IMAGE_MIN_BYTES,
'account_settings_page_url': reverse('account_settings'),
'has_preferences_access': (logged_in_user.username == profile_username or user_is_staff),
'own_profile': own_profile,
'country_options': list(countries),
'find_courses_url': marketing_link('COURSES'),
'language_options': settings.ALL_LANGUAGES,
'badges_logo': staticfiles_storage.url('certificates/images/backpack-logo.png'),
'badges_icon': staticfiles_storage.url('certificates/images/ico-mozillaopenbadges.png'),
'backpack_ui_img': staticfiles_storage.url('certificates/images/backpack-ui.png'),
'platform_name': configuration_helpers.get_value('platform_name', settings.PLATFORM_NAME),
'social_platforms': settings.SOCIAL_PLATFORMS,
'enable_coppa_compliance': settings.ENABLE_COPPA_COMPLIANCE,
'parental_consent_age_limit': settings.PARENTAL_CONSENT_AGE_LIMIT
},
'show_program_listing': ProgramsApiConfig.is_enabled(),
'show_dashboard_tabs': True,
'disable_courseware_js': True,
'nav_hidden': True,
'records_url': get_credentials_records_url(),
}
if own_profile or user_is_staff:
achievements_fragment = LearnerAchievementsFragmentView().render_to_fragment(
request,
username=profile_user.username,
own_profile=own_profile,
)
context['achievements_fragment'] = achievements_fragment
if badges_enabled():
context['data']['badges_api_url'] = reverse("badges_api:user_assertions", kwargs={'username': profile_username})
return context

View File

@@ -81,9 +81,6 @@ module.exports = {
path.resolve(__dirname, '../lms/static/js/learner_dashboard/views/program_header_view.js'),
path.resolve(__dirname, '../lms/static/js/learner_dashboard/views/sidebar_view.js'),
path.resolve(__dirname, '../lms/static/js/learner_dashboard/views/upgrade_message_view.js'),
path.resolve(__dirname, '../lms/static/js/student_account/views/account_section_view.js'),
path.resolve(__dirname, '../lms/static/js/student_account/views/account_settings_fields.js'),
path.resolve(__dirname, '../lms/static/js/student_account/views/account_settings_view.js'),
path.resolve(__dirname, '../lms/static/js/student_account/views/FormView.js'),
path.resolve(__dirname, '../lms/static/js/student_account/views/LoginView.js'),
path.resolve(__dirname, '../lms/static/js/student_account/views/RegisterView.js'),