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:
@@ -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,
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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'
|
||||
}])
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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/*
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -1 +0,0 @@
|
||||
../../openedx/features/learner_profile/static/learner_profile
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
<% }); %>
|
||||
@@ -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:
|
||||
|
||||
@@ -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/',
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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/$',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -1,8 +0,0 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define(['backbone'], function(Backbone) {
|
||||
var BadgesModel = Backbone.Model.extend({});
|
||||
return BadgesModel;
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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't earned any certificates yet.')
|
||||
else:
|
||||
self.assertNotContains(response, 'You haven'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}'
|
||||
)
|
||||
@@ -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()
|
||||
)
|
||||
@@ -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',
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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'),
|
||||
|
||||
Reference in New Issue
Block a user