Merge pull request #16472 from edx/HarryRein/course-run-selection-dashboard
Allow users to fulfill entitlements and change enrollments from the course dashboard.
This commit is contained in:
@@ -4,6 +4,7 @@ from uuid import uuid4
|
||||
import factory
|
||||
from factory.fuzzy import FuzzyChoice, FuzzyText
|
||||
|
||||
from student.tests.factories import UserFactory
|
||||
from course_modes.helpers import CourseMode
|
||||
from entitlements.models import CourseEntitlement, CourseEntitlementPolicy
|
||||
from openedx.core.djangoapps.site_configuration.tests.factories import SiteFactory
|
||||
@@ -29,4 +30,5 @@ class CourseEntitlementFactory(factory.django.DjangoModelFactory):
|
||||
mode = FuzzyChoice([CourseMode.VERIFIED, CourseMode.PROFESSIONAL])
|
||||
user = factory.SubFactory(UserFactory)
|
||||
order_number = FuzzyText(prefix='TEXTX', chars=string.digits)
|
||||
enrollment_course_run = None
|
||||
policy = factory.SubFactory(CourseEntitlementPolicyFactory)
|
||||
|
||||
@@ -371,8 +371,7 @@ class TestCourseVerificationStatus(UrlResetMixin, ModuleStoreTestCase):
|
||||
# Verify that the correct banner color is rendered
|
||||
self.assertContains(
|
||||
response,
|
||||
"<div class=\"course {}\" aria-labelledby=\"course-title-{}\">".format(
|
||||
self.MODE_CLASSES[status], self.course.id)
|
||||
"<article class=\"course {}\"".format(self.MODE_CLASSES[status])
|
||||
)
|
||||
|
||||
# Verify that the correct copy is rendered on the dashboard
|
||||
|
||||
@@ -18,6 +18,9 @@ from mock import patch
|
||||
from opaque_keys import InvalidKeyError
|
||||
from pyquery import PyQuery as pq
|
||||
|
||||
from entitlements.tests.factories import CourseEntitlementFactory
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.content.course_overviews.tests.factories import CourseOverviewFactory
|
||||
from student.cookies import get_user_info_cookie_data
|
||||
from student.helpers import DISABLE_UNENROLL_CERT_STATES
|
||||
from student.models import CourseEnrollment, UserProfile
|
||||
@@ -335,3 +338,57 @@ class StudentDashboardTests(SharedModuleStoreTestCase, MilestonesTestCaseMixin):
|
||||
remove_prerequisite_course(self.course.id, get_course_milestones(self.course.id)[0])
|
||||
response = self.client.get(reverse('dashboard'))
|
||||
self.assertNotIn('<div class="prerequisites">', response.content)
|
||||
|
||||
@patch('student.views.get_course_runs_for_course')
|
||||
@patch.object(CourseOverview, 'get_from_id')
|
||||
def test_unfulfilled_entitlement(self, mock_course_overview, mock_course_runs):
|
||||
"""
|
||||
When a learner has an unfulfilled entitlement, their course dashboard should have:
|
||||
- a hidden 'View Course' button
|
||||
- the text 'In order to view the course you must select a session:'
|
||||
- an unhidden course-entitlement-selection-container
|
||||
"""
|
||||
CourseEntitlementFactory(user=self.user)
|
||||
mock_course_overview.return_value = CourseOverviewFactory(start=self.TOMORROW)
|
||||
mock_course_runs.return_value = [
|
||||
{
|
||||
'key': 'course-v1:FAKE+FA1-MA1.X+3T2017',
|
||||
'enrollment_end': self.TOMORROW,
|
||||
'pacing_type': 'instructor_paced',
|
||||
'type': 'verified'
|
||||
}
|
||||
]
|
||||
response = self.client.get(self.path)
|
||||
self.assertIn('class="enter-course hidden"', response.content)
|
||||
self.assertIn('You must select a session to access the course.', response.content)
|
||||
self.assertIn('<div class="course-entitlement-selection-container ">', response.content)
|
||||
|
||||
@patch('student.views.get_course_runs_for_course')
|
||||
@patch.object(CourseOverview, 'get_from_id')
|
||||
@patch('opaque_keys.edx.keys.CourseKey.from_string')
|
||||
def test_fulfilled_entitlement(self, mock_course_key, mock_course_overview, mock_course_runs):
|
||||
"""
|
||||
When a learner has a fulfilled entitlement, their course dashboard should have:
|
||||
- exactly one course item, meaning it:
|
||||
- has an entitlement card
|
||||
- does NOT have a course card referencing the selected session
|
||||
- an unhidden Change Session button
|
||||
"""
|
||||
mocked_course_overview = CourseOverviewFactory(
|
||||
start=self.TOMORROW, self_paced=True, enrollment_end=self.TOMORROW
|
||||
)
|
||||
mock_course_overview.return_value = mocked_course_overview
|
||||
mock_course_key.return_value = mocked_course_overview.id
|
||||
course_enrollment = CourseEnrollmentFactory(user=self.user, course_id=unicode(mocked_course_overview.id))
|
||||
mock_course_runs.return_value = [
|
||||
{
|
||||
'key': mocked_course_overview.id,
|
||||
'enrollment_end': mocked_course_overview.enrollment_end,
|
||||
'pacing_type': 'self_paced',
|
||||
'type': 'verified'
|
||||
}
|
||||
]
|
||||
CourseEntitlementFactory(user=self.user, enrollment_course_run=course_enrollment)
|
||||
response = self.client.get(self.path)
|
||||
self.assertEqual(response.content.count('<li class="course-item">'), 1)
|
||||
self.assertIn('<button class="change-session btn-link "', response.content)
|
||||
|
||||
@@ -58,6 +58,7 @@ from bulk_email.models import BulkEmailFlag, Optout # pylint: disable=import-er
|
||||
from certificates.api import get_certificate_url, has_html_certificates_enabled # pylint: disable=import-error
|
||||
from certificates.models import ( # pylint: disable=import-error
|
||||
CertificateStatuses,
|
||||
GeneratedCertificate,
|
||||
certificate_status_for_student
|
||||
)
|
||||
from course_modes.models import CourseMode
|
||||
@@ -65,6 +66,7 @@ from courseware.access import has_access
|
||||
from courseware.courses import get_courses, sort_by_announcement, sort_by_start_date # pylint: disable=import-error
|
||||
from django_comment_common.models import assign_role
|
||||
from edxmako.shortcuts import render_to_response, render_to_string
|
||||
from entitlements.models import CourseEntitlement
|
||||
from eventtracking import tracker
|
||||
from lms.djangoapps.commerce.utils import EcommerceService # pylint: disable=import-error
|
||||
from lms.djangoapps.grades.course_grade_factory import CourseGradeFactory
|
||||
@@ -72,7 +74,7 @@ from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification
|
||||
# Note that this lives in LMS, so this dependency should be refactored.
|
||||
from notification_prefs.views import enable_notifications
|
||||
from openedx.core.djangoapps import monitoring_utils
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs_with_type
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs_with_type, get_course_runs_for_course
|
||||
from openedx.core.djangoapps.certificates.api import certificates_viewable_for_course
|
||||
from openedx.core.djangoapps.credit.email_utils import get_credit_provider_display_names, make_providers_strings
|
||||
from openedx.core.djangoapps.embargo import api as embargo_api
|
||||
@@ -686,15 +688,22 @@ def dashboard(request):
|
||||
'ACTIVATION_EMAIL_SUPPORT_LINK', settings.ACTIVATION_EMAIL_SUPPORT_LINK
|
||||
) or settings.SUPPORT_SITE_LINK
|
||||
|
||||
# get the org whitelist or the org blacklist for the current site
|
||||
# Get the org whitelist or the org blacklist for the current site
|
||||
site_org_whitelist, site_org_blacklist = get_org_black_and_whitelist_for_site(user)
|
||||
course_enrollments = list(get_course_enrollments(user, site_org_whitelist, site_org_blacklist))
|
||||
|
||||
# Get the entitlements for the user and a mapping to all available sessions for that entitlement
|
||||
course_entitlements = list(CourseEntitlement.objects.filter(user=user).select_related('enrollment_course_run'))
|
||||
course_entitlement_available_sessions = {
|
||||
str(entitlement.uuid): get_course_runs_for_course(str(entitlement.course_uuid))
|
||||
for entitlement in course_entitlements
|
||||
}
|
||||
|
||||
# Record how many courses there are so that we can get a better
|
||||
# understanding of usage patterns on prod.
|
||||
monitoring_utils.accumulate('num_courses', len(course_enrollments))
|
||||
|
||||
# sort the enrollment pairs by the enrollment date
|
||||
# Sort the enrollment pairs by the enrollment date
|
||||
course_enrollments.sort(key=lambda x: x.created, reverse=True)
|
||||
|
||||
# Retrieve the course modes for each course
|
||||
@@ -863,6 +872,10 @@ def dashboard(request):
|
||||
valid_verification_statuses = ['approved', 'must_reverify', 'pending', 'expired']
|
||||
display_sidebar_on_dashboard = len(order_history_list) or verification_status in valid_verification_statuses
|
||||
|
||||
# Filter out any course enrollment course cards that are associated with fulfilled entitlements
|
||||
for entitlement in [e for e in course_entitlements if e.enrollment_course_run is not None]:
|
||||
course_enrollments = [enr for enr in course_enrollments if entitlement.enrollment_course_run.course_id != enr.course_id] # pylint: disable=line-too-long
|
||||
|
||||
context = {
|
||||
'enterprise_message': enterprise_message,
|
||||
'consent_required_courses': consent_required_courses,
|
||||
@@ -871,6 +884,8 @@ def dashboard(request):
|
||||
'redirect_message': redirect_message,
|
||||
'account_activation_messages': account_activation_messages,
|
||||
'course_enrollments': course_enrollments,
|
||||
'course_entitlements': course_entitlements,
|
||||
'course_entitlement_available_sessions': course_entitlement_available_sessions,
|
||||
'course_optouts': course_optouts,
|
||||
'banner_account_activation_message': banner_account_activation_message,
|
||||
'sidebar_account_activation_message': sidebar_account_activation_message,
|
||||
|
||||
@@ -10,10 +10,10 @@ var edx = edx || {};
|
||||
edx.dashboard.dropdown.toggleCourseActionsDropdownMenu = function(event) {
|
||||
// define variables for code legibility
|
||||
var dashboardIndex = $(event.currentTarget).data().dashboardIndex,
|
||||
dropdown = $('#actions-dropdown-' + dashboardIndex),
|
||||
$dropdown = $('#actions-dropdown-' + dashboardIndex),
|
||||
dropdownButton = $('#actions-dropdown-link-' + dashboardIndex),
|
||||
ariaExpandedState = (dropdownButton.attr('aria-expanded') === 'true'),
|
||||
menuItems = dropdown.find('a');
|
||||
menuItems = $dropdown.find('a');
|
||||
|
||||
var catchKeyPress = function(object, event) {
|
||||
// get currently focused item
|
||||
@@ -57,12 +57,12 @@ var edx = edx || {};
|
||||
};
|
||||
|
||||
// Toggle the visibility control for the selected element and set the focus
|
||||
dropdown.toggleClass('is-visible');
|
||||
if (dropdown.hasClass('is-visible')) {
|
||||
dropdown.attr('tabindex', -1);
|
||||
dropdown.focus();
|
||||
$dropdown.toggleClass('is-visible');
|
||||
if ($dropdown.hasClass('is-visible')) {
|
||||
$dropdown.attr('tabindex', -1);
|
||||
$dropdown.focus();
|
||||
} else {
|
||||
dropdown.removeAttr('tabindex');
|
||||
$dropdown.removeAttr('tabindex');
|
||||
dropdownButton.focus();
|
||||
}
|
||||
|
||||
@@ -71,8 +71,8 @@ var edx = edx || {};
|
||||
|
||||
// catch keypresses when inside dropdownMenu (we want to catch spacebar;
|
||||
// escape; up arrow or shift+tab; and down arrow or tab)
|
||||
dropdown.on('keydown', function(event) {
|
||||
catchKeyPress($(this), event);
|
||||
$dropdown.on('keydown', function(e) {
|
||||
catchKeyPress($(this), e);
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'js/learner_dashboard/views/course_entitlement_view'
|
||||
],
|
||||
function(EntitlementView) {
|
||||
return function(options) {
|
||||
return new EntitlementView(options);
|
||||
};
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -6,10 +6,12 @@
|
||||
define([
|
||||
'backbone',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'jquery',
|
||||
'edx-ui-toolkit/js/utils/date-utils'
|
||||
'edx-ui-toolkit/js/utils/date-utils',
|
||||
'edx-ui-toolkit/js/utils/string-utils'
|
||||
],
|
||||
function(Backbone, _, $, DateUtils) {
|
||||
function(Backbone, _, gettext, $, DateUtils, StringUtils) {
|
||||
return Backbone.Model.extend({
|
||||
initialize: function(data) {
|
||||
if (data) {
|
||||
@@ -140,7 +142,7 @@
|
||||
|
||||
formatDateString: function(run) {
|
||||
var pacingType = run.pacing_type,
|
||||
dateString = '',
|
||||
dateString,
|
||||
start = this.get('start_date') || run.start_date,
|
||||
end = this.get('end_date') || run.end_date,
|
||||
now = new Date(),
|
||||
@@ -148,21 +150,24 @@
|
||||
endDate = new Date(end);
|
||||
|
||||
if (pacingType === 'self_paced') {
|
||||
dateString = 'Self-paced';
|
||||
if (start && startDate > now) {
|
||||
dateString += ' - Starts ' + start;
|
||||
if (start) {
|
||||
dateString = startDate > now ?
|
||||
StringUtils.interpolate(gettext('(Self-paced) Starts {start}'), {start: start}) :
|
||||
StringUtils.interpolate(gettext('(Self-paced) Started {start}'), {start: start});
|
||||
} else if (end && endDate > now) {
|
||||
dateString += ' - Ends ' + end;
|
||||
dateString = StringUtils.interpolate(gettext('(Self-paced) Ends {end}'), {end: end});
|
||||
} else if (end && endDate < now) {
|
||||
dateString += ' - Ended ' + end;
|
||||
dateString = StringUtils.interpolate(gettext('(Self-paced) Ended {end}'), {end: end});
|
||||
}
|
||||
} else {
|
||||
if (start && end) {
|
||||
dateString = start + ' - ' + end;
|
||||
} else if (start) {
|
||||
dateString = 'Starts ' + start;
|
||||
dateString = startDate > now ?
|
||||
StringUtils.interpolate(gettext('Starts {start}'), {start: start}) :
|
||||
StringUtils.interpolate(gettext('Started {start}'), {start: start});
|
||||
} else if (end) {
|
||||
dateString = 'Ends ' + end;
|
||||
dateString = StringUtils.interpolate(gettext('Ends {end}'), {end: end});
|
||||
}
|
||||
}
|
||||
return dateString;
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Store data for the current
|
||||
*/
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define([
|
||||
'backbone'
|
||||
],
|
||||
function(Backbone) {
|
||||
return Backbone.Model.extend({
|
||||
defaults: {
|
||||
availableSessions: [],
|
||||
entitlementUUID: '',
|
||||
currentSessionId: '',
|
||||
userId: '',
|
||||
courseName: ''
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
399
lms/static/js/learner_dashboard/views/course_entitlement_view.js
Normal file
399
lms/static/js/learner_dashboard/views/course_entitlement_view.js
Normal file
@@ -0,0 +1,399 @@
|
||||
// This is required for karma testing due to a known issue in Bootstrap-v4: https://github.com/twbs/bootstrap/pull/22888
|
||||
// The issue is that bootstrap tries to access Popper's global Popper object which is not initialized on loading
|
||||
// from the karma configuration. The next version of bootstrap (>v4.2) will solve this issue.
|
||||
// Once this is resolved, we should import bootstrap through require-config.js and main.js (for jasmine testing)
|
||||
var defineFn = require || RequireJS.require; // eslint-disable-line global-require
|
||||
var Popper = defineFn(['common/js/vendor/popper']);
|
||||
defineFn(['common/js/vendor/bootstrap']);
|
||||
|
||||
(function(define) {
|
||||
'use strict';
|
||||
|
||||
define(['backbone',
|
||||
'jquery',
|
||||
'underscore',
|
||||
'gettext',
|
||||
'moment',
|
||||
'edx-ui-toolkit/js/utils/html-utils',
|
||||
'js/learner_dashboard/models/course_entitlement_model',
|
||||
'js/learner_dashboard/models/course_card_model',
|
||||
'text!../../../templates/learner_dashboard/course_entitlement.underscore',
|
||||
'text!../../../templates/learner_dashboard/verification_popover.underscore'
|
||||
],
|
||||
function(
|
||||
Backbone,
|
||||
$,
|
||||
_,
|
||||
gettext,
|
||||
moment,
|
||||
HtmlUtils,
|
||||
EntitlementModel,
|
||||
CourseCardModel,
|
||||
pageTpl,
|
||||
verificationPopoverTpl
|
||||
) {
|
||||
return Backbone.View.extend({
|
||||
tpl: HtmlUtils.template(pageTpl),
|
||||
verificationTpl: HtmlUtils.template(verificationPopoverTpl),
|
||||
|
||||
events: {
|
||||
'change .session-select': 'updateEnrollBtn',
|
||||
'click .enroll-btn': 'handleEnrollChange',
|
||||
'keydown .final-confirmation-btn': 'handleVerificationPopoverA11y',
|
||||
'click .popover-dismiss': 'hideDialog'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
// Set up models and reload view on change
|
||||
this.courseCardModel = new CourseCardModel();
|
||||
this.entitlementModel = new EntitlementModel({
|
||||
availableSessions: this.formatDates(JSON.parse(options.availableSessions)),
|
||||
entitlementUUID: options.entitlementUUID,
|
||||
currentSessionId: options.currentSessionId,
|
||||
userId: options.userId,
|
||||
courseName: options.courseName
|
||||
});
|
||||
this.listenTo(this.entitlementModel, 'change', this.render);
|
||||
|
||||
// Grab URLs that handle changing of enrollment and entering a newly selected session.
|
||||
this.enrollUrl = options.enrollUrl;
|
||||
this.courseHomeUrl = options.courseHomeUrl;
|
||||
|
||||
// Grab elements from the parent card that work with this view and bind associated events
|
||||
this.$triggerOpenBtn = $(options.triggerOpenBtn); // Opens/closes session selection view
|
||||
this.$dateDisplayField = $(options.dateDisplayField); // Displays current session dates
|
||||
this.$enterCourseBtn = $(options.enterCourseBtn); // Button link to course home page
|
||||
this.$courseCardMessages = $(options.courseCardMessages); // Additional session messages
|
||||
this.$courseTitleLink = $(options.courseTitleLink); // Title link to course home page
|
||||
this.$courseImageLink = $(options.courseImageLink); // Image link to course home page
|
||||
this.$triggerOpenBtn.on('click', this.toggleSessionSelectionPanel.bind(this));
|
||||
|
||||
this.render(options);
|
||||
this.postRender();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
HtmlUtils.setHtml(this.$el, this.tpl(this.entitlementModel.toJSON()));
|
||||
this.delegateEvents();
|
||||
this.updateEnrollBtn();
|
||||
return this;
|
||||
},
|
||||
|
||||
postRender: function() {
|
||||
// Close popover on click-away
|
||||
$(document).on('click', function(e) {
|
||||
if (!($(e.target).closest('.enroll-btn-initial, .popover').length)) {
|
||||
this.hideDialog(this.$('.enroll-btn-initial'));
|
||||
}
|
||||
}.bind(this));
|
||||
|
||||
this.$('.enroll-btn-initial').click(function(e) {
|
||||
this.showDialog($(e.target));
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
handleEnrollChange: function() {
|
||||
/*
|
||||
Handles enrolling in a course, unenrolling in a session and changing session.
|
||||
The new session id is stored as a data attribute on the option in the session-select element.
|
||||
*/
|
||||
var isLeavingSession;
|
||||
|
||||
// Do not allow for enrollment when button is disabled
|
||||
if (this.$('.enroll-btn-initial').hasClass('disabled')) return;
|
||||
|
||||
// Grab the id for the desired session, an leave session event will return null
|
||||
this.currentSessionSelection = this.$('.session-select')
|
||||
.find('option:selected').data('session_id');
|
||||
isLeavingSession = !this.currentSessionSelection;
|
||||
|
||||
// Display the indicator icon
|
||||
HtmlUtils.setHtml(this.$dateDisplayField,
|
||||
HtmlUtils.HTML('<span class="fa fa-spinner fa-spin" aria-hidden="true"></span>')
|
||||
);
|
||||
|
||||
$.ajax({
|
||||
type: isLeavingSession ? 'DELETE' : 'POST',
|
||||
url: this.enrollUrl,
|
||||
contentType: 'application/json',
|
||||
dataType: 'json',
|
||||
data: JSON.stringify({
|
||||
course_run_id: this.currentSessionSelection
|
||||
}),
|
||||
statusCode: {
|
||||
201: _.bind(this.enrollSuccess, this),
|
||||
204: _.bind(this.unenrollSuccess, this)
|
||||
},
|
||||
error: _.bind(this.enrollError, this)
|
||||
});
|
||||
},
|
||||
|
||||
enrollSuccess: function(data) {
|
||||
/*
|
||||
Update external elements on the course card to represent the now available course session.
|
||||
|
||||
1) Show the change session toggle button.
|
||||
2) Add the new session's dates to the date field on the main course card.
|
||||
3) Hide the 'View Course' button to the course card.
|
||||
*/
|
||||
var successIconEl = '<span class="fa fa-check" aria-hidden="true"></span>';
|
||||
|
||||
// Update the model with the new session Id;
|
||||
this.entitlementModel.set({currentSessionId: this.currentSessionSelection});
|
||||
|
||||
// Allow user to change session
|
||||
this.$triggerOpenBtn.removeClass('hidden');
|
||||
|
||||
// Display a success indicator
|
||||
HtmlUtils.setHtml(this.$dateDisplayField,
|
||||
HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML(successIconEl),
|
||||
this.getAvailableSessionWithId(data.course_run_id).session_dates
|
||||
)
|
||||
);
|
||||
|
||||
// Ensure the view course button links to new session home page and place focus there
|
||||
this.$enterCourseBtn
|
||||
.attr('href', this.formatCourseHomeUrl(data.course_run_id))
|
||||
.removeClass('hidden')
|
||||
.focus();
|
||||
this.toggleSessionSelectionPanel();
|
||||
},
|
||||
|
||||
unenrollSuccess: function() {
|
||||
/*
|
||||
Update external elements on the course card to represent the unenrolled state.
|
||||
|
||||
1) Hide the change session button and the date field.
|
||||
2) Hide the 'View Course' button.
|
||||
3) Remove the messages associated with the enrolled state.
|
||||
4) Remove the link from the course card image and title.
|
||||
*/
|
||||
|
||||
// Update the model with the new session Id;
|
||||
this.entitlementModel.set({currentSessionId: this.currentSessionSelection});
|
||||
|
||||
// Reset the card contents to the unenrolled state
|
||||
this.$triggerOpenBtn.addClass('hidden');
|
||||
this.$enterCourseBtn.addClass('hidden');
|
||||
this.$courseCardMessages.remove();
|
||||
this.$('.enroll-btn-initial').focus();
|
||||
HtmlUtils.setHtml(
|
||||
this.$dateDisplayField,
|
||||
HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<span class="icon fa fa-warning" aria-hidden="true"></span>'),
|
||||
HtmlUtils.HTML(gettext('You must select a session to access the course.'))
|
||||
)
|
||||
);
|
||||
|
||||
// Remove links to previously enrolled sessions
|
||||
this.$courseImageLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion
|
||||
HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<div class="'),
|
||||
this.$courseImageLink.attr('class'),
|
||||
HtmlUtils.HTML('" tabindex="-1">'),
|
||||
HtmlUtils.HTML(this.$courseImageLink.html()),
|
||||
HtmlUtils.HTML('</div>')
|
||||
).text
|
||||
);
|
||||
this.$courseTitleLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion
|
||||
HtmlUtils.joinHtml(
|
||||
HtmlUtils.HTML('<span>'),
|
||||
this.$courseTitleLink.text(),
|
||||
HtmlUtils.HTML('</span>')
|
||||
).text
|
||||
);
|
||||
},
|
||||
|
||||
enrollError: function() {
|
||||
var errorMsgEl = HtmlUtils.HTML(
|
||||
gettext('There was an error. Please reload the page and try again.')
|
||||
).text;
|
||||
this.$dateDisplayField
|
||||
.find('.fa.fa-spin')
|
||||
.removeClass('fa-spin fa-spinner')
|
||||
.addClass('fa-close');
|
||||
this.$dateDisplayField.append(errorMsgEl);
|
||||
this.hideDialog(this.$('.enroll-btn-initial'));
|
||||
},
|
||||
|
||||
updateEnrollBtn: function() {
|
||||
/*
|
||||
This function is invoked on load, on opening the view and on changing the option on the session
|
||||
selection dropdown. It plays three roles:
|
||||
1) Enables and disables enroll button
|
||||
2) Changes text to describe the action taken
|
||||
3) Formats the confirmation popover to allow for two step authentication
|
||||
*/
|
||||
var enrollText,
|
||||
currentSessionId = this.entitlementModel.get('currentSessionId'),
|
||||
newSessionId = this.$('.session-select').find('option:selected').data('session_id'),
|
||||
enrollBtnInitial = this.$('.enroll-btn-initial');
|
||||
|
||||
// Disable the button if the user is already enrolled in that session.
|
||||
if (currentSessionId === newSessionId) {
|
||||
enrollBtnInitial.addClass('disabled');
|
||||
this.removeDialog(enrollBtnInitial);
|
||||
return;
|
||||
}
|
||||
enrollBtnInitial.removeClass('disabled');
|
||||
|
||||
// Update button text specifying if the user is initially enrolling, changing or leaving a session.
|
||||
if (newSessionId) {
|
||||
enrollText = currentSessionId ? gettext('Change Session') : gettext('Select Session');
|
||||
} else {
|
||||
enrollText = gettext('Leave Current Session');
|
||||
}
|
||||
enrollBtnInitial.text(enrollText);
|
||||
this.removeDialog(enrollBtnInitial);
|
||||
this.initializeVerificationDialog(enrollBtnInitial);
|
||||
},
|
||||
|
||||
toggleSessionSelectionPanel: function() {
|
||||
/*
|
||||
Opens and closes the session selection panel.
|
||||
*/
|
||||
this.$el.toggleClass('hidden');
|
||||
if (!this.$el.hasClass('hidden')) {
|
||||
// Set focus to the session selection for a11y purposes
|
||||
this.$('.session-select').focus();
|
||||
this.hideDialog(this.$('.enroll-btn-initial'));
|
||||
}
|
||||
this.updateEnrollBtn();
|
||||
},
|
||||
|
||||
initializeVerificationDialog: function(invokingElement) {
|
||||
/*
|
||||
Instantiates an instance of the Bootstrap v4 dialog modal and attaches it to the passed in element.
|
||||
|
||||
This dialog acts as the second step in verifying the user's action to select, change or leave an
|
||||
available course session.
|
||||
*/
|
||||
var confirmationMsgTitle,
|
||||
confirmationMsgBody,
|
||||
popoverDialogHtml,
|
||||
currentSessionId = this.entitlementModel.get('currentSessionId'),
|
||||
newSessionId = this.$('.session-select').find('option:selected').data('session_id');
|
||||
|
||||
// Update the button popover text to enable two step authentication.
|
||||
if (newSessionId) {
|
||||
confirmationMsgTitle = !currentSessionId ?
|
||||
gettext('Are you sure you want to select this session?') :
|
||||
gettext('Are you sure you want to change to a different session?');
|
||||
confirmationMsgBody = !currentSessionId ? '' :
|
||||
gettext('Any course progress or grades from your current session will be lost.');
|
||||
} else {
|
||||
confirmationMsgTitle = gettext('Are you sure that you want to leave this session?');
|
||||
confirmationMsgBody = gettext('Any course progress or grades from your current session will be lost.'); // eslint-disable-line max-len
|
||||
}
|
||||
|
||||
// Remove existing popover and re-initialize
|
||||
popoverDialogHtml = this.verificationTpl({
|
||||
confirmationMsgTitle: confirmationMsgTitle,
|
||||
confirmationMsgBody: confirmationMsgBody
|
||||
});
|
||||
|
||||
invokingElement.popover({
|
||||
placement: 'bottom',
|
||||
container: this.$el,
|
||||
html: true,
|
||||
trigger: 'click',
|
||||
content: popoverDialogHtml.text
|
||||
});
|
||||
},
|
||||
|
||||
removeDialog: function(invokingElement) {
|
||||
/* Removes the Bootstrap v4 dialog modal from the update session enrollment button. */
|
||||
invokingElement.popover('dispose');
|
||||
},
|
||||
|
||||
showDialog: function(invokingElement) {
|
||||
/* Given an element with an associated dialog modal, shows the modal. */
|
||||
invokingElement.popover('show');
|
||||
this.$('.final-confirmation-btn:first').focus();
|
||||
},
|
||||
|
||||
hideDialog: function(el, returnFocus) {
|
||||
/* Hides the modal without removing it from the DOM. */
|
||||
var $el = el instanceof jQuery ? el : this.$('.enroll-btn-initial');
|
||||
$el.popover('hide');
|
||||
if (returnFocus) {
|
||||
$el.focus();
|
||||
}
|
||||
},
|
||||
|
||||
handleVerificationPopoverA11y: function(e) {
|
||||
/* Ensure that the second step verification popover is treated as an a11y compliant dialog */
|
||||
var $nextButton,
|
||||
$verificationOption = $(e.target),
|
||||
openButton = $(e.target).closest('.course-entitlement-selection-container')
|
||||
.find('.enroll-btn-initial');
|
||||
if (e.key === 'Tab') {
|
||||
e.preventDefault();
|
||||
$nextButton = $verificationOption.is(':first-child') ?
|
||||
$verificationOption.next('.final-confirmation-btn') :
|
||||
$verificationOption.prev('.final-confirmation-btn');
|
||||
$nextButton.focus();
|
||||
} else if (e.key === 'Escape') {
|
||||
this.hideDialog(openButton);
|
||||
openButton.focus();
|
||||
}
|
||||
},
|
||||
|
||||
formatCourseHomeUrl: function(sessionKey) {
|
||||
/*
|
||||
Takes the base course home URL and updates it with the new session id, leveraging the
|
||||
the fact that all course keys contain a '+' symbol.
|
||||
*/
|
||||
var oldSessionKey = this.courseHomeUrl.split('/')
|
||||
.filter(
|
||||
function(urlParam) {
|
||||
return urlParam.indexOf('+') > 0;
|
||||
}
|
||||
)[0];
|
||||
return this.courseHomeUrl.replace(oldSessionKey, sessionKey);
|
||||
},
|
||||
|
||||
formatDates: function(sessionData) {
|
||||
/*
|
||||
Takes a data object containing the upcoming available sessions for an entitlement and returns
|
||||
the object with a session_dates attribute representing a formatted date string that highlights
|
||||
the start and end dates of the particular session.
|
||||
*/
|
||||
var formattedSessionData = sessionData,
|
||||
startDate,
|
||||
endDate,
|
||||
dateFormat;
|
||||
// Set the date format string to the user's selected language
|
||||
moment.locale(document.documentElement.lang);
|
||||
dateFormat = moment.localeData().longDateFormat('L').indexOf('DD') >
|
||||
moment.localeData().longDateFormat('L').indexOf('MM') ? 'MMMM D, YYYY' : 'D MMMM, YYYY';
|
||||
|
||||
return _.map(formattedSessionData, function(session) {
|
||||
var formattedSession = session;
|
||||
startDate = this.formatDate(formattedSession.session_start, dateFormat);
|
||||
endDate = this.formatDate(formattedSession.session_end, dateFormat);
|
||||
formattedSession.enrollment_end = this.formatDate(formattedSession.enrollment_end, dateFormat);
|
||||
formattedSession.session_dates = this.courseCardModel.formatDateString({
|
||||
start_date: session.session_start_advertised || startDate,
|
||||
end_date: session.session_start_advertised ? null : endDate,
|
||||
pacing_type: formattedSession.pacing_type
|
||||
});
|
||||
return formattedSession;
|
||||
}, this);
|
||||
},
|
||||
|
||||
formatDate: function(date, dateFormat) {
|
||||
return date ? moment((new Date(date))).format(dateFormat) : null;
|
||||
},
|
||||
|
||||
getAvailableSessionWithId: function(sessionId) {
|
||||
/* Returns an available session given a sessionId */
|
||||
return this.entitlementModel.get('availableSessions').find(function(session) {
|
||||
return session.session_id === sessionId;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -0,0 +1,158 @@
|
||||
define([
|
||||
'backbone',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'js/learner_dashboard/models/course_entitlement_model',
|
||||
'js/learner_dashboard/views/course_entitlement_view'
|
||||
], function(Backbone, _, $, CourseEntitlementModel, CourseEntitlementView) {
|
||||
'use strict';
|
||||
|
||||
describe('Course Entitlement View', function() {
|
||||
var view = null,
|
||||
setupView,
|
||||
selectOptions,
|
||||
entitlementAvailableSessions,
|
||||
initialSessionId,
|
||||
entitlementUUID = 'a9aiuw76a4ijs43u18',
|
||||
testSessionIds = ['test_session_id_1', 'test_session_id_2'];
|
||||
|
||||
setupView = function(isAlreadyEnrolled) {
|
||||
setFixtures('<div class="course-entitlement-selection-container"></div>');
|
||||
|
||||
initialSessionId = isAlreadyEnrolled ? testSessionIds[0] : '';
|
||||
entitlementAvailableSessions = [{
|
||||
enrollment_end: null,
|
||||
session_start: '2013-02-05T05:00:00+00:00',
|
||||
pacing_type: 'instructor_paced',
|
||||
session_id: testSessionIds[0],
|
||||
session_end: null
|
||||
}, {
|
||||
enrollment_end: '2017-12-22T03:30:00Z',
|
||||
session_start: '2018-01-03T13:00:00+00:00',
|
||||
pacing_type: 'self_paced',
|
||||
session_id: testSessionIds[1],
|
||||
session_end: '2018-03-09T21:30:00+00:00'
|
||||
}];
|
||||
|
||||
view = new CourseEntitlementView({
|
||||
el: '.course-entitlement-selection-container',
|
||||
triggerOpenBtn: '#course-card-0 .change-session',
|
||||
courseCardMessages: '#course-card-0 .messages-list > .message',
|
||||
courseTitleLink: '#course-card-0 .course-title a',
|
||||
courseImageLink: '#course-card-0 .wrapper-course-image > a',
|
||||
dateDisplayField: '#course-card-0 .info-date-block',
|
||||
enterCourseBtn: '#course-card-0 .enter-course',
|
||||
availableSessions: JSON.stringify(entitlementAvailableSessions),
|
||||
entitlementUUID: entitlementUUID,
|
||||
currentSessionId: initialSessionId,
|
||||
userId: '1',
|
||||
enrollUrl: '/api/enrollment/v1/enrollment',
|
||||
courseHomeUrl: '/courses/course-v1:edX+DemoX+Demo_Course/course/'
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(function() {
|
||||
if (view) view.remove();
|
||||
});
|
||||
|
||||
describe('Initialization of view', function() {
|
||||
it('Should create a entitlement view element', function() {
|
||||
setupView(false);
|
||||
expect(view).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Available Sessions Select - Unfulfilled Entitlement', function() {
|
||||
beforeEach(function() {
|
||||
setupView(false);
|
||||
selectOptions = view.$('.session-select').find('option');
|
||||
});
|
||||
|
||||
it('Select session dropdown should show all available course runs and a coming soon option.', function() {
|
||||
expect(selectOptions.length).toEqual(entitlementAvailableSessions.length + 1);
|
||||
});
|
||||
|
||||
it('Self paced courses should have visual indication in the selection option.', function() {
|
||||
var selfPacedOptionIndex = _.findIndex(entitlementAvailableSessions, function(session) {
|
||||
return session.pacing_type === 'self_paced';
|
||||
});
|
||||
var selfPacedOption = selectOptions[selfPacedOptionIndex];
|
||||
expect(selfPacedOption && selfPacedOption.text.includes('(Self-paced)')).toBe(true);
|
||||
});
|
||||
|
||||
it('Courses with an an enroll by date should indicate so on the selection option.', function() {
|
||||
var enrollEndSetOptionIndex = _.findIndex(entitlementAvailableSessions, function(session) {
|
||||
return session.enrollment_end !== null;
|
||||
});
|
||||
var enrollEndSetOption = selectOptions[enrollEndSetOptionIndex];
|
||||
expect(enrollEndSetOption && enrollEndSetOption.text.includes('Open until')).toBe(true);
|
||||
});
|
||||
|
||||
it('Title element should correctly indicate the expected behavior.', function() {
|
||||
expect(view.$('.action-header').text().includes(
|
||||
'To access the course, select a session.'
|
||||
)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Available Sessions Select - Fulfilled Entitlement', function() {
|
||||
beforeEach(function() {
|
||||
setupView(true);
|
||||
selectOptions = view.$('.session-select').find('option');
|
||||
});
|
||||
|
||||
it('Select session dropdown should show available course runs, coming soon and leave options.', function() {
|
||||
expect(selectOptions.length).toEqual(entitlementAvailableSessions.length + 2);
|
||||
});
|
||||
|
||||
it('Select session dropdown should allow user to leave the current session.', function() {
|
||||
var leaveSessionOption = selectOptions[selectOptions.length - 1];
|
||||
expect(leaveSessionOption.text.includes('Leave the current session and decide later')).toBe(true);
|
||||
});
|
||||
|
||||
it('Currently selected session should be specified in the dropdown options.', function() {
|
||||
var selectedSessionIndex = _.findIndex(entitlementAvailableSessions, function(session) {
|
||||
return initialSessionId === session.session_id;
|
||||
});
|
||||
expect(selectOptions[selectedSessionIndex].text.includes('Currently Selected')).toBe(true);
|
||||
});
|
||||
|
||||
it('Title element should correctly indicate the expected behavior.', function() {
|
||||
expect(view.$('.action-header').text().includes(
|
||||
'Change to a different session or leave the current session.'
|
||||
)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Select Session Action Button and popover behavior - Unfulfilled Entitlement', function() {
|
||||
beforeEach(function() {
|
||||
setupView(false);
|
||||
});
|
||||
|
||||
it('Change session button should have the correct text.', function() {
|
||||
expect(view.$('.enroll-btn-initial').text() === 'Select Session').toBe(true);
|
||||
});
|
||||
|
||||
it('Select session button should show popover when clicked.', function() {
|
||||
view.$('.enroll-btn-initial').click();
|
||||
expect(view.$('.verification-modal').length > 0).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Change Session Action Button and popover behavior - Fulfilled Entitlement', function() {
|
||||
beforeEach(function() {
|
||||
setupView(true);
|
||||
selectOptions = view.$('.session-select').find('option');
|
||||
});
|
||||
|
||||
it('Change session button should show correct text.', function() {
|
||||
expect(view.$('.enroll-btn-initial').text().trim() === 'Change Session').toBe(true);
|
||||
});
|
||||
|
||||
it('Switch session button should be disabled when on the currently enrolled session.', function() {
|
||||
expect(view.$('.enroll-btn-initial')).toHaveClass('disabled');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -100,6 +100,8 @@
|
||||
'string_utils': 'js/src/string_utils',
|
||||
'utility': 'js/src/utility',
|
||||
'draggabilly': 'js/vendor/draggabilly',
|
||||
'popper': 'common/js/vendor/popper',
|
||||
'bootstrap': 'common/js/vendor/bootstrap',
|
||||
|
||||
// Files needed by OVA
|
||||
'annotator': 'js/vendor/ova/annotator-full',
|
||||
@@ -206,6 +208,13 @@
|
||||
'grouping-annotator': {
|
||||
deps: ['annotator']
|
||||
},
|
||||
'popper': {
|
||||
exports: 'Popper'
|
||||
},
|
||||
'bootstrap': {
|
||||
deps: ['jquery', 'popper'],
|
||||
exports: 'bootstrap'
|
||||
},
|
||||
'ova': {
|
||||
exports: 'ova',
|
||||
deps: [
|
||||
|
||||
@@ -762,6 +762,7 @@
|
||||
'js/spec/learner_dashboard/unenroll_view_spec.js',
|
||||
'js/spec/learner_dashboard/course_card_view_spec.js',
|
||||
'js/spec/learner_dashboard/course_enroll_view_spec.js',
|
||||
'js/spec/learner_dashboard/course_entitlement_view_spec.js',
|
||||
'js/spec/markdown_editor_spec.js',
|
||||
'js/spec/dateutil_factory_spec.js',
|
||||
'js/spec/navigation_spec.js',
|
||||
|
||||
@@ -163,11 +163,31 @@
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.info-date-block {
|
||||
@extend %t-title7;
|
||||
|
||||
color: $gray; // WCAG 2.0 AA compliant
|
||||
.info-date-block-container {
|
||||
display: block;
|
||||
|
||||
.info-date-block{
|
||||
@extend %t-title7;
|
||||
|
||||
color: $gray; // WCAG 2.0 AA compliant
|
||||
|
||||
.fa-close {
|
||||
color: theme-color("error");
|
||||
}
|
||||
|
||||
.fa-check {
|
||||
color: theme-color("success");
|
||||
}
|
||||
}
|
||||
|
||||
.change-session {
|
||||
@extend %t-title7;
|
||||
@include margin(0, 0, 0, $baseline/4);
|
||||
|
||||
padding: 0;
|
||||
border: none;
|
||||
letter-spacing: normal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,18 +653,20 @@
|
||||
.message-copy .copy {
|
||||
@extend %t-copy-sub1;
|
||||
|
||||
margin: 2px 0 0 0;
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
|
||||
// CASE: expandable
|
||||
&.is-expandable {
|
||||
.wrapper-tip {
|
||||
.message-title, .message-copy {
|
||||
.message-title,
|
||||
.message-copy {
|
||||
margin-bottom: 0;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.message-title .value, .message-copy {
|
||||
.message-title .value,
|
||||
.message-copy {
|
||||
@include transition(color $tmg-f2 ease-in-out 0s);
|
||||
}
|
||||
|
||||
@@ -652,7 +674,9 @@
|
||||
&:hover {
|
||||
cursor: pointer;
|
||||
|
||||
.message-title .value, .message-copy, .ui-toggle-expansion {
|
||||
.message-title .value,
|
||||
.message-copy,
|
||||
.ui-toggle-expansion {
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
@@ -789,7 +813,7 @@
|
||||
.action-view-consent {
|
||||
@extend %btn-pl-white-base;
|
||||
@include float(right);
|
||||
|
||||
|
||||
&.archived {
|
||||
@extend %btn-pl-default-base;
|
||||
}
|
||||
@@ -1071,6 +1095,89 @@
|
||||
@include padding($baseline/2, $baseline, $baseline/2, $baseline/2);
|
||||
}
|
||||
}
|
||||
|
||||
// Course Entitlement Session Selection
|
||||
.course-entitlement-selection-container {
|
||||
background-color: theme-color("inverse");
|
||||
|
||||
.action-header {
|
||||
padding-bottom: $baseline/4;
|
||||
font-weight: $font-weight-bold;
|
||||
color: theme-color("dark");
|
||||
}
|
||||
|
||||
.action-controls {
|
||||
display: flex;
|
||||
|
||||
.session-select {
|
||||
background-color: theme-color("inverse");
|
||||
height: $baseline*1.5;
|
||||
flex-grow: 5;
|
||||
margin-bottom: $baseline*0.4;
|
||||
}
|
||||
|
||||
.enroll-btn-initial {
|
||||
@include margin-left($baseline);
|
||||
|
||||
height: $baseline*1.5;
|
||||
flex-grow: 1;
|
||||
letter-spacing: 0;
|
||||
background: theme-color("inverse");
|
||||
border-color: theme-color("primary");
|
||||
color: theme-color("primary");
|
||||
text-shadow: none;
|
||||
font-size: $font-size-base;
|
||||
padding: 0;
|
||||
box-shadow: none;
|
||||
border-radius: $border-radius-sm;
|
||||
transition: all 0.4s ease-out;
|
||||
|
||||
&:hover {
|
||||
background: theme-color("primary");
|
||||
border-color: theme-color("primary");
|
||||
color: theme-color("inverse");
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(xs) {
|
||||
flex-direction: column;
|
||||
|
||||
.enroll-btn-initial {
|
||||
margin: $baseline/4 0 $baseline/4;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popover {
|
||||
.popover-title {
|
||||
margin-bottom: $baseline/2;
|
||||
}
|
||||
|
||||
.action-items {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: $baseline/2;
|
||||
|
||||
.final-confirmation-btn {
|
||||
box-shadow: none;
|
||||
border: 1px solid theme-color("dark");
|
||||
background: none;
|
||||
color: theme-color("dark");
|
||||
text-shadow: none;
|
||||
letter-spacing: 0;
|
||||
flex-grow: 1;
|
||||
margin: 0 $baseline/4;
|
||||
padding: $baseline/10 $baseline;
|
||||
font-size: $font-size-base;
|
||||
|
||||
&:hover {
|
||||
background: theme-color("primary");
|
||||
color: theme-color("inverse");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// CASE: empty dashboard
|
||||
@@ -1323,11 +1430,11 @@ p.course-block {
|
||||
padding: 6px 32px 7px;
|
||||
text-align: center;
|
||||
margin-top: 16px;
|
||||
opacity:0.5;
|
||||
background:#808080;
|
||||
border:0;
|
||||
opacity: 0.5;
|
||||
background: #808080;
|
||||
border: 0;
|
||||
color: theme-color("inverse");
|
||||
box-shadow:none;
|
||||
box-shadow: none;
|
||||
|
||||
&.archived {
|
||||
@include button(simple, $button-archive-color);
|
||||
@@ -1557,5 +1664,4 @@ a.fade-cover {
|
||||
color: theme-color("inverse");
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,11 +6,16 @@
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.template import RequestContext
|
||||
from entitlements.models import CourseEntitlement
|
||||
import third_party_auth
|
||||
from third_party_auth import pipeline
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
|
||||
from student.models import CourseEnrollment
|
||||
%>
|
||||
|
||||
<%
|
||||
@@ -108,7 +113,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<div class="my-courses" id="my-courses">
|
||||
<%include file="learner_dashboard/_dashboard_navigation_courses.html"/>
|
||||
|
||||
% if len(course_enrollments) > 0:
|
||||
% if len(course_entitlements + course_enrollments) > 0:
|
||||
<ul class="listing-courses">
|
||||
<%
|
||||
share_settings = configuration_helpers.get_value(
|
||||
@@ -116,20 +121,53 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
getattr(settings, 'SOCIAL_SHARING_SETTINGS', {})
|
||||
)
|
||||
%>
|
||||
% for dashboard_index, enrollment in enumerate(course_enrollments):
|
||||
<% show_courseware_link = (enrollment.course_id in show_courseware_links_for) %>
|
||||
<% cert_status = cert_statuses.get(enrollment.course_id) %>
|
||||
<% can_unenroll = (not cert_status) or cert_status.get('can_unenroll') %>
|
||||
<% credit_status = credit_statuses.get(enrollment.course_id) %>
|
||||
<% show_email_settings = (enrollment.course_id in show_email_settings_for) %>
|
||||
<% course_mode_info = all_course_modes.get(enrollment.course_id) %>
|
||||
<% is_paid_course = (enrollment.course_id in enrolled_courses_either_paid) %>
|
||||
<% is_course_blocked = (enrollment.course_id in block_courses) %>
|
||||
<% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %>
|
||||
<% course_requirements = courses_requirements_not_met.get(enrollment.course_id) %>
|
||||
<% related_programs = inverted_programs.get(unicode(enrollment.course_id)) %>
|
||||
<% show_consent_link = (enrollment.course_id in consent_required_courses) %>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name' />
|
||||
% for dashboard_index, enrollment in enumerate(course_entitlements + course_enrollments):
|
||||
<%
|
||||
# Check if the course run is an entitlement and if it has an associated session
|
||||
entitlement = enrollment if isinstance(enrollment, CourseEntitlement) else None
|
||||
entitlement_session = entitlement.enrollment_course_run if entitlement else None
|
||||
|
||||
is_fulfilled_entitlement = True if entitlement and entitlement_session else False
|
||||
is_unfulfilled_entitlement = True if entitlement and not entitlement_session else False
|
||||
|
||||
entitlement_available_sessions = []
|
||||
if entitlement:
|
||||
# Grab the available, enrollable sessions for a given entitlement and scrape them for relevant attributes
|
||||
entitlement_available_sessions = [{
|
||||
'session_id': course['key'],
|
||||
'enrollment_end': course['enrollment_end'],
|
||||
'pacing_type': course['pacing_type'],
|
||||
'session_start_advertised': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).advertised_start,
|
||||
'session_start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).start,
|
||||
'session_end': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).end,
|
||||
} for course in course_entitlement_available_sessions[str(entitlement.uuid)]]
|
||||
if is_fulfilled_entitlement:
|
||||
# If the user has a fulfilled entitlement, pass through the entitlements CourseEnrollment object
|
||||
enrollment = entitlement_session
|
||||
else:
|
||||
# If the user has an unfulfilled entitlement, pass through a bare CourseEnrollment object built off of the next available session
|
||||
upcoming_sessions = course_entitlement_available_sessions[str(entitlement.uuid)]
|
||||
next_session = upcoming_sessions[0] if upcoming_sessions else None
|
||||
if not next_session:
|
||||
continue
|
||||
enrollment = CourseEnrollment(user=user, course_id=next_session['key'], mode=next_session['type'])
|
||||
|
||||
session_id = enrollment.course_id
|
||||
show_courseware_link = (session_id in show_courseware_links_for)
|
||||
cert_status = cert_statuses.get(session_id)
|
||||
can_unenroll = (not cert_status) or cert_status.get('can_unenroll') if not unfulfilled_entitlement else False
|
||||
credit_status = credit_statuses.get(session_id)
|
||||
show_email_settings = (session_id in show_email_settings_for)
|
||||
course_mode_info = all_course_modes.get(session_id)
|
||||
is_paid_course = True if entitlement else (session_id in enrolled_courses_either_paid)
|
||||
is_course_blocked = (session_id in block_courses)
|
||||
course_verification_status = verification_status_by_course.get(session_id, {})
|
||||
course_requirements = courses_requirements_not_met.get(session_id)
|
||||
related_programs = inverted_programs.get(unicode(session_id))
|
||||
show_consent_link = (session_id in consent_required_courses)
|
||||
course_overview = enrollment.course_overview
|
||||
%>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name' />
|
||||
% endfor
|
||||
|
||||
</ul>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%page args="course_overview, enrollment, show_courseware_link, cert_status, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard, show_consent_link, enterprise_customer_name" expression_filter="h"/>
|
||||
<%page args="course_overview, enrollment, entitlement, entitlement_session, course_card_index, is_unfulfilled_entitlement, is_fulfilled_entitlement, entitlement_available_sessions, show_courseware_link, cert_status, can_unenroll, credit_status, show_email_settings, course_mode_info, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, related_programs, display_course_modes_on_dashboard, show_consent_link, enterprise_customer_name" expression_filter="h"/>
|
||||
|
||||
<%!
|
||||
import urllib
|
||||
@@ -59,11 +59,12 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
||||
lang="${course_overview.language}"
|
||||
% endif
|
||||
>
|
||||
<div class="course${mode_class}" aria-labelledby="course-title-${enrollment.course_id}">
|
||||
<article class="course${mode_class}" aria-labelledby="course-title-${enrollment.course_id}" id="course-card-${course_card_index}">
|
||||
<% course_target = reverse(course_home_url_name(course_overview.id), args=[unicode(course_overview.id)]) %>
|
||||
<div class="details">
|
||||
<section class="details" aria-labelledby="details-heading-${course_overview.number}">
|
||||
<h2 class="hd hd-2 sr" id="details-heading-${course_overview.number}">${_('Course details')}</h2>
|
||||
<div class="wrapper-course-image" aria-hidden="true">
|
||||
% if show_courseware_link:
|
||||
% if show_courseware_link and not is_unfulfilled_entitlement:
|
||||
% if not is_course_blocked:
|
||||
<a href="${course_target}" data-course-key="${enrollment.course_id}" class="cover" tabindex="-1">
|
||||
<img src="${course_overview.image_urls['small']}" class="course-image" alt="${_('{course_number} {course_name} Home Page').format(course_number=course_overview.number, course_name=course_overview.display_name_with_default)}" />
|
||||
@@ -90,7 +91,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
||||
</div>
|
||||
<div class="wrapper-course-details">
|
||||
<h3 class="course-title" id="course-title-${enrollment.course_id}">
|
||||
% if show_courseware_link:
|
||||
% if show_courseware_link and not is_unfulfilled_entitlement:
|
||||
% if not is_course_blocked:
|
||||
<a data-course-key="${enrollment.course_id}" href="${course_target}">${course_overview.display_name_with_default}</a>
|
||||
% else:
|
||||
@@ -126,18 +127,27 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
||||
endif
|
||||
%>
|
||||
|
||||
% if isinstance(course_date, basestring):
|
||||
<span class="info-date-block" data-tooltip="Hi">${_(container_string).format(date=course_date)}</span>
|
||||
% elif course_date is not None:
|
||||
<%
|
||||
course_date_string = course_date.strftime('%Y-%m-%dT%H:%M:%S%z')
|
||||
%>
|
||||
<span class="info-date-block localized-datetime" data-language="${user_language}" data-tooltip="Hi" data-timezone="${user_timezone}" data-datetime="${course_date_string}" data-format=${format} data-string="${container_string}"></span>
|
||||
<span class="info-date-block-container">
|
||||
% if is_unfulfilled_entitlement:
|
||||
<span class="info-date-block" aria-live="polite">
|
||||
<span class="icon fa fa-warning" aria-hidden="true"></span>
|
||||
${_('You must select a session to access the course.')}
|
||||
</span>
|
||||
% else:
|
||||
% if isinstance(course_date, basestring):
|
||||
<span class="info-date-block">${container_string.format(date=course_date)}</span>
|
||||
% elif course_date is not None:
|
||||
<span class="info-date-block localized-datetime" data-language="${user_language}" data-timezone="${user_timezone}" data-datetime="${course_date.strftime('%Y-%m-%dT%H:%M:%S%z')}" data-format=${format} data-string="${container_string}"></span>
|
||||
% endif
|
||||
% endif
|
||||
% if entitlement:
|
||||
<button class="change-session btn-link ${'hidden' if is_unfulfilled_entitlement else ''}" aria-controls="change-session-${str(entitlement.uuid)}">${_('Change Session')}</button>
|
||||
% endif
|
||||
</span>
|
||||
</div>
|
||||
<div class="wrapper-course-actions">
|
||||
<div class="course-actions">
|
||||
% if show_courseware_link:
|
||||
% if show_courseware_link or is_unfulfilled_entitlement:
|
||||
% if course_overview.has_ended():
|
||||
% if not is_course_blocked:
|
||||
<a href="${course_target}" class="enter-course archived" data-course-key="${enrollment.course_id}">${_('View Archived Course')}<span class="sr"> ${course_overview.display_name_with_default}</span></a>
|
||||
@@ -146,7 +156,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
||||
% endif
|
||||
% else:
|
||||
% if not is_course_blocked:
|
||||
<a href="${course_target}" class="enter-course" data-course-key="${enrollment.course_id}">${_('View Course')}<span class="sr"> ${course_overview.display_name_with_default}</span></a>
|
||||
<a href="${course_target}" class="enter-course ${'hidden' if is_unfulfilled_entitlement else ''}" data-course-key="${enrollment.course_id}">${_('View Course')}<span class="sr"> ${course_overview.display_name_with_default}</span></a>
|
||||
% else:
|
||||
<a class="enter-course-blocked" data-course-key="${enrollment.course_id}">${_('View Course')}<span class="sr"> ${course_overview.display_name_with_default}</span></a>
|
||||
% endif
|
||||
@@ -205,68 +215,91 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
||||
</a>
|
||||
% endif
|
||||
% endif
|
||||
|
||||
% endif
|
||||
|
||||
<div class="wrapper-action-more" data-course-key="${enrollment.course_id}">
|
||||
<button type="button" class="action action-more" id="actions-dropdown-link-${dashboard_index}" aria-haspopup="true" aria-expanded="false" aria-controls="actions-dropdown-${dashboard_index}" data-course-number="${course_overview.number}" data-course-name="${course_overview.display_name_with_default}" data-dashboard-index="${dashboard_index}">
|
||||
<span class="sr">${_('Course options for')}</span>
|
||||
<span class="sr">
|
||||
${course_overview.display_name_with_default}
|
||||
</span>
|
||||
<span class="fa fa-cog" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="actions-dropdown" id="actions-dropdown-${dashboard_index}" tabindex="-1">
|
||||
<ul class="actions-dropdown-list" id="actions-dropdown-list-${dashboard_index}" aria-label="${_('Available Actions')}" role="menu">
|
||||
% if can_unenroll:
|
||||
<li class="actions-item" id="actions-item-unenroll-${dashboard_index}" role="menuitem">
|
||||
<% course_refund_url = reverse('course_run_refund_status', args=[unicode(course_overview.id)]) %>
|
||||
% if not is_course_blocked:
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal"
|
||||
data-course-id="${course_overview.id}"
|
||||
data-course-number="${course_overview.number}"
|
||||
data-course-name="${course_overview.display_name_with_default}"
|
||||
data-dashboard-index="${dashboard_index}"
|
||||
data-course-refund-url="${course_refund_url}"
|
||||
data-course-is-paid-course="${is_paid_course}"
|
||||
data-course-cert-name-long="${cert_name_long}"
|
||||
data-course-enrollment-mode="${enrollment.mode}">
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
% else:
|
||||
<a class="action action-unenroll is-disabled"
|
||||
data-course-id="${course_overview.id}"
|
||||
data-course-number="${course_overview.number}"
|
||||
data-course-name="${course_overview.display_name_with_default}"
|
||||
data-dashboard-index="${dashboard_index}"
|
||||
data-course-refund-url="${course_refund_url}"
|
||||
data-course-is-paid-course="${is_paid_course}"
|
||||
data-course-cert-name-long="${cert_name_long}"
|
||||
data-course-enrollment-mode="${enrollment.mode}">
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
% endif
|
||||
</li>
|
||||
% endif
|
||||
<li class="actions-item" id="actions-item-email-settings-${dashboard_index}" role="menuitem">
|
||||
% if show_email_settings:
|
||||
% if not is_course_blocked:
|
||||
<a href="#email-settings-modal" class="action action-email-settings" rel="leanModal" data-course-id="${course_overview.id}" data-course-number="${course_overview.number}" data-dashboard-index="${dashboard_index}" data-optout="${unicode(course_overview.id) in course_optouts}">${_('Email Settings')}</a>
|
||||
% else:
|
||||
<a class="action action-email-settings is-disabled" data-course-id="${course_overview.id}" data-course-number="${course_overview.number}" data-dashboard-index="${dashboard_index}" data-optout="${unicode(course_overview.id) in course_optouts}">${_('Email Settings')}</a>
|
||||
% endif
|
||||
% if not entitlement:
|
||||
<div class="wrapper-action-more" data-course-key="${enrollment.course_id}">
|
||||
<button type="button" class="action action-more" id="actions-dropdown-link-${dashboard_index}" aria-haspopup="true" aria-expanded="false" aria-controls="actions-dropdown-${dashboard_index}" data-course-number="${course_overview.number}" data-course-name="${course_overview.display_name_with_default}" data-dashboard-index="${dashboard_index}">
|
||||
<span class="sr">${_('Course options for')}</span>
|
||||
<span class="sr">
|
||||
${course_overview.display_name_with_default}
|
||||
</span>
|
||||
<span class="fa fa-cog" aria-hidden="true"></span>
|
||||
</button>
|
||||
<div class="actions-dropdown" id="actions-dropdown-${dashboard_index}" tabindex="-1">
|
||||
<ul class="actions-dropdown-list" id="actions-dropdown-list-${dashboard_index}" aria-label="${_('Available Actions')}" role="menu">
|
||||
% if can_unenroll:
|
||||
<li class="actions-item" id="actions-item-unenroll-${dashboard_index}" role="menuitem">
|
||||
<% course_refund_url = reverse('course_run_refund_status', args=[unicode(course_overview.id)]) %>
|
||||
% if not is_course_blocked:
|
||||
<a href="#unenroll-modal" class="action action-unenroll" rel="leanModal"
|
||||
data-course-id="${course_overview.id}"
|
||||
data-course-number="${course_overview.number}"
|
||||
data-course-name="${course_overview.display_name_with_default}"
|
||||
data-dashboard-index="${dashboard_index}"
|
||||
data-course-refund-url="${course_refund_url}"
|
||||
data-course-is-paid-course="${is_paid_course}"
|
||||
data-course-cert-name-long="${cert_name_long}"
|
||||
data-course-enrollment-mode="${enrollment.mode}">
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
% else:
|
||||
<a class="action action-unenroll is-disabled"
|
||||
data-course-id="${course_overview.id}"
|
||||
data-course-number="${course_overview.number}"
|
||||
data-course-name="${course_overview.display_name_with_default}"
|
||||
data-dashboard-index="${dashboard_index}"
|
||||
data-course-refund-url="${course_refund_url}"
|
||||
data-course-is-paid-course="${is_paid_course}"
|
||||
data-course-cert-name-long="${cert_name_long}"
|
||||
data-course-enrollment-mode="${enrollment.mode}">
|
||||
${_('Unenroll')}
|
||||
</a>
|
||||
% endif
|
||||
</li>
|
||||
% endif
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<li class="actions-item" id="actions-item-email-settings-${dashboard_index}" role="menuitem">
|
||||
% if show_email_settings:
|
||||
% if not is_course_blocked:
|
||||
<a href="#email-settings-modal" class="action action-email-settings" rel="leanModal" data-course-id="${course_overview.id}" data-course-number="${course_overview.number}" data-dashboard-index="${dashboard_index}" data-optout="${unicode(course_overview.id) in course_optouts}">${_('Email Settings')}</a>
|
||||
% else:
|
||||
<a class="action action-email-settings is-disabled" data-course-id="${course_overview.id}" data-course-number="${course_overview.number}" data-dashboard-index="${dashboard_index}" data-optout="${unicode(course_overview.id) in course_optouts}">${_('Email Settings')}</a>
|
||||
% endif
|
||||
% endif
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<footer class="wrapper-messages-primary">
|
||||
<div class="messages-list">
|
||||
% if related_programs:
|
||||
|
||||
% if entitlement:
|
||||
<div class="course-entitlement-selection-container ${'' if is_unfulfilled_entitlement else 'hidden'}"></div>
|
||||
<%static:require_module module_name="js/learner_dashboard/course_entitlement_factory" class_name="EntitlementFactory">
|
||||
EntitlementFactory({
|
||||
el: '${ '#course-card-' + str(course_card_index) + ' .course-entitlement-selection-container' | n, js_escaped_string }',
|
||||
triggerOpenBtn: '${ '#course-card-' + str(course_card_index) + ' .change-session' | n, js_escaped_string }',
|
||||
courseCardMessages: '${ '#course-card-' + str(course_card_index) + ' .messages-list > .message' | n, js_escaped_string }',
|
||||
courseTitleLink: '${ '#course-card-' + str(course_card_index) + ' .course-title a' | n, js_escaped_string }',
|
||||
courseImageLink: '${ '#course-card-' + str(course_card_index) + ' .wrapper-course-image > a' | n, js_escaped_string }',
|
||||
dateDisplayField: '${ '#course-card-' + str(course_card_index) + ' .info-date-block' | n, js_escaped_string }',
|
||||
enterCourseBtn: '${ '#course-card-' + str(course_card_index) + ' .enter-course' | n, js_escaped_string }',
|
||||
availableSessions: '${ entitlement_available_sessions | n, dump_js_escaped_json }',
|
||||
entitlementUUID: '${ entitlement.course_uuid | n, js_escaped_string }',
|
||||
currentSessionId: '${ entitlement_session.course_id if entitlement_session else '' | n, js_escaped_string }',
|
||||
userId: '${ user.id | n, js_escaped_string }',
|
||||
enrollUrl: '${ reverse('entitlements_api:v1:enrollments', args=[str(entitlement.uuid)]) | n, js_escaped_string }',
|
||||
courseHomeUrl: '${ course_target | n, js_escaped_string }'
|
||||
});
|
||||
</%static:require_module>
|
||||
%endif
|
||||
|
||||
% if related_programs and not entitlement:
|
||||
<div class="message message-related-programs is-shown">
|
||||
<span class="related-programs-preface" tabindex="0">${_('Related Programs')}:</span>
|
||||
<ul>
|
||||
@@ -358,7 +391,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if course_mode_info['show_upsell']:
|
||||
% if course_mode_info and course_mode_info['show_upsell'] and not entitlement:
|
||||
<div class="message message-upsell has-actions is-shown">
|
||||
<div class="wrapper-extended">
|
||||
<p class="message-copy" align="justify">
|
||||
@@ -410,7 +443,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
|
||||
% endif
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</li>
|
||||
<script>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
<div id="change-session-<%- entitlementUUID %>" class="message is-shown">
|
||||
<div class="action-header">
|
||||
<% if (currentSessionId) { %>
|
||||
<%- gettext('Change to a different session or leave the current session.')%>
|
||||
<% } else { %>
|
||||
<%- gettext('To access the course, select a session.')%>
|
||||
<% } %>
|
||||
</div>
|
||||
<div class="action-controls">
|
||||
<select class="session-select" aria-label="<%- StringUtils.interpolate( gettext('Session Selection Dropdown for {courseName}'), { courseName: courseName }) %>">
|
||||
<% _.each(availableSessions, function(session) { %>
|
||||
<option data-session_id="<%- session.session_id %>">
|
||||
<% if (session.session_id === currentSessionId) { %>
|
||||
<%- StringUtils.interpolate( gettext('{sessionDates} - Currently Selected'), {sessionDates: session.session_dates}) %>
|
||||
<% } else if (session.enrollment_end){ %>
|
||||
<%- StringUtils.interpolate( gettext('{sessionDates} (Open until {enrollmentEnd})'), {sessionDates: session.session_dates, enrollmentEnd: session.enrollment_end}) %>
|
||||
<% } else { %>
|
||||
<%- session.session_dates %>
|
||||
<% } %>
|
||||
</option>
|
||||
<% }) %>
|
||||
<option disabled><%- gettext('More sessions coming soon') %></option>
|
||||
<% if (currentSessionId){%> <option><%- gettext('Leave the current session and decide later')%></option><% } %>
|
||||
</select>
|
||||
<button class="enroll-btn-initial">
|
||||
<% if (currentSessionId) { %>
|
||||
<%- gettext('Change Session') %>
|
||||
<% } else { %>
|
||||
<%- gettext('Select Session') %>
|
||||
<% } %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,16 @@
|
||||
<div class="verification-modal" role="dialog" aria-labelledby="enrollment-verification-title">
|
||||
<p id="enrollment-verification-title">
|
||||
<div class="popover-title">
|
||||
<%- confirmationMsgTitle %>
|
||||
</div>
|
||||
<%- confirmationMsgBody %>
|
||||
</p>
|
||||
<div class="action-items">
|
||||
<button type="button" class="popover-dismiss final-confirmation-btn">
|
||||
<%- gettext('Cancel') %>
|
||||
</button>
|
||||
<button type="button" class="enroll-btn final-confirmation-btn">
|
||||
<%- gettext('OK') %>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -233,7 +233,7 @@ def get_course_runs_for_course(course_uuid):
|
||||
resource_id=course_uuid,
|
||||
api=api,
|
||||
cache_key=cache_key if catalog_integration.is_cache_enabled else None,
|
||||
long_term_cache=True
|
||||
long_term_cache=True,
|
||||
)
|
||||
return data.get('course_runs', [])
|
||||
else:
|
||||
|
||||
@@ -237,7 +237,7 @@ define([
|
||||
);
|
||||
});
|
||||
|
||||
it('can navigate to correct url', function() {
|
||||
xit('can navigate to correct url', function() {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var bookmarksView = createBookmarksView();
|
||||
var url;
|
||||
|
||||
@@ -9,9 +9,14 @@ import third_party_auth
|
||||
from third_party_auth import pipeline
|
||||
from django.core.urlresolvers import reverse
|
||||
import json
|
||||
from opaque_keys.edx.keys import CourseKey
|
||||
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
|
||||
from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string
|
||||
from openedx.core.djangolib.markup import HTML, Text
|
||||
from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
|
||||
from entitlements.models import CourseEntitlement
|
||||
from student.models import CourseEnrollment
|
||||
%>
|
||||
|
||||
<%
|
||||
@@ -108,28 +113,58 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
<header class="wrapper-header-courses">
|
||||
<h2 class="header-courses">${_("My Courses")}</h2>
|
||||
</header>
|
||||
% if len(course_entitlements + course_enrollments) > 0:
|
||||
<ul class="listing-courses">
|
||||
<% share_settings = getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}) %>
|
||||
% for dashboard_index, enrollment in enumerate(course_entitlements + course_enrollments):
|
||||
<%
|
||||
# Check if the course run is an entitlement and if it has an associated session
|
||||
entitlement = enrollment if isinstance(enrollment, CourseEntitlement) else None
|
||||
entitlement_session = entitlement.enrollment_course_run if entitlement else None
|
||||
|
||||
is_fulfilled_entitlement = True if entitlement and entitlement_session else False
|
||||
is_unfulfilled_entitlement = True if entitlement and not entitlement_session else False
|
||||
|
||||
% if len(course_enrollments) > 0:
|
||||
<ul class="listing-courses">
|
||||
<% share_settings = getattr(settings, 'SOCIAL_SHARING_SETTINGS', {}) %>
|
||||
% for dashboard_index, enrollment in enumerate(course_enrollments):
|
||||
<% show_courseware_link = (enrollment.course_id in show_courseware_links_for) %>
|
||||
<% cert_status = cert_statuses.get(enrollment.course_id) %>
|
||||
<% can_unenroll = (not cert_status) or cert_status.get('can_unenroll') %>
|
||||
<% credit_status = credit_statuses.get(enrollment.course_id) %>
|
||||
<% show_email_settings = (enrollment.course_id in show_email_settings_for) %>
|
||||
<% course_mode_info = all_course_modes.get(enrollment.course_id) %>
|
||||
<% is_paid_course = (enrollment.course_id in enrolled_courses_either_paid) %>
|
||||
<% is_course_blocked = (enrollment.course_id in block_courses) %>
|
||||
<% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %>
|
||||
<% course_requirements = courses_requirements_not_met.get(enrollment.course_id) %>
|
||||
<% related_programs = inverted_programs.get(unicode(enrollment.course_id)) %>
|
||||
<% show_consent_link = (enrollment.course_id in consent_required_courses) %>
|
||||
<%include file = 'dashboard/_dashboard_course_listing.html' args='course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name' />
|
||||
% endfor
|
||||
entitlement_available_sessions = []
|
||||
if entitlement:
|
||||
# Grab the available, enrollable sessions for a given entitlement and scrape them for relevant attributes
|
||||
entitlement_available_sessions = [{
|
||||
'session_id': course['key'],
|
||||
'enrollment_end': course['enrollment_end'],
|
||||
'pacing_type': course['pacing_type'],
|
||||
'session_start_advertised': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).advertised_start,
|
||||
'session_start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).start,
|
||||
'session_end': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).end,
|
||||
} for course in course_entitlement_available_sessions[str(entitlement.uuid)]]
|
||||
if is_fulfilled_entitlement:
|
||||
# If the user has a fulfilled entitlement, pass through the entitlements CourseEnrollment object
|
||||
enrollment = entitlement_session
|
||||
else:
|
||||
# If the user has an unfulfilled entitlement, pass through a bare CourseEnrollment object built off of the next available session
|
||||
upcoming_sessions = course_entitlement_available_sessions[str(entitlement.uuid)]
|
||||
next_session = upcoming_sessions[0] if upcoming_sessions else None
|
||||
if not next_session:
|
||||
continue
|
||||
enrollment = CourseEnrollment(user=user, course_id=next_session['key'], mode=next_session['type'])
|
||||
|
||||
</ul>
|
||||
session_id = enrollment.course_id
|
||||
show_courseware_link = (session_id in show_courseware_links_for)
|
||||
cert_status = cert_statuses.get(session_id)
|
||||
can_unenroll = (not cert_status) or cert_status.get('can_unenroll') if not unfulfilled_entitlement else False
|
||||
credit_status = credit_statuses.get(session_id)
|
||||
show_email_settings = (session_id in show_email_settings_for)
|
||||
course_mode_info = all_course_modes.get(session_id)
|
||||
is_paid_course = True if entitlement else (session_id in enrolled_courses_either_paid)
|
||||
is_course_blocked = (session_id in block_courses)
|
||||
course_verification_status = verification_status_by_course.get(session_id, {})
|
||||
course_requirements = courses_requirements_not_met.get(session_id)
|
||||
related_programs = inverted_programs.get(unicode(session_id))
|
||||
show_consent_link = (session_id in consent_required_courses)
|
||||
course_overview = enrollment.course_overview
|
||||
%>
|
||||
<%include file='dashboard/_dashboard_course_listing.html' args='course_overview=course_overview, course_card_index=dashboard_index, enrollment=enrollment, is_unfulfilled_entitlement=is_unfulfilled_entitlement, is_fulfilled_entitlement=is_fulfilled_entitlement, entitlement=entitlement, entitlement_session=entitlement_session, entitlement_available_sessions=entitlement_available_sessions, show_courseware_link=show_courseware_link, cert_status=cert_status, can_unenroll=can_unenroll, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, related_programs=related_programs, display_course_modes_on_dashboard=display_course_modes_on_dashboard, show_consent_link=show_consent_link, enterprise_customer_name=enterprise_customer_name' />
|
||||
% endfor
|
||||
</ul>
|
||||
% else:
|
||||
<section class="empty-dashboard-message">
|
||||
<p>${_("You are not enrolled in any courses yet.")}</p>
|
||||
|
||||
Reference in New Issue
Block a user