'),
+ HtmlUtils.HTML(this.$courseImageLink.html()),
+ HtmlUtils.HTML('
')
+ ).text
+ );
+ this.$courseTitleLink.replaceWith( // xss-lint: disable=javascript-jquery-insertion
+ HtmlUtils.joinHtml(
+ HtmlUtils.HTML(''),
+ this.$courseTitleLink.text(),
+ HtmlUtils.HTML('')
+ ).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);
diff --git a/lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js b/lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js
new file mode 100644
index 0000000000..934613d929
--- /dev/null
+++ b/lms/static/js/spec/learner_dashboard/course_entitlement_view_spec.js
@@ -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('');
+
+ 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');
+ });
+ });
+ });
+}
+);
diff --git a/lms/static/lms/js/require-config.js b/lms/static/lms/js/require-config.js
index ba785e1d51..c438c2137c 100644
--- a/lms/static/lms/js/require-config.js
+++ b/lms/static/lms/js/require-config.js
@@ -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: [
diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js
index 0ac5f80d62..f52a208565 100644
--- a/lms/static/lms/js/spec/main.js
+++ b/lms/static/lms/js/spec/main.js
@@ -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',
diff --git a/lms/static/sass/multicourse/_dashboard.scss b/lms/static/sass/multicourse/_dashboard.scss
index b6e0b51151..6ee1904ea6 100644
--- a/lms/static/sass/multicourse/_dashboard.scss
+++ b/lms/static/sass/multicourse/_dashboard.scss
@@ -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;
}
-
}
diff --git a/lms/templates/dashboard.html b/lms/templates/dashboard.html
index b70271f6bf..c60337be24 100644
--- a/lms/templates/dashboard.html
+++ b/lms/templates/dashboard.html
@@ -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
<%include file="learner_dashboard/_dashboard_navigation_courses.html"/>
- % if len(course_enrollments) > 0:
+ % if len(course_entitlements + course_enrollments) > 0:
-
<%
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
+
<% course_target = reverse(course_home_url_name(course_overview.id), args=[unicode(course_overview.id)]) %>
-
+
+
+
- ${_('Course details')}
- % if show_courseware_link: + % if show_courseware_link and not is_unfulfilled_entitlement: % if not is_course_blocked: ${course_overview.display_name_with_default} % 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): - ${_(container_string).format(date=course_date)} - % elif course_date is not None: - <% - course_date_string = course_date.strftime('%Y-%m-%dT%H:%M:%S%z') - %> - + + % if is_unfulfilled_entitlement: + + + ${_('You must select a session to access the course.')} + + % else: + % if isinstance(course_date, basestring): + ${container_string.format(date=course_date)} + % elif course_date is not None: + + % endif % endif + % if entitlement: + + % endif +
- % if show_courseware_link:
+ % if show_courseware_link or is_unfulfilled_entitlement:
% if course_overview.has_ended():
% if not is_course_blocked:
${_('View Archived Course')} ${course_overview.display_name_with_default}
@@ -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:
- ${_('View Course')} ${course_overview.display_name_with_default}
+ ${_('View Course')} ${course_overview.display_name_with_default}
% else:
${_('View Course')} ${course_overview.display_name_with_default}
% endif
@@ -205,68 +215,91 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
% endif
% endif
-
% endif
-
-
-
+ % endif
-
+ -
- % if can_unenroll:
-
- - <% course_refund_url = reverse('course_run_refund_status', args=[unicode(course_overview.id)]) %> - % if not is_course_blocked: - - ${_('Unenroll')} - - % else: - - ${_('Unenroll')} - - % endif - - % endif -
-
- % if show_email_settings:
- % if not is_course_blocked:
- ${_('Email Settings')}
- % else:
- ${_('Email Settings')}
- % endif
+ % if not entitlement:
+ + +++-
-
+ % if can_unenroll:
+
- + <% course_refund_url = reverse('course_run_refund_status', args=[unicode(course_overview.id)]) %> + % if not is_course_blocked: + + ${_('Unenroll')} + + % else: + + ${_('Unenroll')} + + % endif + % endif - -
- + % if show_email_settings: + % if not is_course_blocked: + ${_('Email Settings')} + % else: + ${_('Email Settings')} + % endif + % endif + +