Commit 8d672bc8 by Simon Chen Committed by GitHub

ECOM-3206 allow run selection and enrollment (#12685)

parent 781a2049
......@@ -284,7 +284,8 @@ class TestProgramDetails(ProgramsApiConfigMixin, SharedModuleStoreTestCase):
def _assert_program_data_present(self, response):
"""Verify that program data is present."""
self.assertContains(response, 'programData')
self.assertContains(response, 'programListingUrl')
self.assertContains(response, 'urls')
self.assertContains(response, 'program_listing_url')
self.assertContains(response, self.data['name'])
self._assert_programs_tab_present(response)
......
"""
Unit test module covering utils module
"""
import ddt
from django.test import TestCase
from lms.djangoapps.learner_dashboard import utils
@ddt.ddt
class TestUtils(TestCase):
"""
The test case class covering the all the utils functions
"""
@ddt.data('path1/', '/path1/path2/', '/', '')
def test_strip_course_id(self, path):
"""
Test to make sure the function 'strip_course_id'
handles various url input
"""
actual = utils.strip_course_id(path + unicode(utils.FAKE_COURSE_KEY))
self.assertEqual(actual, path)
"""
The utility methods and functions to help the djangoapp logic
"""
from opaque_keys.edx.keys import CourseKey
FAKE_COURSE_KEY = CourseKey.from_string('course-v1:fake+course+run')
def strip_course_id(path):
"""
The utility function to help remove the fake
course ID from the url path
"""
course_id = unicode(FAKE_COURSE_KEY)
return path.split(course_id)[0]
......@@ -11,6 +11,10 @@ from edxmako.shortcuts import render_to_response
from openedx.core.djangoapps.credentials.utils import get_programs_credentials
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs import utils
from lms.djangoapps.learner_dashboard.utils import (
FAKE_COURSE_KEY,
strip_course_id
)
@login_required
......@@ -63,9 +67,16 @@ def program_details(request, program_id):
program_data = utils.supplement_program_data(program_data, request.user)
show_program_listing = ProgramsApiConfig.current().show_program_listing
urls = {
'program_listing_url': reverse('program_listing_view'),
'track_selection_url': strip_course_id(
reverse('course_modes_choose', kwargs={'course_id': FAKE_COURSE_KEY})),
'commerce_api_url': reverse('commerce_api:v0:baskets:create')
}
context = {
'program_data': program_data,
'program_listing_url': reverse('program_listing_view'),
'urls': urls,
'show_program_listing': show_program_listing,
'nav_hidden': True,
'disable_courseware_js': True,
......
......@@ -11,27 +11,49 @@
initialize: function(data) {
if (data){
this.context = data;
//we should populate our model by looking at the run_modes
if (data.run_modes.length > 0){
//We only have 1 run mode for this program
this.setActiveRunMode(data.run_modes[0]);
}
this.setActiveRunMode(this.getRunMode(data.run_modes));
}
},
getRunMode: function(runModes){
//we should populate our model by looking at the run_modes
if (runModes.length > 0){
if(runModes.length === 1){
return runModes[0];
}else{
//We need to implement logic here to select the
//most relevant run mode for the student to enroll
return runModes[0];
}
}else{
return null;
}
},
setActiveRunMode: function(runMode){
this.set({
display_name: this.context.display_name,
key: this.context.key,
marketing_url: runMode.marketing_url || '',
start_date: runMode.start_date,
end_date: runMode.end_date,
is_enrolled: runMode.is_enrolled,
is_enrollment_open: runMode.is_enrollment_open,
course_url: runMode.course_url || '',
course_image_url: runMode.course_image_url || '',
mode_slug: runMode.mode_slug
});
if (runMode){
this.set({
display_name: this.context.display_name,
key: this.context.key,
marketing_url: runMode.marketing_url || '',
start_date: runMode.start_date,
end_date: runMode.end_date,
is_enrolled: runMode.is_enrolled,
is_enrollment_open: runMode.is_enrollment_open,
course_key: runMode.course_key,
course_url: runMode.course_url || '',
course_image_url: runMode.course_image_url || '',
mode_slug: runMode.mode_slug,
run_key: runMode.run_key
});
}
},
updateRun: function(runKey){
var selectedRun = _.findWhere(this.get('run_modes'), {run_key: runKey});
if (selectedRun){
this.setActiveRunMode(selectedRun);
}
}
});
});
......
/**
* Store data to enroll learners into the course
*/
;(function (define) {
'use strict';
define([
'backbone'
],
function( Backbone) {
return Backbone.Model.extend({
defaults: {
course_id: '',
optIn: false,
}
});
}
);
}).call(this, define || RequireJS.define);
......@@ -46,6 +46,7 @@
if (this.titleContext){
this.$el.before(HtmlUtils.ensureHtml(this.getTitleHtml()).toString());
}
this.$el.html(childList);
}
},
......
......@@ -6,6 +6,7 @@
'underscore',
'gettext',
'edx-ui-toolkit/js/utils/html-utils',
'js/learner_dashboard/models/course_enroll_model',
'js/learner_dashboard/views/course_enroll_view',
'text!../../../templates/learner_dashboard/course_card.underscore'
],
......@@ -15,6 +16,7 @@
_,
gettext,
HtmlUtils,
EnrollModel,
CourseEnrollView,
pageTpl
) {
......@@ -23,8 +25,14 @@
tpl: HtmlUtils.template(pageTpl),
initialize: function() {
initialize: function(options) {
this.enrollModel = new EnrollModel();
if(options.context && options.context.urls){
this.urlModel = new Backbone.Model(options.context.urls);
this.enrollModel.urlRoot = this.urlModel.get('commerce_api_url');
}
this.render();
this.listenTo(this.model, 'change', this.render);
},
render: function() {
......@@ -35,9 +43,10 @@
postRender: function(){
this.enrollView = new CourseEnrollView({
$el: this.$('.course-actions'),
$parentEl: this.$('.course-actions'),
model: this.model,
context: this.context
urlModel: this.urlModel,
enrollModel: this.enrollModel
});
}
});
......
......@@ -20,23 +20,89 @@
tpl: HtmlUtils.template(pageTpl),
events: {
'click .enroll-button': 'handleEnroll'
'click .enroll-button': 'handleEnroll',
'change .run-select': 'handleRunSelect',
},
initialize: function(options) {
if (options.$el){
this.$el = options.$el;
this.render();
this.$parentEl = options.$parentEl;
this.enrollModel = options.enrollModel;
this.urlModel = options.urlModel;
this.render();
if(this.urlModel){
this.trackSelectionUrl = this.urlModel.get('track_selection_url');
}
},
render: function() {
var filledTemplate = this.tpl(this.model.toJSON());
HtmlUtils.setHtml(this.$el, filledTemplate);
var filledTemplate;
if (this.$parentEl &&
this.enrollModel &&
this.model.get('course_key')){
filledTemplate = this.tpl(this.model.toJSON());
HtmlUtils.setHtml(this.$el, filledTemplate);
HtmlUtils.setHtml(this.$parentEl, HtmlUtils.HTML(this.$el));
}
},
handleEnroll: function(){
//Enrollment click event handled here
if (!this.model.get('is_enrolled')){
// actually enroll
this.enrollModel.save({
course_id: this.model.get('course_key')
}, {
success: _.bind(this.enrollSuccess, this),
error: _.bind(this.enrollError, this)
});
}
},
handleRunSelect: function(event){
var runKey;
if (event.target){
runKey = $(event.target).val();
if (runKey){
this.model.updateRun(runKey);
}
}
},
enrollSuccess: function(){
var courseKey = this.model.get('course_key');
if (this.trackSelectionUrl) {
// Go to track selection page
this.redirect( this.trackSelectionUrl + courseKey );
}else{
this.model.set({
is_enrolled: true
});
}
},
enrollError: function(model, response) {
if (response.status === 403 && response.responseJSON.user_message_url) {
/**
* Check if we've been blocked from the course
* because of country access rules.
* If so, redirect to a page explaining to the user
* why they were blocked.
*/
this.redirect( response.responseJSON.user_message_url );
} else if (this.trackSelectionUrl){
/**
* Otherwise, go to the track selection page as usual.
* This can occur, for example, when a course does not
* have a free enrollment mode, so we can't auto-enroll.
*/
this.redirect( this.trackSelectionUrl + this.model.get('course_key') );
}
},
redirect: function( url ) {
window.location.href = url;
}
});
}
......
......@@ -51,7 +51,7 @@
el: '.js-course-list',
childView: CourseCardView,
collection: this.courseCardCollection,
context: this.programModel.toJSON(),
context: this.options,
titleContext: {
el: 'h2',
title: 'Course List'
......
......@@ -10,7 +10,6 @@ define([
describe('Course Card View', function () {
var view = null,
courseCardModel,
setupView,
context = {
course_modes: [],
display_name: 'Astrophysics: Exploring Exoplanets',
......@@ -32,7 +31,7 @@ define([
is_enrolled: true,
certificate_url: '',
}]
};
},
setupView = function(isEnrolled){
context.run_modes[0].is_enrolled = isEnrolled;
......@@ -41,6 +40,17 @@ define([
view = new CourseCardView({
model: courseCardModel
});
},
validateCourseInfoDisplay = function(){
//DRY validation for course card in enrolled state
expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url);
expect(view.$('.course-details .course-title-link').text().trim()).toEqual(context.display_name);
expect(view.$('.course-details .course-title-link').attr('href')).toEqual(
context.run_modes[0].course_url);
expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key);
expect(view.$('.course-details .course-text .run-period').html())
.toEqual(context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date);
};
beforeEach(function() {
......@@ -58,22 +68,18 @@ define([
it('should render the course card based on the data enrolled', function() {
view.remove();
setupView(true);
expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url);
expect(view.$('.course-details .course-title-link').text().trim()).toEqual(context.display_name);
expect(view.$('.course-details .course-title-link').attr('href')).toEqual(
context.run_modes[0].course_url);
expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key);
expect(view.$('.course-details .course-text .run-period').html())
.toEqual(context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date);
validateCourseInfoDisplay();
});
it('should render the course card based on the data not enrolled', function() {
expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url);
expect(view.$('.course-details .course-title-link').text().trim()).toEqual(context.display_name);
expect(view.$('.course-details .course-title-link').attr('href')).toEqual(
context.run_modes[0].course_url);
expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key);
expect(view.$('.course-details .course-text .run-period').html()).not.toBeDefined();
validateCourseInfoDisplay();
});
it('should update render if the course card is_enrolled updated', function(){
courseCardModel.set({
is_enrolled: true
});
validateCourseInfoDisplay();
});
});
}
......
......@@ -8,9 +8,10 @@ define([
describe('Program Details Header View', function () {
var view = null,
programModel,
context = {
programListingUrl: '/dashboard/programs',
urls: {
program_listing_url: '/dashboard/programs'
},
programData: {
uuid: '12-ab',
name: 'Astrophysics',
......@@ -58,7 +59,7 @@ define([
expect(view.$('.org-logo').attr('alt')).toEqual(
context.programData.organizations[0].display_name + '\'s logo'
);
expect(programListUrl).toEqual(context.programListingUrl);
expect(programListUrl).toEqual(context.urls.program_listing_url);
});
});
}
......
.course-card{
@include span(10);
margin-left: $baseline*2 + px;
margin-bottom: $baseline + px;
margin-left: $baseline*2;
margin-bottom: $baseline;
.course-image-link{
@include float(left);
.header-img{
......@@ -12,25 +12,34 @@
@include float(right);
width: 100%;
@include susy-media($bp-screen-sm) { width: calc(100% - 191px); }
padding-left: $baseline*1.5 + px;
padding-left: $baseline*1.5;
.course-title{
font-size: font-size(x-large);
font-weight: font-weight(normal);
margin-bottom: $baseline/4 + px;
margin-bottom: $baseline/4;
}
.course-text{
color: palette(grayscale, dark);
.run-period{
color: palette(grayscale, black);
}
}
}
.course-actions{
.enrollment-info{
width: $baseline*10 + px;
width: $baseline*10;
text-align: center;
margin-bottom: $baseline/2 + px;
margin-bottom: $baseline/2;
text-transform: uppercase;
}
.run-select-container{
margin-bottom: $baseline;
.run-select{
width: $baseline*10;
}
}
.enroll-button{
width: $baseline*10 + px;
width: $baseline*10;
text-align: center;
background-color: palette(success, dark);
border-color: palette(success, dark);
......@@ -42,7 +51,7 @@
}
.view-course-link{
width: $baseline*10 + px;
width: $baseline*10;
text-align: center;
}
}
......
......@@ -4,7 +4,7 @@
<img
class="header-img"
src="<%- course_image_url %>"
alt="<%= interpolate(gettext('%(courseName)s Home Page.'), {courseName: display_name}, true)%>"/>
alt="<%= interpolate(gettext('%(courseName)s Home Page.'), {courseName: display_name}, true) %>"/>
</a>
<div class="course-details">
<h3 class="course-title">
......@@ -13,9 +13,8 @@
</a>
</h3>
<div class="course-text">
<% if (is_enrolled){ %>
<span class="run-period"><%- start_date %> - <%- end_date %></span>
<% } %>
<span class="run-period"><%- start_date %> - <%- end_date %></span>
-
<span class="course-key"><%- key %></span>
</div>
</div>
......
......@@ -5,6 +5,27 @@
</a>
<% }else{ %>
<div class="enrollment-info"><%- gettext('not enrolled') %></div>
<% if (run_modes.length > 1){ %>
<div class="run-select-container">
<label class="sr-only" for="select-<%- course_key %>-run">Select Course Run</label>
<select id="select-<%- course_key %>-run" class="run-select" autocomplete="off">
<% _.each (run_modes, function(runMode){ %>
<option
value="<%- runMode.run_key %>"
<% if(run_key === runMode.run_key){ %>
selected="selected"
<% }%>
>
<%= interpolate(
gettext('Starts %(start)s'),
{ start: runMode.start_date },
true)
%>
</option>
<% }); %>
</select>
</div>
<% } %>
<button type="button" class="btn-brand btn enroll-button">
<%- gettext('Enroll Now') %>
</button>
......
......@@ -15,7 +15,7 @@ from openedx.core.djangolib.js_utils import (
<%static:require_module module_name="js/learner_dashboard/program_details_factory" class_name="ProgramDetailsFactory">
ProgramDetailsFactory({
programData: ${program_data | n, dump_js_escaped_json},
programListingUrl: '${program_listing_url | n, js_escaped_string}',
urls: ${urls | n, dump_js_escaped_json},
});
</%static:require_module>
</%block>
......
......@@ -29,7 +29,7 @@
<span class="crumb-separator fa fa-chevron-right" aria-hidden="true"></span>
</li>
<li class="crumb">
<a href="<%- programListingUrl %>" class="crumb-link"><%- gettext('Programs') %></a>
<a href="<%- urls.program_listing_url %>" class="crumb-link"><%- gettext('Programs') %></a>
<span class="crumb-separator fa fa-chevron-right" aria-hidden="true"></span>
</li>
<li class="crumb active">
......
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