diff --git a/common/djangoapps/student/views/management.py b/common/djangoapps/student/views/management.py index 4cf8fad8aea..2bd91d5d647 100644 --- a/common/djangoapps/student/views/management.py +++ b/common/djangoapps/student/views/management.py @@ -424,8 +424,8 @@ def change_enrollment(request, check_access=True): enroll_mode = CourseMode.auto_enroll_mode(course_id, available_modes) if enroll_mode: CourseEnrollment.enroll(user, course_id, check_access=check_access, mode=enroll_mode) - except EnrollmentNotAllowed as exc: - return HttpResponseBadRequest(str(exc)) + except EnrollmentNotAllowed as exec: # pylint: disable=broad-except + return HttpResponseBadRequest(str(exec)) except Exception: # pylint: disable=broad-except return HttpResponseBadRequest(_("Could not enroll")) diff --git a/lms/djangoapps/certificates/views/webview.py b/lms/djangoapps/certificates/views/webview.py index 3b6cc75e489..1c75860204e 100644 --- a/lms/djangoapps/certificates/views/webview.py +++ b/lms/djangoapps/certificates/views/webview.py @@ -585,6 +585,21 @@ def render_html_view(request, course_id, certificate=None): # pylint: disable=t context.update(get_certificate_header_context(is_secure=request.is_secure())) context.update(get_certificate_footer_context()) + # Append/Override the existing view context values with plugin defined values + run_extension_point( + 'NAU_CERTIFICATE_CONTEXT_EXTENSION', + context=context, + request=request, + course=course, + user=user, + user_certificate=user_certificate, + configuration=configuration, + certificate_language=certificate_language, + ) + + # NAU Customization so the filter know which certificate is being rendered + context['user_certificate'] = user_certificate + # Append/Override the existing view context values with any course-specific static values from Advanced Settings context.update(course.cert_html_view_overrides) diff --git a/lms/djangoapps/instructor/tests/test_api.py b/lms/djangoapps/instructor/tests/test_api.py index f1e7322abc6..fb5a5f865b1 100644 --- a/lms/djangoapps/instructor/tests/test_api.py +++ b/lms/djangoapps/instructor/tests/test_api.py @@ -22,6 +22,7 @@ from django.http import HttpRequest, HttpResponse from django.test import RequestFactory, TestCase from django.test.client import MULTIPART_CONTENT +from django.test.utils import override_settings from django.urls import reverse as django_reverse from django.utils.translation import gettext as _ from edx_when.api import get_dates_for_course, get_overrides_for_user, set_date_for_block @@ -2626,6 +2627,23 @@ def test_get_students_features(self): assert student_json['city'] == student.profile.city assert student_json['country'] == '' + @ddt.data(True, False) + def test_get_students_features_private_fields(self, show_private_fields): + """ + Test that the get_students_features returns the expected private fields + """ + with override_settings(FEATURES={'SHOW_PRIVATE_FIELDS_IN_PROFILE_INFORMATION_REPORT': show_private_fields}): + url = reverse('get_students_features', kwargs={'course_id': str(self.course.id)}) + response = self.client.post(url, {}) + res_json = json.loads(response.content.decode('utf-8')) + + assert 'students' in res_json + for student in res_json['students']: + if show_private_fields: + assert 'year_of_birth' in student + else: + assert 'year_of_birth' not in student + @ddt.data(True, False) def test_get_students_features_cohorted(self, is_cohorted): """ diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 84058dfeae4..c90407d68fe 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -1485,40 +1485,45 @@ def post(self, request, course_id, csv=False): # pylint: disable=redefined-oute # We need to clone the list because we modify it below query_features = list(configuration_helpers.get_value('student_profile_download_fields', [])) - if not query_features: - query_features = [ - 'id', 'username', 'name', 'email', 'language', 'location', - 'year_of_birth', 'gender', 'level_of_education', 'mailing_address', - 'goals', 'enrollment_mode', 'last_login', 'date_joined', 'external_user_key' - ] - keep_field_private(query_features, 'year_of_birth') # protected information - - # Provide human-friendly and translatable names for these features. These names - # will be displayed in the table generated in data_download.js. It is not (yet) - # used as the header row in the CSV, but could be in the future. - query_features_names = { - 'id': _('User ID'), - 'username': _('Username'), - 'name': _('Name'), - 'email': _('Email'), - 'language': _('Language'), - 'location': _('Location'), - # 'year_of_birth': _('Birth Year'), treated as privileged information as of TNL-10683, - # not to go in reports - 'gender': _('Gender'), - 'level_of_education': _('Level of Education'), - 'mailing_address': _('Mailing Address'), - 'goals': _('Goals'), - 'enrollment_mode': _('Enrollment Mode'), - 'last_login': _('Last Login'), - 'date_joined': _('Date Joined'), - 'external_user_key': _('External User Key'), - } + if not query_features: + query_features = [ + 'id', 'username', 'name', 'email', 'language', 'location', + 'year_of_birth', 'gender', 'level_of_education', 'mailing_address', + 'goals', 'enrollment_mode', 'verification_status', + 'last_login', 'date_joined', 'external_user_key', + 'enrollment_date' + ] + + # Provide human-friendly and translatable names for these features. These names + # will be displayed in the table generated in data_download.js. It is not (yet) + # used as the header row in the CSV, but could be in the future. + query_features_names = { + 'id': _('User ID'), + 'username': _('Username'), + 'name': _('Name'), + 'email': _('Email'), + 'language': _('Language'), + 'location': _('Location'), + 'year_of_birth': _('Birth Year'), + 'gender': _('Gender'), + 'level_of_education': _('Level of Education'), + 'mailing_address': _('Mailing Address'), + 'goals': _('Goals'), + 'enrollment_mode': _('Enrollment Mode'), + 'last_login': _('Last Login'), + 'date_joined': _('Date Joined'), + 'external_user_key': _('External User Key'), + 'enrollment_date': _('Enrollment Date'), + } + + if not settings.FEATURES.get('SHOW_PRIVATE_FIELDS_IN_PROFILE_INFORMATION_REPORT', False): + keep_field_private(query_features, 'year_of_birth') + query_features_names.pop('year_of_birth', None) - if is_course_cohorted(course.id): - # Translators: 'Cohort' refers to a group of students within a course. - query_features.append('cohort') - query_features_names['cohort'] = _('Cohort') + if is_course_cohorted(course.id): + # Translators: 'Cohort' refers to a group of students within a course. + query_features.append('cohort') + query_features_names['cohort'] = _('Cohort') if course.teams_enabled: query_features.append('team') diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py index c7bc6ca6da4..52612b77002 100644 --- a/lms/djangoapps/instructor_analytics/basic.py +++ b/lms/djangoapps/instructor_analytics/basic.py @@ -10,7 +10,6 @@ import logging from django.conf import settings -from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from django.core.exceptions import ObjectDoesNotExist from django.core.serializers.json import DjangoJSONEncoder from django.db.models import Count # lint-amnesty, pylint: disable=unused-import @@ -38,6 +37,7 @@ 'level_of_education', 'mailing_address', 'goals', 'meta', 'city', 'country') PROGRAM_ENROLLMENT_FEATURES = ('external_user_key', ) +ENROLLMENT_FEATURES = ('enrollment_date', ) ORDER_ITEM_FEATURES = ('list_price', 'unit_cost', 'status') ORDER_FEATURES = ('purchase_time',) @@ -49,7 +49,7 @@ 'bill_to_street2', 'bill_to_city', 'bill_to_state', 'bill_to_postalcode', 'bill_to_country', 'order_type', 'created') -AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES + PROGRAM_ENROLLMENT_FEATURES +AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES + PROGRAM_ENROLLMENT_FEATURES + ENROLLMENT_FEATURES COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at', 'is_valid') COUPON_FEATURES = ('code', 'course_id', 'percentage_discount', 'description', 'expiration_date', 'is_active') CERTIFICATE_FEATURES = ('course_id', 'mode', 'status', 'grade', 'created_date', 'is_active', 'error_reason') @@ -84,7 +84,62 @@ def issued_certificates(course_key, features): return generated_certificates -def enrolled_students_features(course_key, features): +def get_student_features_with_custom(course_key): + """ + Allow site operators to include on the export custom fields if platform has an extending + User model. This can be used if you have an extended model that include for example + an university student number. + Basic example of adding age: + ```python + def get_age(self): + return datetime.datetime.now().year - self.profile.year_of_birth + setattr(User, 'age', property(get_age)) + ``` + Then you have to add `age` to both site configurations: + - `student_profile_download_fields_custom_student_attributes` + - `student_profile_download_fields` site configurations` + ```json + "student_profile_download_fields_custom_student_attributes": ["age"], + "student_profile_download_fields": [ + "id", "username", "name", "email", "language", "location", + "year_of_birth", "gender", "level_of_education", "mailing_address", + "goals", "enrollment_mode", "last_login", "date_joined", "external_user_key", + "enrollment_date", "age" + ] + ``` + Example if the platform has a custom user extended model like a One-To-One Link + with the User Model: + ```python + def get_user_extended_model_custom_field(self): + if hasattr(self, "userextendedmodel"): + return self.userextendedmodel.custom_field + return None + setattr(User, 'user_extended_model_custom_field', property(get_user_extended_model_custom_field)) + ``` + ```json + "student_profile_download_fields_custom_student_attributes": ["user_extended_model_custom_field"], + "student_profile_download_fields": [ + "id", "username", "name", "email", "language", "location", + "year_of_birth", "gender", "level_of_education", "mailing_address", + "goals", "enrollment_mode", "last_login", "date_joined", "external_user_key", + "enrollment_date", "user_extended_model_custom_field" + ] + ``` + """ + return STUDENT_FEATURES + tuple( + configuration_helpers.get_value_for_org( + course_key.org, + "student_profile_download_fields_custom_student_attributes", + getattr( + settings, + "STUDENT_PROFILE_DOWNLOAD_FIELDS_CUSTOM_STUDENT_ATTRIBUTES", + (), + ), + ) + ) + + +def enrolled_students_features(course_key, features): # lint-amnesty, pylint: disable=too-many-statements """ Return list of student features as dictionaries. @@ -101,18 +156,25 @@ def enrolled_students_features(course_key, features): include_enrollment_mode = 'enrollment_mode' in features include_verification_status = 'verification_status' in features include_program_enrollments = 'external_user_key' in features + include_enrollment_date = 'enrollment_date' in features external_user_key_dict = {} - students = User.objects.filter( - courseenrollment__course_id=course_key, - courseenrollment__is_active=1, - ).order_by('username').select_related('profile') + enrollments = CourseEnrollment.objects.filter( + course_id=course_key, + is_active=1, + ).select_related('user').order_by('user__username').select_related('user__profile') if include_cohort_column: - students = students.prefetch_related('course_groups') + enrollments = enrollments.prefetch_related('user__course_groups') if include_team_column: - students = students.prefetch_related('teams') + enrollments = enrollments.prefetch_related('user__teams') + + + students = [enrollment.user for enrollment in enrollments] + + # student_features = [x for x in get_student_features_with_custom(course_key) if x in features] + # profile_features = [x for x in PROFILE_FEATURES if x in features] if include_program_enrollments and len(students) > 0: program_enrollments = fetch_program_enrollments_by_students(users=students, realized_only=True) @@ -128,11 +190,15 @@ def extract_attr(student, feature): except TypeError: return str(attr) - def extract_student(student, features): + def extract_enrollment_student(enrollment, features): """ convert student to dictionary """ + student_features = [x for x in STUDENT_FEATURES if x in features] profile_features = [x for x in PROFILE_FEATURES if x in features] + student = enrollment.user + + # For data extractions on the 'meta' field # the feature name should be in the format of 'meta.foo' where # 'foo' is the keyname in the meta dictionary @@ -189,9 +255,12 @@ def extract_student(student, features): # extra external_user_key student_dict['external_user_key'] = external_user_key_dict.get(student.id, '') + if include_enrollment_date: + student_dict['enrollment_date'] = enrollment.created + return student_dict - return [extract_student(student, features) for student in students] + return [extract_enrollment_student(enrollment, features) for enrollment in enrollments] def list_may_enroll(course_key, features): diff --git a/lms/djangoapps/instructor_analytics/tests/test_basic.py b/lms/djangoapps/instructor_analytics/tests/test_basic.py index 83f23246eaa..4261cf31d76 100644 --- a/lms/djangoapps/instructor_analytics/tests/test_basic.py +++ b/lms/djangoapps/instructor_analytics/tests/test_basic.py @@ -5,8 +5,11 @@ from unittest.mock import MagicMock, Mock, patch +import random +import datetime import ddt import json # lint-amnesty, pylint: disable=wrong-import-order +from django.contrib.auth.models import User # lint-amnesty, pylint: disable=imported-auth-user from edx_proctoring.api import create_exam from edx_proctoring.models import ProctoredExamStudentAttempt from opaque_keys.edx.locator import UsageKey @@ -15,6 +18,7 @@ PROFILE_FEATURES, PROGRAM_ENROLLMENT_FEATURES, STUDENT_FEATURES, + ENROLLMENT_FEATURES, StudentModule, enrolled_students_features, get_proctored_exam_results, @@ -24,6 +28,7 @@ ) from lms.djangoapps.program_enrollments.tests.factories import ProgramEnrollmentFactory from openedx.core.djangoapps.course_groups.tests.helpers import CohortFactory +from openedx.core.djangoapps.site_configuration.tests.factories import SiteConfigurationFactory from common.djangoapps.student.models import CourseEnrollment, CourseEnrollmentAllowed from common.djangoapps.student.tests.factories import InstructorFactory from common.djangoapps.student.tests.factories import UserFactory @@ -250,8 +255,61 @@ def test_enrolled_student_features_external_user_keys(self): assert '' == report['external_user_key'] def test_available_features(self): - assert len(AVAILABLE_FEATURES) == len(STUDENT_FEATURES + PROFILE_FEATURES + PROGRAM_ENROLLMENT_FEATURES) - assert set(AVAILABLE_FEATURES) == set(STUDENT_FEATURES + PROFILE_FEATURES + PROGRAM_ENROLLMENT_FEATURES) + assert len(AVAILABLE_FEATURES) == len( + STUDENT_FEATURES + + PROFILE_FEATURES + + PROGRAM_ENROLLMENT_FEATURES + + ENROLLMENT_FEATURES + ) + assert set(AVAILABLE_FEATURES) == set( + STUDENT_FEATURES + + PROFILE_FEATURES + + PROGRAM_ENROLLMENT_FEATURES + + ENROLLMENT_FEATURES + ) + + def test_enrolled_students_enrollment_date(self): + query_features = ('username', 'enrollment_date',) + for feature in query_features: + assert feature in AVAILABLE_FEATURES + with self.assertNumQueries(2): + userreports = enrolled_students_features(self.course_key, query_features) + assert len(userreports) == len(self.users) + + userreports = sorted(userreports, key=lambda u: u["username"]) + users = sorted(self.users, key=lambda u: u.username) + for userreport, user in zip(userreports, users): + assert set(userreport.keys()) == set(query_features) + assert userreport['enrollment_date'] == CourseEnrollment.enrollments_for_user(user)[0].created + + def test_enrolled_students_extended_model_age(self): + SiteConfigurationFactory.create( + site_values={ + 'course_org_filter': ['robot'], + 'student_profile_download_fields_custom_student_attributes': ['age'], + } + ) + + def get_age(self): + return datetime.datetime.now().year - self.profile.year_of_birth + setattr(User, "age", property(get_age)) # lint-amnesty, pylint: disable=literal-used-as-attribute + + for user in self.users: + user.profile.year_of_birth = random.randint(1900, 2000) + user.profile.save() + + query_features = ('username', 'age',) + with self.assertNumQueries(3): + userreports = enrolled_students_features(self.course_key, query_features) + assert len(userreports) == len(self.users) + + userreports = sorted(userreports, key=lambda u: u["username"]) + users = sorted(self.users, key=lambda u: u.username) + for userreport, user in zip(userreports, users): + assert set(userreport.keys()) == set(query_features) + assert userreport['age'] == str(user.age) + + delattr(User, "age") # lint-amnesty, pylint: disable=literal-used-as-attribute def test_list_may_enroll(self): may_enroll = list_may_enroll(self.course_key, ['email']) diff --git a/lms/djangoapps/support/views/manage_user.py b/lms/djangoapps/support/views/manage_user.py index e29652a905c..958092982a1 100644 --- a/lms/djangoapps/support/views/manage_user.py +++ b/lms/djangoapps/support/views/manage_user.py @@ -18,7 +18,7 @@ from openedx.core.djangoapps.user_api.accounts.serializers import AccountUserSerializer from openedx.core.djangolib.oauth2_retirement_utils import retire_dot_oauth2_models -from edx_django_utils.user import generate_password # lint-amnesty, pylint: disable=wrong-import-order +from openedx.core.djangoapps.user_authn.utils import generate_password # lint-amnesty, pylint: disable=wrong-import-order class ManageUserSupportView(View): diff --git a/lms/envs/common.py b/lms/envs/common.py index 763bd83b9d8..db0ba2ef9e2 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -1067,6 +1067,16 @@ # .. toggle_creation_date: 2024-04-02 # .. toggle_target_removal_date: None 'BADGES_ENABLED': False, + + # .. toggle_name: FEATURES['SHOW_PRIVATE_FIELDS_IN_PROFILE_INFORMATION_REPORT'] + # .. toggle_implementation: DjangoSetting + # .. toggle_default: False + # .. toggle_description: Adds private fields to the profile information report. + # .. toggle_use_cases: open_edx + # .. toggle_creation_date: 2025-05-12 + # .. toggle_target_removal_date: None + # .. toggle_tickets: https://github.com/openedx/edx-platform/pull/36688 + 'SHOW_PRIVATE_FIELDS_IN_PROFILE_INFORMATION_REPORT': False, } # Specifies extra XBlock fields that should available when requested via the Course Blocks API diff --git a/lms/static/js/spec/student_account/register_spec.js b/lms/static/js/spec/student_account/register_spec.js index 17d82e2b13d..506b03b697a 100644 --- a/lms/static/js/spec/student_account/register_spec.js +++ b/lms/static/js/spec/student_account/register_spec.js @@ -536,17 +536,21 @@ expect(elementChildren.length).toEqual(1); }); - it('hides optional fields by default', function() { + // These tests behave contrary to the use case of nau + xit('hides optional fields by default', function() { createRegisterView(this); expect(view.$('.optional-fields')).toHaveClass('hidden'); }); - it('displays optional fields when checkbox is selected', function() { + // These tests behave contrary to the use case of nau + xit('displays optional fields when checkbox is selected', function() { createRegisterView(this); $('#toggle_optional_fields').click(); expect(view.$('.optional-fields')).not.toHaveClass('hidden'); }); + + it('displays a modal with the terms of service', function() { var $modal, $content; diff --git a/lms/static/js/student_account/views/RegisterView.js b/lms/static/js/student_account/views/RegisterView.js index 42ab7c8857a..8a6e17ef013 100644 --- a/lms/static/js/student_account/views/RegisterView.js +++ b/lms/static/js/student_account/views/RegisterView.js @@ -254,7 +254,8 @@ }; FormView.prototype.postRender.call(this); - $('.optional-fields').addClass('hidden'); + // NAU always show optional fields + // $('.optional-fields').addClass('hidden'); $('#toggle_optional_fields').change(function() { window.analytics.track('edx.bi.user.register.optional_fields_selected'); $('.optional-fields').toggleClass('hidden'); @@ -273,14 +274,18 @@ // improvement so that we don't have to show all the optional fields. // xss-lint: disable=javascript-jquery-insert-into-target $('.checkbox-optional_fields_toggle').insertAfter('.required-fields'); - if (!this.hasOptionalFields) { + // NAU always hide checkbox to show/hide optional fields + // if (!this.hasOptionalFields) { $('.checkbox-optional_fields_toggle').addClass('hidden'); - } + // } // xss-lint: disable=javascript-jquery-insert-into-target $('.checkbox-honor_code').insertAfter('.optional-fields'); // xss-lint: disable=javascript-jquery-insert-into-target $('.checkbox-terms_of_service').insertAfter('.optional-fields'); + // NAU custom, move this to be the first after the optional fields + $('.checkbox-data_authorization').insertAfter('.optional-fields') + // Clicking on links inside a label should open that link. $('label a').click(function(ev) { ev.stopPropagation(); diff --git a/lms/static/js/student_account/views/account_settings_factory.js b/lms/static/js/student_account/views/account_settings_factory.js new file mode 100644 index 00000000000..c39eecff14d --- /dev/null +++ b/lms/static/js/student_account/views/account_settings_factory.js @@ -0,0 +1,505 @@ +// eslint-disable-next-line no-shadow-restricted-names +(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( + '', { + edx_support_url: edxSupportUrl + } + ) + ), + link_end: HtmlUtils.HTML('') + } + ); + } + + 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 + }) + }); + } else { + if (fieldItem.field_type === 'CheckboxField') { + additionalFields.fields.push({ + view: new AccountSettingsFieldViews.ExtendedFieldCheckboxFieldView({ + model: userAccountModel, + title: fieldItem.field_label, + fieldName: fieldItem.field_name, + 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) { + // eslint-disable-next-line no-shadow + 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) { + // eslint-disable-next-line no-bitwise + 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); diff --git a/lms/static/js/student_account/views/account_settings_fields.js b/lms/static/js/student_account/views/account_settings_fields.js new file mode 100644 index 00000000000..0381a2b4472 --- /dev/null +++ b/lms/static/js/student_account/views/account_settings_fields.js @@ -0,0 +1,526 @@ +// eslint-disable-next-line no-shadow-restricted-names +(function(define, undefined) { + 'use strict'; + + define([ + 'gettext', + 'jquery', + 'underscore', + 'backbone', + 'js/views/fields', + 'text!templates/fields/field_checkbox_account.underscore', + '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_checkbox_account_template, + 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( + '', { + passwordResetSupportUrl: this.options.passwordResetSupportUrl + } + ) + ), + anchorEnd: HtmlUtils.HTML('') + } + ) + ); + } + }), + 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.fieldName, + 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); + } + } + }), + ExtendedFieldCheckboxFieldView: FieldViews.EditableFieldView.extend({ + + fieldType: 'checkbox', + fieldTemplate: field_checkbox_account_template, + + events: { + 'change input': 'saveValue' + }, + + initialize: function(options) { + this._super(options); + _.bindAll(this, 'render', 'fieldValue', 'updateValueInField', 'saveValue'); + this.listenTo(this.model, 'change:' + this.options.valueAttribute, this.updateValueInField); + }, + + render: function () { + HtmlUtils.setHtml(this.$el, HtmlUtils.template(field_checkbox_account_template)({ + id: this.options.valueAttribute + '_' + this.options.fieldName, + title: this.options.title, + value: this.modelValue(), + message: this.options.helpMessage + })); + 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; + }, + + fieldValue: function() { + return this.$('.u-field-value input').is(':checked'); + }, + + updateValueInField: function() { + const checked = this.modelValue() === true; + this.$('.u-field-value input').prop('checked', checked); + }, + + saveValue: function () { + let attributes = {}, value; + + if (this.persistChanges === true) { + value = [{ + 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); diff --git a/lms/templates/fields/field_checkbox_account.underscore b/lms/templates/fields/field_checkbox_account.underscore new file mode 100644 index 00000000000..8792366102b --- /dev/null +++ b/lms/templates/fields/field_checkbox_account.underscore @@ -0,0 +1,13 @@ +