Commit 2711fb52 by Harry Rein

Allowing a user to fulfill their entitlement on the programs dashboard.

LEARNER-3438

The user can now enroll in a session, unenroll from a session or change session
from a new course enrollment card on the programs dashboard.
parent a9506c1a
...@@ -213,6 +213,14 @@ class CourseEntitlement(TimeStampedModel): ...@@ -213,6 +213,14 @@ class CourseEntitlement(TimeStampedModel):
""" """
return self.policy.is_entitlement_redeemable(self) return self.policy.is_entitlement_redeemable(self)
def to_dict(self):
""" Convert entitlement to dictionary representation. """
return {
'uuid': str(self.uuid),
'course_uuid': str(self.course_uuid),
'expired_at': self.expired_at
}
@classmethod @classmethod
def set_enrollment(cls, entitlement, enrollment): def set_enrollment(cls, entitlement, enrollment):
""" """
......
...@@ -42,6 +42,11 @@ ...@@ -42,6 +42,11 @@
return desiredCourseRun; return desiredCourseRun;
}, },
isEnrolledInSession: function() {
// Returns true if the user is currently enrolled in a session of the course
return _.findWhere(this.context.course_runs, {is_enrolled: true}) !== undefined;
},
getUnselectedCourseRun: function(courseRuns) { getUnselectedCourseRun: function(courseRuns) {
var unselectedRun = {}, var unselectedRun = {},
courseRun; courseRun;
...@@ -143,8 +148,9 @@ ...@@ -143,8 +148,9 @@
formatDateString: function(run) { formatDateString: function(run) {
var pacingType = run.pacing_type, var pacingType = run.pacing_type,
dateString, dateString,
start = this.get('start_date') || run.start_date, start = this.valueIsDefined(run.start_date) ? run.advertised_start || run.start_date :
end = this.get('end_date') || run.end_date, this.get('start_date'),
end = this.valueIsDefined(run.end_date) ? run.end_date : this.get('end_date'),
now = new Date(), now = new Date(),
startDate = new Date(start), startDate = new Date(start),
endDate = new Date(end); endDate = new Date(end);
...@@ -178,26 +184,27 @@ ...@@ -178,26 +184,27 @@
}, },
setActiveCourseRun: function(courseRun, userPreferences) { setActiveCourseRun: function(courseRun, userPreferences) {
var startDateString; var startDateString,
isEnrolled = this.isEnrolledInSession() && courseRun.key;
if (courseRun) { if (courseRun) {
if (this.valueIsDefined(courseRun.advertised_start)) { if (this.valueIsDefined(courseRun.advertised_start)) {
startDateString = courseRun.advertised_start; startDateString = courseRun.advertised_start;
} else { } else {
startDateString = this.formatDate(courseRun.start, userPreferences); startDateString = this.formatDate(courseRun.start, userPreferences);
} }
this.set({ this.set({
certificate_url: courseRun.certificate_url, certificate_url: courseRun.certificate_url,
course_run_key: courseRun.key, course_run_key: courseRun.key || '',
course_url: courseRun.course_url || '', course_url: courseRun.course_url || '',
title: this.context.title, title: this.context.title,
end_date: this.formatDate(courseRun.end, userPreferences), end_date: this.formatDate(courseRun.end, userPreferences),
enrollable_course_runs: this.getEnrollableCourseRuns(), enrollable_course_runs: this.getEnrollableCourseRuns(),
is_course_ended: courseRun.is_course_ended, is_course_ended: courseRun.is_course_ended,
is_enrolled: courseRun.is_enrolled, is_enrolled: isEnrolled,
is_enrollment_open: courseRun.is_enrollment_open, is_enrollment_open: courseRun.is_enrollment_open,
course_key: this.context.key, course_key: this.context.key,
user_entitlement: this.context.user_entitlement,
is_unfulfilled_entitlement: this.context.user_entitlement && !isEnrolled,
marketing_url: courseRun.marketing_url, marketing_url: courseRun.marketing_url,
mode_slug: courseRun.type, mode_slug: courseRun.type,
start_date: startDateString, start_date: startDateString,
...@@ -220,6 +227,10 @@ ...@@ -220,6 +227,10 @@
updateCourseRun: function(courseRunKey) { updateCourseRun: function(courseRunKey) {
var selectedCourseRun = _.findWhere(this.get('course_runs'), {key: courseRunKey}); var selectedCourseRun = _.findWhere(this.get('course_runs'), {key: courseRunKey});
if (selectedCourseRun) { if (selectedCourseRun) {
// Update the current context to set the course run to the enrolled state
_.each(this.context.course_runs, function(run) {
if (run.key === selectedCourseRun.key) run.is_enrolled = true; // eslint-disable-line no-param-reassign, max-len
});
this.setActiveCourseRun(selectedCourseRun); this.setActiveCourseRun(selectedCourseRun);
} }
} }
......
/** /**
* Store data for the current * Store data for the current entitlement.
*/ */
(function(define) { (function(define) {
'use strict'; 'use strict';
...@@ -13,7 +13,6 @@ ...@@ -13,7 +13,6 @@
availableSessions: [], availableSessions: [],
entitlementUUID: '', entitlementUUID: '',
currentSessionId: '', currentSessionId: '',
userId: '',
courseName: '' courseName: ''
} }
}); });
......
...@@ -11,6 +11,7 @@ ...@@ -11,6 +11,7 @@
'js/learner_dashboard/views/certificate_status_view', 'js/learner_dashboard/views/certificate_status_view',
'js/learner_dashboard/views/expired_notification_view', 'js/learner_dashboard/views/expired_notification_view',
'js/learner_dashboard/views/course_enroll_view', 'js/learner_dashboard/views/course_enroll_view',
'js/learner_dashboard/views/course_entitlement_view',
'text!../../../templates/learner_dashboard/course_card.underscore' 'text!../../../templates/learner_dashboard/course_card.underscore'
], ],
function( function(
...@@ -24,6 +25,7 @@ ...@@ -24,6 +25,7 @@
CertificateStatusView, CertificateStatusView,
ExpiredNotificationView, ExpiredNotificationView,
CourseEnrollView, CourseEnrollView,
EntitlementView,
pageTpl pageTpl
) { ) {
return Backbone.View.extend({ return Backbone.View.extend({
...@@ -41,6 +43,8 @@ ...@@ -41,6 +43,8 @@
this.grade = this.context.courseData.grades[this.model.get('course_run_key')]; this.grade = this.context.courseData.grades[this.model.get('course_run_key')];
this.grade = this.grade * 100; this.grade = this.grade * 100;
this.collectionCourseStatus = this.context.collectionCourseStatus || ''; this.collectionCourseStatus = this.context.collectionCourseStatus || '';
this.entitlement = this.model.get('user_entitlement');
this.render(); this.render();
this.listenTo(this.model, 'change', this.render); this.listenTo(this.model, 'change', this.render);
}, },
...@@ -57,7 +61,9 @@ ...@@ -57,7 +61,9 @@
var $upgradeMessage = this.$('.upgrade-message'), var $upgradeMessage = this.$('.upgrade-message'),
$certStatus = this.$('.certificate-status'), $certStatus = this.$('.certificate-status'),
$expiredNotification = this.$('.expired-notification'), $expiredNotification = this.$('.expired-notification'),
expired = this.model.get('expired'); expired = this.model.get('expired'),
courseUUID = this.model.get('uuid'),
containerSelector = '#course-' + courseUUID;
this.enrollView = new CourseEnrollView({ this.enrollView = new CourseEnrollView({
$parentEl: this.$('.course-actions'), $parentEl: this.$('.course-actions'),
...@@ -68,6 +74,27 @@ ...@@ -68,6 +74,27 @@
enrollModel: this.enrollModel enrollModel: this.enrollModel
}); });
if (this.entitlement) {
this.sessionSelectionView = new EntitlementView({
el: this.$(containerSelector + ' .course-entitlement-selection-container'),
$parentEl: this.$el,
courseCardModel: this.model,
enrollModel: this.enrollModel,
triggerOpenBtn: '.course-details .change-session',
courseCardMessages: '',
courseImageLink: '',
courseTitleLink: containerSelector + ' .course-details .course-title',
dateDisplayField: containerSelector + ' .course-details .course-text',
enterCourseBtn: containerSelector + ' .view-course-button',
availableSessions: JSON.stringify(this.model.get('course_runs')),
entitlementUUID: this.entitlement.uuid,
currentSessionId: this.model.isEnrolledInSession() ?
this.model.get('course_run_key') : null,
enrollUrl: this.model.get('enroll_url'),
courseHomeUrl: this.model.get('course_url')
});
}
if (this.model.get('upgrade_url') && !(expired === true)) { if (this.model.get('upgrade_url') && !(expired === true)) {
this.upgradeMessage = new UpgradeMessageView({ this.upgradeMessage = new UpgradeMessageView({
$el: $upgradeMessage, $el: $upgradeMessage,
......
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
// Enrollment click event handled here // Enrollment click event handled here
var courseRunKey = $('.run-select').val() || this.model.get('course_run_key'); var courseRunKey = $('.run-select').val() || this.model.get('course_run_key');
this.model.updateCourseRun(courseRunKey); this.model.updateCourseRun(courseRunKey);
if (!this.model.get('is_enrolled')) { if (this.model.get('is_enrolled')) {
// Create the enrollment. // Create the enrollment.
this.enrollModel.save({ this.enrollModel.save({
course_id: courseRunKey course_id: courseRunKey
......
...@@ -37,12 +37,12 @@ ...@@ -37,12 +37,12 @@
initialize: function(options) { initialize: function(options) {
// Set up models and reload view on change // Set up models and reload view on change
this.courseCardModel = new CourseCardModel(); this.courseCardModel = options.courseCardModel || new CourseCardModel();
this.enrollModel = options.enrollModel;
this.entitlementModel = new EntitlementModel({ this.entitlementModel = new EntitlementModel({
availableSessions: this.formatDates(JSON.parse(options.availableSessions)), availableSessions: this.formatDates(JSON.parse(options.availableSessions)),
entitlementUUID: options.entitlementUUID, entitlementUUID: options.entitlementUUID,
currentSessionId: options.currentSessionId, currentSessionId: options.currentSessionId,
userId: options.userId,
courseName: options.courseName courseName: options.courseName
}); });
this.listenTo(this.entitlementModel, 'change', this.render); this.listenTo(this.entitlementModel, 'change', this.render);
...@@ -51,13 +51,18 @@ ...@@ -51,13 +51,18 @@
this.enrollUrl = options.enrollUrl; this.enrollUrl = options.enrollUrl;
this.courseHomeUrl = options.courseHomeUrl; this.courseHomeUrl = options.courseHomeUrl;
// Grab elements from the parent card that work with this view and bind associated events // Grab elements from the parent card that work with this view
this.$triggerOpenBtn = $(options.triggerOpenBtn); // Opens/closes session selection view this.$parentEl = options.$parentEl; // Containing course card (must be a backbone view root el)
this.$dateDisplayField = $(options.dateDisplayField); // Displays current session dates
this.$enterCourseBtn = $(options.enterCourseBtn); // Button link to course home page this.$enterCourseBtn = $(options.enterCourseBtn); // Button link to course home page
this.$courseCardMessages = $(options.courseCardMessages); // Additional session messages this.$courseCardMessages = $(options.courseCardMessages); // Additional session messages
this.$courseTitleLink = $(options.courseTitleLink); // Title link to course home page this.$courseTitleLink = $(options.courseTitleLink); // Title link to course home page
this.$courseImageLink = $(options.courseImageLink); // Image link to course home page this.$courseImageLink = $(options.courseImageLink); // Image link to course home page
// Bind action elements with associated events to objects outside this view
this.$dateDisplayField = this.$parentEl ? this.$parentEl.find(options.dateDisplayField) :
$(options.dateDisplayField); // Displays current session dates
this.$triggerOpenBtn = this.$parentEl ? this.$parentEl.find(options.triggerOpenBtn) :
$(options.triggerOpenBtn); // Opens/closes session selection view
this.$triggerOpenBtn.on('click', this.toggleSessionSelectionPanel.bind(this)); this.$triggerOpenBtn.on('click', this.toggleSessionSelectionPanel.bind(this));
this.render(options); this.render(options);
...@@ -72,15 +77,17 @@ ...@@ -72,15 +77,17 @@
}, },
postRender: function() { postRender: function() {
// Close popover on click-away // Close any visible popovers on click-away
$(document).on('click', function(e) { $(document).on('click', function(e) {
if (!($(e.target).closest('.enroll-btn-initial, .popover').length)) { if (this.$('.popover:visible').length &&
!($(e.target).closest('.enroll-btn-initial, .popover').length)) {
this.hideDialog(this.$('.enroll-btn-initial')); this.hideDialog(this.$('.enroll-btn-initial'));
} }
}.bind(this)); }.bind(this));
this.$('.enroll-btn-initial').click(function(e) { // Initialize focus to cancel button on popover load
this.showDialog($(e.target)); $(document).on('shown.bs.popover', function() {
this.$('.final-confirmation-btn:first').focus();
}.bind(this)); }.bind(this));
}, },
...@@ -130,7 +137,13 @@ ...@@ -130,7 +137,13 @@
*/ */
var successIconEl = '<span class="fa fa-check" aria-hidden="true"></span>'; var successIconEl = '<span class="fa fa-check" aria-hidden="true"></span>';
// Update the model with the new session Id; // With a containing backbone view, we can simply re-render the parent card
if (this.$parentEl) {
this.courseCardModel.updateCourseRun(this.currentSessionSelection);
return;
}
// Update the model with the new session Id
this.entitlementModel.set({currentSessionId: this.currentSessionSelection}); this.entitlementModel.set({currentSessionId: this.currentSessionSelection});
// Allow user to change session // Allow user to change session
...@@ -161,6 +174,11 @@ ...@@ -161,6 +174,11 @@
3) Remove the messages associated with the enrolled state. 3) Remove the messages associated with the enrolled state.
4) Remove the link from the course card image and title. 4) Remove the link from the course card image and title.
*/ */
// With a containing backbone view, we can simply re-render the parent card
if (this.$parentEl) {
this.courseCardModel.setUnselected();
return;
}
// Update the model with the new session Id; // Update the model with the new session Id;
this.entitlementModel.set({currentSessionId: this.currentSessionSelection}); this.entitlementModel.set({currentSessionId: this.currentSessionSelection});
...@@ -198,13 +216,18 @@ ...@@ -198,13 +216,18 @@
}, },
enrollError: function() { enrollError: function() {
var errorMsgEl = HtmlUtils.HTML( // Display a success indicator
gettext('There was an error. Please reload the page and try again.') var errorMsgEl = HtmlUtils.joinHtml(
HtmlUtils.HTML('<span class="enroll-error">'),
gettext('There was an error. Please reload the page and try again.'),
HtmlUtils.HTML('</spandiv>')
).text; ).text;
this.$dateDisplayField this.$dateDisplayField
.find('.fa.fa-spin') .find('.fa.fa-spin')
.removeClass('fa-spin fa-spinner') .removeClass('fa-spin fa-spinner')
.addClass('fa-close'); .addClass('fa-close');
this.$dateDisplayField.append(errorMsgEl); this.$dateDisplayField.append(errorMsgEl);
this.hideDialog(this.$('.enroll-btn-initial')); this.hideDialog(this.$('.enroll-btn-initial'));
}, },
...@@ -237,7 +260,6 @@ ...@@ -237,7 +260,6 @@
enrollText = gettext('Leave Current Session'); enrollText = gettext('Leave Current Session');
} }
enrollBtnInitial.text(enrollText); enrollBtnInitial.text(enrollText);
this.removeDialog(enrollBtnInitial);
this.initializeVerificationDialog(enrollBtnInitial); this.initializeVerificationDialog(enrollBtnInitial);
}, },
...@@ -263,7 +285,6 @@ ...@@ -263,7 +285,6 @@
*/ */
var confirmationMsgTitle, var confirmationMsgTitle,
confirmationMsgBody, confirmationMsgBody,
popoverDialogHtml,
currentSessionId = this.entitlementModel.get('currentSessionId'), currentSessionId = this.entitlementModel.get('currentSessionId'),
newSessionId = this.$('.session-select').find('option:selected').data('session_id'); newSessionId = this.$('.session-select').find('option:selected').data('session_id');
...@@ -279,38 +300,35 @@ ...@@ -279,38 +300,35 @@
confirmationMsgBody = gettext('Any course progress or grades from your current session will be lost.'); // eslint-disable-line max-len 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 // Re-initialize the popover
popoverDialogHtml = this.verificationTpl({
confirmationMsgTitle: confirmationMsgTitle,
confirmationMsgBody: confirmationMsgBody
});
invokingElement.popover({ invokingElement.popover({
placement: 'bottom', placement: 'bottom',
container: this.$el, container: this.$el,
html: true, html: true,
trigger: 'click', trigger: 'click',
content: popoverDialogHtml.text content: this.verificationTpl({
confirmationMsgTitle: confirmationMsgTitle,
confirmationMsgBody: confirmationMsgBody
}).text
}); });
}, },
removeDialog: function(invokingElement) { removeDialog: function(el) {
/* Removes the Bootstrap v4 dialog modal from the update session enrollment button. */ /* Removes the Bootstrap v4 dialog modal from the update session enrollment button. */
invokingElement.popover('dispose'); var $el = el instanceof jQuery ? el : this.$('.enroll-btn-initial');
}, if (this.$('popover').length) {
$el.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) { hideDialog: function(el, returnFocus) {
/* Hides the modal without removing it from the DOM. */ /* Hides the modal if it is visible without removing it from the DOM. */
var $el = el instanceof jQuery ? el : this.$('.enroll-btn-initial'); var $el = el instanceof jQuery ? el : this.$('.enroll-btn-initial');
$el.popover('hide'); if (this.$('.popover:visible').length) {
if (returnFocus) { $el.popover('hide');
$el.focus(); if (returnFocus) {
$el.focus();
}
} }
}, },
...@@ -363,12 +381,13 @@ ...@@ -363,12 +381,13 @@
return _.map(formattedSessionData, function(session) { return _.map(formattedSessionData, function(session) {
var formattedSession = session; var formattedSession = session;
startDate = this.formatDate(formattedSession.session_start, dateFormat); startDate = this.formatDate(formattedSession.start, dateFormat);
endDate = this.formatDate(formattedSession.session_end, dateFormat); endDate = this.formatDate(formattedSession.end, dateFormat);
formattedSession.enrollment_end = this.formatDate(formattedSession.enrollment_end, dateFormat); formattedSession.enrollment_end = this.formatDate(formattedSession.enrollment_end, dateFormat);
formattedSession.session_dates = this.courseCardModel.formatDateString({ formattedSession.session_dates = this.courseCardModel.formatDateString({
start_date: session.session_start_advertised || startDate, start_date: startDate,
end_date: session.session_start_advertised ? null : endDate, advertised_start: session.advertised_start,
end_date: endDate,
pacing_type: formattedSession.pacing_type pacing_type: formattedSession.pacing_type
}); });
return formattedSession; return formattedSession;
...@@ -376,7 +395,7 @@ ...@@ -376,7 +395,7 @@
}, },
formatDate: function(date, dateFormat) { formatDate: function(date, dateFormat) {
return date ? moment((new Date(date))).format(dateFormat) : null; return date ? moment((new Date(date))).format(dateFormat) : '';
}, },
getAvailableSessionWithId: function(sessionId) { getAvailableSessionWithId: function(sessionId) {
......
...@@ -36,7 +36,6 @@ ...@@ -36,7 +36,6 @@
initialize: function(options) { initialize: function(options) {
this.options = options; this.options = options;
this.programModel = new Backbone.Model(this.options.programData); this.programModel = new Backbone.Model(this.options.programData);
this.courseData = new Backbone.Model(this.options.courseData); this.courseData = new Backbone.Model(this.options.courseData);
this.certificateCollection = new Backbone.Collection(this.options.certificateData); this.certificateCollection = new Backbone.Collection(this.options.certificateData);
......
...@@ -46,6 +46,8 @@ ...@@ -46,6 +46,8 @@
'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min', 'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min',
'backbone.paginator': 'common/js/vendor/backbone.paginator', 'backbone.paginator': 'common/js/vendor/backbone.paginator',
'backbone-super': 'js/vendor/backbone-super', 'backbone-super': 'js/vendor/backbone-super',
'popper': 'common/js/vendor/popper',
'bootstrap': 'common/js/vendor/bootstrap',
'URI': 'xmodule_js/common_static/js/vendor/URI.min', 'URI': 'xmodule_js/common_static/js/vendor/URI.min',
'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min', 'tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/tinymce.full.min',
'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce', 'jquery.tinymce': 'xmodule_js/common_static/js/vendor/tinymce/js/tinymce/jquery.tinymce',
...@@ -197,6 +199,10 @@ ...@@ -197,6 +199,10 @@
'backbone-super': { 'backbone-super': {
deps: ['backbone'] deps: ['backbone']
}, },
'bootstrap': {
deps: ['jquery', 'popper'],
exports: 'bootstrap'
},
'paging-collection': { 'paging-collection': {
deps: ['jquery', 'underscore', 'backbone.paginator'] deps: ['jquery', 'underscore', 'backbone.paginator']
}, },
......
...@@ -10,5 +10,8 @@ ...@@ -10,5 +10,8 @@
@import 'elements/program-card'; @import 'elements/program-card';
@import 'elements-v2/icons'; @import 'elements-v2/icons';
@import 'elements/progress-circle'; @import 'elements/progress-circle';
// Various View Styling
@import 'views/course-entitlements';
@import 'views/program-details'; @import 'views/program-details';
@import 'views/program-list'; @import 'views/program-list';
...@@ -51,7 +51,8 @@ ...@@ -51,7 +51,8 @@
@import 'multicourse/survey-page'; @import 'multicourse/survey-page';
// base - specific views // base - specific views
@import "views/account-settings"; @import 'views/account-settings';
@import 'views/course-entitlements';
@import 'views/login-register'; @import 'views/login-register';
@import 'views/verification'; @import 'views/verification';
@import 'views/decoupled-verification'; @import 'views/decoupled-verification';
...@@ -59,7 +60,7 @@ ...@@ -59,7 +60,7 @@
@import 'views/homepage'; @import 'views/homepage';
@import 'views/support'; @import 'views/support';
@import 'views/oauth2'; @import 'views/oauth2';
@import "views/financial-assistance"; @import 'views/financial-assistance';
@import 'course/auto-cert'; @import 'course/auto-cert';
@import 'views/api-access'; @import 'views/api-access';
......
...@@ -1045,6 +1045,7 @@ ...@@ -1045,6 +1045,7 @@
.related-programs-preface { .related-programs-preface {
@include float(left); @include float(left);
margin: 0 $baseline/2;
font-weight: bold; font-weight: bold;
} }
...@@ -1095,89 +1096,6 @@ ...@@ -1095,89 +1096,6 @@
@include padding($baseline/2, $baseline, $baseline/2, $baseline/2); @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 // CASE: empty dashboard
......
// Shared styling between courses and programs dashboard
.course-entitlement-selection-container {
width: 100%;
position: relative;
flex-grow: 1;
.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;
max-width: calc(100% - 200px);
}
.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");
}
&.disabled {
pointer-events: none;
opacity: 0.5;
}
}
@include media-breakpoint-down(xs) {
flex-direction: column;
.session-select {
max-width: 100%;
}
.enroll-btn-initial {
margin: $baseline/4 0;
}
}
}
.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");
}
}
}
}
}
// Styling overrides specific to the programs dashboard
.program-course-card {
.course-text {
.fa-close {
color: theme-color("error");
}
.enroll-error {
@include margin-left($baseline/4);
font-size: $font-size-sm;
}
.change-session {
@include margin(0, 0, $baseline/4, $baseline/4);
padding: 0;
font-size: $font-size-sm;
letter-spacing: normal;
}
}
.course-entitlement-selection-container {
padding-top: $baseline/2;
.action-header,
.action-controls .session-select{
font-size: $font-size-sm;
}
}
}
...@@ -437,7 +437,6 @@ ...@@ -437,7 +437,6 @@
.program-course-card { .program-course-card {
width: 100%; width: 100%;
padding: 15px; padding: 15px;
margin-bottom: 10px;
@media (min-width: $bp-screen-md) { @media (min-width: $bp-screen-md) {
height: auto; height: auto;
...@@ -445,6 +444,7 @@ ...@@ -445,6 +444,7 @@
.section { .section {
display: flex; display: flex;
flex-direction: column;
justify-content: space-between; justify-content: space-between;
@media (min-width: $bp-screen-md) { @media (min-width: $bp-screen-md) {
...@@ -452,6 +452,10 @@ ...@@ -452,6 +452,10 @@
} }
} }
.section:not(:last-child) {
margin-bottom: $baseline/2;
}
.section:not(:first-child) { .section:not(:first-child) {
margin-top: 0; margin-top: 0;
} }
...@@ -461,10 +465,14 @@ ...@@ -461,10 +465,14 @@
.course-title { .course-title {
font-size: 1em; font-size: 1em;
color: palette(primary, base);
font-weight: 600; font-weight: 600;
text-decoration: underline;
margin: 0; margin: 0;
.course-title-link,
.course-title-link:visited{
color: palette(primary, base);
text-decoration: underline;
}
} }
.run-period { .run-period {
......
...@@ -34,6 +34,7 @@ from student.models import CourseEnrollment ...@@ -34,6 +34,7 @@ from student.models import CourseEnrollment
</script> </script>
% endfor % endfor
% if course_entitlements: % if course_entitlements:
<!-- This is a temporary solution before we land a fix to load these through Webpack, tracked by LEARNER-3483 -->
<script type="text/javascript" src="${static.url('common/js/vendor/popper.js')}"></script> <script type="text/javascript" src="${static.url('common/js/vendor/popper.js')}"></script>
<script type="text/javascript" src="${static.url('common/js/vendor/bootstrap.js')}"></script> <script type="text/javascript" src="${static.url('common/js/vendor/bootstrap.js')}"></script>
% endif % endif
...@@ -141,9 +142,9 @@ from student.models import CourseEnrollment ...@@ -141,9 +142,9 @@ from student.models import CourseEnrollment
'session_id': course['key'], 'session_id': course['key'],
'enrollment_end': course['enrollment_end'], 'enrollment_end': course['enrollment_end'],
'pacing_type': course['pacing_type'], 'pacing_type': course['pacing_type'],
'session_start_advertised': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).advertised_start, 'advertised_start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).advertised_start,
'session_start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).start, 'start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).start,
'session_end': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).end, 'end': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).end,
} for course in course_entitlement_available_sessions[str(entitlement.uuid)]] } for course in course_entitlement_available_sessions[str(entitlement.uuid)]]
if is_fulfilled_entitlement: if is_fulfilled_entitlement:
# If the user has a fulfilled entitlement, pass through the entitlements CourseEnrollment object # If the user has a fulfilled entitlement, pass through the entitlements CourseEnrollment object
......
...@@ -291,8 +291,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_ ...@@ -291,8 +291,7 @@ from util.course import get_link_for_about_page, get_encoded_course_sharing_utm_
enterCourseBtn: '${ '#course-card-' + str(course_card_index) + ' .enter-course' | 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 }', availableSessions: '${ entitlement_available_sessions | n, dump_js_escaped_json }',
entitlementUUID: '${ entitlement.course_uuid | n, js_escaped_string }', entitlementUUID: '${ entitlement.course_uuid | n, js_escaped_string }',
currentSessionId: '${ entitlement_session.course_id if entitlement_session else '' | 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 }', enrollUrl: '${ reverse('entitlements_api:v1:enrollments', args=[str(entitlement.uuid)]) | n, js_escaped_string }',
courseHomeUrl: '${ course_target | n, js_escaped_string }' courseHomeUrl: '${ course_target | n, js_escaped_string }'
}); });
......
<div class="section"> <div class="section" id="course-<%-uuid%>">
<div class="course-meta-container"> <div class="course-meta-container">
<div class="course-content"> <div class="course-content">
<div class="course-details"> <div class="course-details">
<h5 class="course-title"> <h5 class="course-title">
<% if ( marketing_url || course_url ) { %> <% if ( (marketing_url || course_url) && !is_unfulfilled_entitlement) { %>
<a href="<%- marketing_url || course_url %>" class="course-title-link"> <a href="<%- marketing_url || course_url %>" class="course-title-link">
<%- title %> <%- title %>
</a> </a>
...@@ -12,11 +12,14 @@ ...@@ -12,11 +12,14 @@
<% } %> <% } %>
</h5> </h5>
<div class="course-text"> <div class="course-text">
<% if (enrolled) { %> <% if (enrolled && !user_entitlement) { %>
<span class="enrolled"><%- enrolled %>: </span> <span class="enrolled"><%- enrolled %>: </span>
<% } %> <% } %>
<% if (dateString) { %> <% if (dateString && !is_unfulfilled_entitlement) { %>
<span class="run-period"><%- dateString %></span> <span class="run-period"><%- dateString %></span>
<% if (user_entitlement && !is_unfulfilled_entitlement) { %>
<button class="change-session btn-link" aria-controls="change-session-<%-user_entitlement.uuid%>"> <%- gettext('Change Session')%></button>
<% } %>
<% } %> <% } %>
</div> </div>
</div> </div>
...@@ -24,6 +27,7 @@ ...@@ -24,6 +27,7 @@
</div> </div>
<div class="course-certificate certificate-status"></div> <div class="course-certificate certificate-status"></div>
</div> </div>
<div class="course-entitlement-selection-container<% if (!is_unfulfilled_entitlement && user_entitlement) { %> hidden <% } %>"></div>
</div> </div>
<div class="section action-msg-view"></div> <div class="section action-msg-view"></div>
<div class="section upgrade-message"></div> <div class="section upgrade-message"></div>
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<%- gettext('View Course') %> <%- gettext('View Course') %>
<% } %> <% } %>
</a> </a>
<% } else { %> <% } else if (!user_entitlement) { %>
<% if (enrollable_course_runs.length > 0) { %> <% if (enrollable_course_runs.length > 0) { %>
<% if (enrollable_course_runs.length > 1) { %> <% if (enrollable_course_runs.length > 1) { %>
<div class="run-select-container"> <div class="run-select-container">
......
...@@ -9,8 +9,8 @@ ...@@ -9,8 +9,8 @@
<div class="action-controls"> <div class="action-controls">
<select class="session-select" aria-label="<%- StringUtils.interpolate( gettext('Session Selection Dropdown for {courseName}'), { courseName: courseName }) %>"> <select class="session-select" aria-label="<%- StringUtils.interpolate( gettext('Session Selection Dropdown for {courseName}'), { courseName: courseName }) %>">
<% _.each(availableSessions, function(session) { %> <% _.each(availableSessions, function(session) { %>
<option data-session_id="<%- session.session_id %>"> <option data-session_id="<%- session.session_id || session.key %>">
<% if (session.session_id === currentSessionId) { %> <% if ((session.session_id || session.key) === currentSessionId) { %>
<%- StringUtils.interpolate( gettext('{sessionDates} - Currently Selected'), {sessionDates: session.session_dates}) %> <%- StringUtils.interpolate( gettext('{sessionDates} - Currently Selected'), {sessionDates: session.session_dates}) %>
<% } else if (session.enrollment_end){ %> <% } else if (session.enrollment_end){ %>
<%- StringUtils.interpolate( gettext('{sessionDates} (Open until {enrollmentEnd})'), {sessionDates: session.session_dates, enrollmentEnd: session.enrollment_end}) %> <%- StringUtils.interpolate( gettext('{sessionDates} (Open until {enrollmentEnd})'), {sessionDates: session.session_dates, enrollmentEnd: session.enrollment_end}) %>
......
...@@ -9,6 +9,10 @@ from openedx.core.djangolib.js_utils import ( ...@@ -9,6 +9,10 @@ from openedx.core.djangolib.js_utils import (
%> %>
<%block name="js_extra"> <%block name="js_extra">
<!-- This is a temporary solution before we land a fix to load these through Webpack, tracked by LEARNER-3483 -->
<script type="text/javascript" src="${static.url('common/js/vendor/popper.js')}"></script>
<script type="text/javascript" src="${static.url('common/js/vendor/bootstrap.js')}"></script>
<%static:require_module module_name="js/learner_dashboard/program_details_factory" class_name="ProgramDetailsFactory"> <%static:require_module module_name="js/learner_dashboard/program_details_factory" class_name="ProgramDetailsFactory">
ProgramDetailsFactory({ ProgramDetailsFactory({
programData: ${program_data | n, dump_js_escaped_json}, programData: ${program_data | n, dump_js_escaped_json},
......
...@@ -222,15 +222,26 @@ class ProgramProgressMeter(object): ...@@ -222,15 +222,26 @@ class ProgramProgressMeter(object):
completed, in_progress, not_started = [], [], [] completed, in_progress, not_started = [], [], []
for course in program_copy['courses']: for course in program_copy['courses']:
try:
entitlement = CourseEntitlement.objects.get(user=self.user, course_uuid=course['uuid'])
except CourseEntitlement.DoesNotExist:
entitlement = None
if self._is_course_complete(course): if self._is_course_complete(course):
completed.append(course) completed.append(course)
elif self._is_course_enrolled(course): elif self._is_course_enrolled(course) or entitlement:
course_in_progress = self._is_course_in_progress(now, course) # Show all currently enrolled courses and entitlements as in progress
if course_in_progress: if entitlement:
course['user_entitlement'] = entitlement.to_dict()
course['enroll_url'] = reverse('entitlements_api:v1:enrollments', args=[str(entitlement.uuid)])
in_progress.append(course) in_progress.append(course)
else: else:
course['expired'] = not course_in_progress course_in_progress = self._is_course_in_progress(now, course)
not_started.append(course) if course_in_progress:
in_progress.append(course)
else:
course['expired'] = not course_in_progress
not_started.append(course)
else: else:
not_started.append(course) not_started.append(course)
......
...@@ -35,6 +35,7 @@ from student.models import CourseEnrollment ...@@ -35,6 +35,7 @@ from student.models import CourseEnrollment
</script> </script>
% endfor % endfor
% if course_entitlements: % if course_entitlements:
<!-- This is a temporary solution before we land a fix to load these through Webpack, tracked by LEARNER-3483 -->
<script type="text/javascript" src="${static.url('common/js/vendor/popper.js')}"></script> <script type="text/javascript" src="${static.url('common/js/vendor/popper.js')}"></script>
<script type="text/javascript" src="${static.url('common/js/vendor/bootstrap.js')}"></script> <script type="text/javascript" src="${static.url('common/js/vendor/bootstrap.js')}"></script>
% endif % endif
...@@ -136,9 +137,9 @@ from student.models import CourseEnrollment ...@@ -136,9 +137,9 @@ from student.models import CourseEnrollment
'session_id': course['key'], 'session_id': course['key'],
'enrollment_end': course['enrollment_end'], 'enrollment_end': course['enrollment_end'],
'pacing_type': course['pacing_type'], 'pacing_type': course['pacing_type'],
'session_start_advertised': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).advertised_start, 'advertised_start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).advertised_start,
'session_start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).start, 'start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).start,
'session_end': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).end, 'end': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).end,
} for course in course_entitlement_available_sessions[str(entitlement.uuid)]] } for course in course_entitlement_available_sessions[str(entitlement.uuid)]]
if is_fulfilled_entitlement: if is_fulfilled_entitlement:
# If the user has a fulfilled entitlement, pass through the entitlements CourseEnrollment object # If the user has a fulfilled entitlement, pass through the entitlements CourseEnrollment object
......
...@@ -25,6 +25,8 @@ module.exports = { ...@@ -25,6 +25,8 @@ module.exports = {
// LMS // LMS
SingleSupportForm: './lms/static/support/jsx/single_support_form.jsx', SingleSupportForm: './lms/static/support/jsx/single_support_form.jsx',
AlertStatusBar: './lms/static/js/accessible_components/StatusBarAlert.jsx', AlertStatusBar: './lms/static/js/accessible_components/StatusBarAlert.jsx',
Bootstrap: './lms/static/common/js/vendor/bootstrap.js',
EntitlementView: './lms/static/js/learner_dashboard/views/course_entitlement_view.js',
// Features // Features
CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js', CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js',
...@@ -63,6 +65,9 @@ module.exports = { ...@@ -63,6 +65,9 @@ module.exports = {
jQuery: 'jquery', jQuery: 'jquery',
'window.jQuery': 'jquery' 'window.jQuery': 'jquery'
}), }),
new webpack.ProvidePlugin({
Popper: 'popper.js'
}),
// Note: Until karma-webpack releases v3, it doesn't play well with // Note: Until karma-webpack releases v3, it doesn't play well with
// the CommonsChunkPlugin. We have a kludge in karma.common.conf.js // the CommonsChunkPlugin. We have a kludge in karma.common.conf.js
...@@ -169,6 +174,7 @@ module.exports = { ...@@ -169,6 +174,7 @@ module.exports = {
gettext: 'gettext', gettext: 'gettext',
jquery: 'jQuery', jquery: 'jQuery',
logger: 'Logger', logger: 'Logger',
popper: 'Popper',
underscore: '_', underscore: '_',
URI: 'URI' URI: 'URI'
}, },
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment