Unverified Commit badb1f84 by Harry Rein Committed by GitHub

Merge pull request #16783 from edx/HarryRein/course-run-selection-program-dashboard

Harry rein/course run selection program dashboard
parents a9506c1a 2711fb52
......@@ -213,6 +213,14 @@ class CourseEntitlement(TimeStampedModel):
"""
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
def set_enrollment(cls, entitlement, enrollment):
"""
......
......@@ -42,6 +42,11 @@
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) {
var unselectedRun = {},
courseRun;
......@@ -143,8 +148,9 @@
formatDateString: function(run) {
var pacingType = run.pacing_type,
dateString,
start = this.get('start_date') || run.start_date,
end = this.get('end_date') || run.end_date,
start = this.valueIsDefined(run.start_date) ? run.advertised_start || run.start_date :
this.get('start_date'),
end = this.valueIsDefined(run.end_date) ? run.end_date : this.get('end_date'),
now = new Date(),
startDate = new Date(start),
endDate = new Date(end);
......@@ -178,26 +184,27 @@
},
setActiveCourseRun: function(courseRun, userPreferences) {
var startDateString;
var startDateString,
isEnrolled = this.isEnrolledInSession() && courseRun.key;
if (courseRun) {
if (this.valueIsDefined(courseRun.advertised_start)) {
startDateString = courseRun.advertised_start;
} else {
startDateString = this.formatDate(courseRun.start, userPreferences);
}
this.set({
certificate_url: courseRun.certificate_url,
course_run_key: courseRun.key,
course_run_key: courseRun.key || '',
course_url: courseRun.course_url || '',
title: this.context.title,
end_date: this.formatDate(courseRun.end, userPreferences),
enrollable_course_runs: this.getEnrollableCourseRuns(),
is_course_ended: courseRun.is_course_ended,
is_enrolled: courseRun.is_enrolled,
is_enrolled: isEnrolled,
is_enrollment_open: courseRun.is_enrollment_open,
course_key: this.context.key,
user_entitlement: this.context.user_entitlement,
is_unfulfilled_entitlement: this.context.user_entitlement && !isEnrolled,
marketing_url: courseRun.marketing_url,
mode_slug: courseRun.type,
start_date: startDateString,
......@@ -220,6 +227,10 @@
updateCourseRun: function(courseRunKey) {
var selectedCourseRun = _.findWhere(this.get('course_runs'), {key: courseRunKey});
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);
}
}
......
/**
* Store data for the current
* Store data for the current entitlement.
*/
(function(define) {
'use strict';
......@@ -13,7 +13,6 @@
availableSessions: [],
entitlementUUID: '',
currentSessionId: '',
userId: '',
courseName: ''
}
});
......
......@@ -11,6 +11,7 @@
'js/learner_dashboard/views/certificate_status_view',
'js/learner_dashboard/views/expired_notification_view',
'js/learner_dashboard/views/course_enroll_view',
'js/learner_dashboard/views/course_entitlement_view',
'text!../../../templates/learner_dashboard/course_card.underscore'
],
function(
......@@ -24,6 +25,7 @@
CertificateStatusView,
ExpiredNotificationView,
CourseEnrollView,
EntitlementView,
pageTpl
) {
return Backbone.View.extend({
......@@ -41,6 +43,8 @@
this.grade = this.context.courseData.grades[this.model.get('course_run_key')];
this.grade = this.grade * 100;
this.collectionCourseStatus = this.context.collectionCourseStatus || '';
this.entitlement = this.model.get('user_entitlement');
this.render();
this.listenTo(this.model, 'change', this.render);
},
......@@ -57,7 +61,9 @@
var $upgradeMessage = this.$('.upgrade-message'),
$certStatus = this.$('.certificate-status'),
$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({
$parentEl: this.$('.course-actions'),
......@@ -68,6 +74,27 @@
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)) {
this.upgradeMessage = new UpgradeMessageView({
$el: $upgradeMessage,
......
......@@ -57,7 +57,7 @@
// Enrollment click event handled here
var courseRunKey = $('.run-select').val() || this.model.get('course_run_key');
this.model.updateCourseRun(courseRunKey);
if (!this.model.get('is_enrolled')) {
if (this.model.get('is_enrolled')) {
// Create the enrollment.
this.enrollModel.save({
course_id: courseRunKey
......
......@@ -36,7 +36,6 @@
initialize: function(options) {
this.options = options;
this.programModel = new Backbone.Model(this.options.programData);
this.courseData = new Backbone.Model(this.options.courseData);
this.certificateCollection = new Backbone.Collection(this.options.certificateData);
......
......@@ -46,6 +46,8 @@
'backbone.associations': 'xmodule_js/common_static/js/vendor/backbone-associations-min',
'backbone.paginator': 'common/js/vendor/backbone.paginator',
'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',
'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',
......@@ -197,6 +199,10 @@
'backbone-super': {
deps: ['backbone']
},
'bootstrap': {
deps: ['jquery', 'popper'],
exports: 'bootstrap'
},
'paging-collection': {
deps: ['jquery', 'underscore', 'backbone.paginator']
},
......
......@@ -10,5 +10,8 @@
@import 'elements/program-card';
@import 'elements-v2/icons';
@import 'elements/progress-circle';
// Various View Styling
@import 'views/course-entitlements';
@import 'views/program-details';
@import 'views/program-list';
......@@ -51,7 +51,8 @@
@import 'multicourse/survey-page';
// base - specific views
@import "views/account-settings";
@import 'views/account-settings';
@import 'views/course-entitlements';
@import 'views/login-register';
@import 'views/verification';
@import 'views/decoupled-verification';
......@@ -59,7 +60,7 @@
@import 'views/homepage';
@import 'views/support';
@import 'views/oauth2';
@import "views/financial-assistance";
@import 'views/financial-assistance';
@import 'course/auto-cert';
@import 'views/api-access';
......
......@@ -1045,6 +1045,7 @@
.related-programs-preface {
@include float(left);
margin: 0 $baseline/2;
font-weight: bold;
}
......@@ -1095,89 +1096,6 @@
@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
......
// 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 @@
.program-course-card {
width: 100%;
padding: 15px;
margin-bottom: 10px;
@media (min-width: $bp-screen-md) {
height: auto;
......@@ -445,6 +444,7 @@
.section {
display: flex;
flex-direction: column;
justify-content: space-between;
@media (min-width: $bp-screen-md) {
......@@ -452,6 +452,10 @@
}
}
.section:not(:last-child) {
margin-bottom: $baseline/2;
}
.section:not(:first-child) {
margin-top: 0;
}
......@@ -461,10 +465,14 @@
.course-title {
font-size: 1em;
color: palette(primary, base);
font-weight: 600;
text-decoration: underline;
margin: 0;
.course-title-link,
.course-title-link:visited{
color: palette(primary, base);
text-decoration: underline;
}
}
.run-period {
......
......@@ -34,6 +34,7 @@ from student.models import CourseEnrollment
</script>
% endfor
% 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/bootstrap.js')}"></script>
% endif
......@@ -141,9 +142,9 @@ from student.models import CourseEnrollment
'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,
'advertised_start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).advertised_start,
'start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).start,
'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
......
......@@ -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 }',
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 }',
currentSessionId: '${ entitlement_session.course_id if entitlement_session else "" | 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 }'
});
......
<div class="section">
<div class="section" id="course-<%-uuid%>">
<div class="course-meta-container">
<div class="course-content">
<div class="course-details">
<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">
<%- title %>
</a>
......@@ -12,11 +12,14 @@
<% } %>
</h5>
<div class="course-text">
<% if (enrolled) { %>
<% if (enrolled && !user_entitlement) { %>
<span class="enrolled"><%- enrolled %>: </span>
<% } %>
<% if (dateString) { %>
<% if (dateString && !is_unfulfilled_entitlement) { %>
<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>
......@@ -24,6 +27,7 @@
</div>
<div class="course-certificate certificate-status"></div>
</div>
<div class="course-entitlement-selection-container<% if (!is_unfulfilled_entitlement && user_entitlement) { %> hidden <% } %>"></div>
</div>
<div class="section action-msg-view"></div>
<div class="section upgrade-message"></div>
......
......@@ -11,7 +11,7 @@
<%- gettext('View Course') %>
<% } %>
</a>
<% } else { %>
<% } else if (!user_entitlement) { %>
<% if (enrollable_course_runs.length > 0) { %>
<% if (enrollable_course_runs.length > 1) { %>
<div class="run-select-container">
......
......@@ -9,8 +9,8 @@
<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) { %>
<option data-session_id="<%- session.session_id || session.key %>">
<% if ((session.session_id || session.key) === 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}) %>
......
......@@ -9,6 +9,10 @@ from openedx.core.djangolib.js_utils import (
%>
<%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">
ProgramDetailsFactory({
programData: ${program_data | n, dump_js_escaped_json},
......
......@@ -222,15 +222,26 @@ class ProgramProgressMeter(object):
completed, in_progress, not_started = [], [], []
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):
completed.append(course)
elif self._is_course_enrolled(course):
course_in_progress = self._is_course_in_progress(now, course)
if course_in_progress:
elif self._is_course_enrolled(course) or entitlement:
# Show all currently enrolled courses and entitlements as 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)
else:
course['expired'] = not course_in_progress
not_started.append(course)
course_in_progress = self._is_course_in_progress(now, course)
if course_in_progress:
in_progress.append(course)
else:
course['expired'] = not course_in_progress
not_started.append(course)
else:
not_started.append(course)
......
......@@ -35,6 +35,7 @@ from student.models import CourseEnrollment
</script>
% endfor
% 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/bootstrap.js')}"></script>
% endif
......@@ -136,9 +137,9 @@ from student.models import CourseEnrollment
'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,
'advertised_start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).advertised_start,
'start': CourseOverview.get_from_id(CourseKey.from_string(course['key'])).start,
'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
......
......@@ -25,6 +25,8 @@ module.exports = {
// LMS
SingleSupportForm: './lms/static/support/jsx/single_support_form.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
CourseGoals: './openedx/features/course_experience/static/course_experience/js/CourseGoals.js',
......@@ -63,6 +65,9 @@ module.exports = {
jQuery: 'jquery',
'window.jQuery': 'jquery'
}),
new webpack.ProvidePlugin({
Popper: 'popper.js'
}),
// Note: Until karma-webpack releases v3, it doesn't play well with
// the CommonsChunkPlugin. We have a kludge in karma.common.conf.js
......@@ -169,6 +174,7 @@ module.exports = {
gettext: 'gettext',
jquery: 'jQuery',
logger: 'Logger',
popper: 'Popper',
underscore: '_',
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