Finish transition to catalog for program data
Updates Mako and Underscore templates as well as Backbone models and views so they work with catalog programs. Removes all remaining response munging from the back end. ECOM-4422
This commit is contained in:
@@ -122,15 +122,14 @@ import newrelic_custom_metrics
|
||||
# Note that this lives in LMS, so this dependency should be refactored.
|
||||
from notification_prefs.views import enable_notifications
|
||||
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs_with_type_logo
|
||||
from openedx.core.djangoapps.credit.email_utils import get_credit_provider_display_names, make_providers_strings
|
||||
from openedx.core.djangoapps.lang_pref import LANGUAGE_KEY
|
||||
from openedx.core.djangoapps.catalog.utils import munge_catalog_program
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.programs.utils import ProgramProgressMeter
|
||||
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
|
||||
from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
from openedx.core.djangoapps.user_api.preferences import api as preferences_api
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs_with_type_logo
|
||||
|
||||
|
||||
log = logging.getLogger("edx.student")
|
||||
@@ -670,9 +669,6 @@ def dashboard(request):
|
||||
meter = ProgramProgressMeter(user, enrollments=course_enrollments)
|
||||
inverted_programs = meter.invert_programs()
|
||||
|
||||
for program_list in inverted_programs.itervalues():
|
||||
program_list[:] = [munge_catalog_program(program) for program in program_list]
|
||||
|
||||
# Construct a dictionary of course mode information
|
||||
# used to render the course list. We re-use the course modes dict
|
||||
# we loaded earlier to avoid hitting the database.
|
||||
@@ -795,7 +791,7 @@ def dashboard(request):
|
||||
'order_history_list': order_history_list,
|
||||
'courses_requirements_not_met': courses_requirements_not_met,
|
||||
'nav_hidden': True,
|
||||
'programs_by_run': inverted_programs,
|
||||
'inverted_programs': inverted_programs,
|
||||
'show_program_listing': ProgramsApiConfig.current().show_program_listing,
|
||||
'disable_courseware_js': True,
|
||||
'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard,
|
||||
|
||||
@@ -16,7 +16,6 @@ import mock
|
||||
|
||||
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory, CourseFactory, CourseRunFactory
|
||||
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
|
||||
from openedx.core.djangoapps.catalog.utils import munge_catalog_program
|
||||
from openedx.core.djangoapps.credentials.tests.factories import UserCredential, ProgramCredential
|
||||
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
|
||||
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
|
||||
@@ -64,13 +63,7 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
|
||||
"""
|
||||
Helper function used to sort dictionaries representing programs.
|
||||
"""
|
||||
try:
|
||||
return program['title']
|
||||
except: # pylint: disable=bare-except
|
||||
# This is here temporarily because programs are still being munged
|
||||
# to look like they came from the programs service before going out
|
||||
# to the front end.
|
||||
return program['name']
|
||||
return program['title']
|
||||
|
||||
def credential_sort_key(self, credential):
|
||||
"""
|
||||
@@ -157,7 +150,7 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
|
||||
actual = sorted(actual, key=self.program_sort_key)
|
||||
|
||||
for index, actual_program in enumerate(actual):
|
||||
expected_program = munge_catalog_program(self.data[index])
|
||||
expected_program = self.data[index]
|
||||
self.assert_dict_contains_subset(actual_program, expected_program)
|
||||
|
||||
def test_program_discovery(self, mock_get_programs):
|
||||
|
||||
@@ -6,12 +6,11 @@ from django.views.decorators.http import require_GET
|
||||
|
||||
from edxmako.shortcuts import render_to_response
|
||||
from lms.djangoapps.learner_dashboard.utils import strip_course_id, FAKE_COURSE_KEY
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs, munge_catalog_program
|
||||
from openedx.core.djangoapps.catalog.utils import get_programs
|
||||
from openedx.core.djangoapps.credentials.utils import get_programs_credentials
|
||||
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
|
||||
from openedx.core.djangoapps.programs.utils import (
|
||||
get_program_marketing_url,
|
||||
munge_progress_map,
|
||||
ProgramProgressMeter,
|
||||
ProgramDataExtender,
|
||||
)
|
||||
@@ -27,16 +26,14 @@ def program_listing(request):
|
||||
raise Http404
|
||||
|
||||
meter = ProgramProgressMeter(request.user)
|
||||
engaged_programs = [munge_catalog_program(program) for program in meter.engaged_programs]
|
||||
progress = [munge_progress_map(progress_map) for progress_map in meter.progress]
|
||||
|
||||
context = {
|
||||
'credentials': get_programs_credentials(request.user),
|
||||
'disable_courseware_js': True,
|
||||
'marketing_url': get_program_marketing_url(programs_config),
|
||||
'nav_hidden': True,
|
||||
'programs': engaged_programs,
|
||||
'progress': progress,
|
||||
'programs': meter.engaged_programs,
|
||||
'progress': meter.progress,
|
||||
'show_program_listing': programs_config.show_program_listing,
|
||||
'uses_pattern_library': True,
|
||||
}
|
||||
@@ -56,7 +53,6 @@ def program_details(request, program_uuid):
|
||||
if not program_data:
|
||||
raise Http404
|
||||
|
||||
program_data = munge_catalog_program(program_data)
|
||||
program_data = ProgramDataExtender(program_data, request.user).extend()
|
||||
|
||||
urls = {
|
||||
|
||||
@@ -5,60 +5,93 @@
|
||||
'use strict';
|
||||
define([
|
||||
'backbone',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'edx-ui-toolkit/js/utils/date-utils'
|
||||
],
|
||||
function(Backbone, DateUtils) {
|
||||
function(Backbone, _, $, DateUtils) {
|
||||
return Backbone.Model.extend({
|
||||
initialize: function(data) {
|
||||
if (data) {
|
||||
this.context = data;
|
||||
this.setActiveRunMode(this.getRunMode(data.run_modes), data.user_preferences);
|
||||
this.setActiveCourseRun(this.getCourseRun(data.course_runs), data.user_preferences);
|
||||
}
|
||||
},
|
||||
|
||||
getUnselectedRunMode: function(runModes) {
|
||||
if (runModes && runModes.length > 0) {
|
||||
return {
|
||||
course_image_url: runModes[0].course_image_url,
|
||||
marketing_url: runModes[0].marketing_url,
|
||||
is_enrollment_open: runModes[0].is_enrollment_open
|
||||
};
|
||||
}
|
||||
getCourseRun: function(courseRuns) {
|
||||
var enrolledCourseRun = _.findWhere(courseRuns, {is_enrolled: true}),
|
||||
openEnrollmentCourseRuns = this.getEnrollableCourseRuns(),
|
||||
desiredCourseRun;
|
||||
|
||||
return {};
|
||||
},
|
||||
|
||||
getRunMode: function(runModes) {
|
||||
var enrolled_mode = _.findWhere(runModes, {is_enrolled: true}),
|
||||
openEnrollmentRunModes = this.getEnrollableRunModes(),
|
||||
desiredRunMode;
|
||||
// We populate our model by looking at the run modes.
|
||||
if (enrolled_mode) {
|
||||
// If the learner is already enrolled in a run mode, return that one.
|
||||
desiredRunMode = enrolled_mode;
|
||||
} else if (openEnrollmentRunModes.length > 0) {
|
||||
if (openEnrollmentRunModes.length === 1) {
|
||||
desiredRunMode = openEnrollmentRunModes[0];
|
||||
// We populate our model by looking at the course runs.
|
||||
if (enrolledCourseRun) {
|
||||
// If the learner is already enrolled in a course run, return that one.
|
||||
desiredCourseRun = enrolledCourseRun;
|
||||
} else if (openEnrollmentCourseRuns.length > 0) {
|
||||
if (openEnrollmentCourseRuns.length === 1) {
|
||||
desiredCourseRun = openEnrollmentCourseRuns[0];
|
||||
} else {
|
||||
desiredRunMode = this.getUnselectedRunMode(openEnrollmentRunModes);
|
||||
desiredCourseRun = this.getUnselectedCourseRun(openEnrollmentCourseRuns);
|
||||
}
|
||||
} else {
|
||||
desiredRunMode = this.getUnselectedRunMode(runModes);
|
||||
desiredCourseRun = this.getUnselectedCourseRun(courseRuns);
|
||||
}
|
||||
|
||||
return desiredRunMode;
|
||||
return desiredCourseRun;
|
||||
},
|
||||
|
||||
getEnrollableRunModes: function() {
|
||||
return _.where(this.context.run_modes, {
|
||||
getUnselectedCourseRun: function(courseRuns) {
|
||||
var unselectedRun = {},
|
||||
courseRun,
|
||||
courseImageUrl;
|
||||
|
||||
if (courseRuns && courseRuns.length > 0) {
|
||||
courseRun = courseRuns[0];
|
||||
|
||||
if (courseRun.hasOwnProperty('image')) {
|
||||
courseImageUrl = courseRun.image.src;
|
||||
} else {
|
||||
// The course_image_url property is attached by setActiveCourseRun.
|
||||
// If that hasn't been called, it won't be present yet.
|
||||
courseImageUrl = courseRun.course_image_url;
|
||||
}
|
||||
|
||||
$.extend(unselectedRun, {
|
||||
course_image_url: courseImageUrl,
|
||||
marketing_url: courseRun.marketing_url,
|
||||
is_enrollment_open: courseRun.is_enrollment_open
|
||||
});
|
||||
}
|
||||
|
||||
return unselectedRun;
|
||||
},
|
||||
|
||||
getEnrollableCourseRuns: function() {
|
||||
var rawCourseRuns,
|
||||
enrollableCourseRuns;
|
||||
|
||||
rawCourseRuns = _.where(this.context.course_runs, {
|
||||
is_enrollment_open: true,
|
||||
is_enrolled: false,
|
||||
is_course_ended: false
|
||||
});
|
||||
|
||||
// Deep copy to avoid mutating this.context.
|
||||
enrollableCourseRuns = $.extend(true, [], rawCourseRuns);
|
||||
|
||||
// These are raw course runs from the server. The start
|
||||
// dates are ISO-8601 formatted strings that need to be
|
||||
// prepped for display.
|
||||
_.each(enrollableCourseRuns, (function(courseRun) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
courseRun.start_date = this.formatDate(courseRun.start);
|
||||
}).bind(this));
|
||||
|
||||
return enrollableCourseRuns;
|
||||
},
|
||||
|
||||
getUpcomingRunModes: function() {
|
||||
return _.where(this.context.run_modes, {
|
||||
getUpcomingCourseRuns: function() {
|
||||
return _.where(this.context.course_runs, {
|
||||
is_enrollment_open: false,
|
||||
is_enrolled: false,
|
||||
is_course_ended: false
|
||||
@@ -82,51 +115,54 @@
|
||||
return DateUtils.localize(context);
|
||||
},
|
||||
|
||||
setActiveRunMode: function(runMode, userPreferences) {
|
||||
var startDateString;
|
||||
if (runMode) {
|
||||
if (runMode.advertised_start !== undefined && runMode.advertised_start !== 'None') {
|
||||
startDateString = runMode.advertised_start;
|
||||
setActiveCourseRun: function(courseRun, userPreferences) {
|
||||
var startDateString,
|
||||
courseImageUrl;
|
||||
|
||||
if (courseRun) {
|
||||
if (courseRun.advertised_start !== undefined && courseRun.advertised_start !== 'None') {
|
||||
startDateString = courseRun.advertised_start;
|
||||
} else {
|
||||
startDateString = this.formatDate(
|
||||
runMode.start_date,
|
||||
userPreferences
|
||||
);
|
||||
startDateString = this.formatDate(courseRun.start, userPreferences);
|
||||
}
|
||||
|
||||
if (courseRun.hasOwnProperty('image')) {
|
||||
courseImageUrl = courseRun.image.src;
|
||||
} else {
|
||||
courseImageUrl = courseRun.course_image_url;
|
||||
}
|
||||
|
||||
this.set({
|
||||
certificate_url: runMode.certificate_url,
|
||||
course_image_url: runMode.course_image_url || '',
|
||||
course_key: runMode.course_key,
|
||||
course_url: runMode.course_url || '',
|
||||
display_name: this.context.display_name,
|
||||
end_date: this.formatDate(
|
||||
runMode.end_date,
|
||||
userPreferences
|
||||
),
|
||||
enrollable_run_modes: this.getEnrollableRunModes(),
|
||||
is_course_ended: runMode.is_course_ended,
|
||||
is_enrolled: runMode.is_enrolled,
|
||||
is_enrollment_open: runMode.is_enrollment_open,
|
||||
key: this.context.key,
|
||||
marketing_url: runMode.marketing_url,
|
||||
mode_slug: runMode.mode_slug,
|
||||
run_key: runMode.run_key,
|
||||
certificate_url: courseRun.certificate_url,
|
||||
course_image_url: courseImageUrl || '',
|
||||
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_enrollment_open: courseRun.is_enrollment_open,
|
||||
course_key: this.context.key,
|
||||
marketing_url: courseRun.marketing_url,
|
||||
mode_slug: courseRun.type,
|
||||
start_date: startDateString,
|
||||
upcoming_run_modes: this.getUpcomingRunModes(),
|
||||
upgrade_url: runMode.upgrade_url
|
||||
upcoming_course_runs: this.getUpcomingCourseRuns(),
|
||||
upgrade_url: courseRun.upgrade_url
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setUnselected: function() {
|
||||
// Called to reset the model back to the unselected state.
|
||||
var unselectedMode = this.getUnselectedRunMode(this.get('enrollable_run_modes'));
|
||||
this.setActiveRunMode(unselectedMode);
|
||||
// Called to reset the model back to the unselected state.
|
||||
var unselectedCourseRun = this.getUnselectedCourseRun(this.get('enrollable_course_runs'));
|
||||
this.setActiveCourseRun(unselectedCourseRun);
|
||||
},
|
||||
|
||||
updateRun: function(runKey) {
|
||||
var selectedRun = _.findWhere(this.get('run_modes'), {run_key: runKey});
|
||||
if (selectedRun) {
|
||||
this.setActiveRunMode(selectedRun);
|
||||
updateCourseRun: function(courseRunKey) {
|
||||
var selectedCourseRun = _.findWhere(this.get('course_runs'), {key: courseRunKey});
|
||||
if (selectedCourseRun) {
|
||||
this.setActiveCourseRun(selectedCourseRun);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -11,17 +11,17 @@
|
||||
initialize: function(data) {
|
||||
if (data) {
|
||||
this.set({
|
||||
name: data.name,
|
||||
category: data.category,
|
||||
title: data.title,
|
||||
type: data.type,
|
||||
subtitle: data.subtitle,
|
||||
organizations: data.organizations,
|
||||
authoring_organizations: data.authoring_organizations,
|
||||
detailUrl: data.detail_url,
|
||||
smallBannerUrl: data.banner_image_urls.w348h116,
|
||||
mediumBannerUrl: data.banner_image_urls.w435h145,
|
||||
largeBannerUrl: data.banner_image_urls.w726h242,
|
||||
xsmallBannerUrl: data.banner_image['x-small'].url,
|
||||
smallBannerUrl: data.banner_image.small.url,
|
||||
mediumBannerUrl: data.banner_image.medium.url,
|
||||
breakpoints: {
|
||||
max: {
|
||||
tiny: '320px',
|
||||
xsmall: '320px',
|
||||
small: '540px',
|
||||
medium: '768px',
|
||||
large: '979px'
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
events: {
|
||||
'click .enroll-button': 'handleEnroll',
|
||||
'change .run-select': 'handleRunSelect'
|
||||
'change .run-select': 'handleCourseRunSelect'
|
||||
},
|
||||
|
||||
initialize: function(options) {
|
||||
@@ -45,12 +45,12 @@
|
||||
|
||||
handleEnroll: function() {
|
||||
// Enrollment click event handled here
|
||||
if (!this.model.get('course_key')) {
|
||||
if (!this.model.get('course_run_key')) {
|
||||
this.$('.select-error').css('visibility', 'visible');
|
||||
} else if (!this.model.get('is_enrolled')) {
|
||||
// actually enroll
|
||||
// Create the enrollment.
|
||||
this.enrollModel.save({
|
||||
course_id: this.model.get('course_key')
|
||||
course_id: this.model.get('course_run_key')
|
||||
}, {
|
||||
success: _.bind(this.enrollSuccess, this),
|
||||
error: _.bind(this.enrollError, this)
|
||||
@@ -58,24 +58,22 @@
|
||||
}
|
||||
},
|
||||
|
||||
handleRunSelect: function(event) {
|
||||
var runKey;
|
||||
if (event.target) {
|
||||
runKey = $(event.target).val();
|
||||
if (runKey) {
|
||||
this.model.updateRun(runKey);
|
||||
} else {
|
||||
// Set back the unselected states
|
||||
this.model.setUnselected();
|
||||
}
|
||||
handleCourseRunSelect: function(event) {
|
||||
var courseRunKey = $(event.target).val();
|
||||
|
||||
if (courseRunKey) {
|
||||
this.model.updateCourseRun(courseRunKey);
|
||||
} else {
|
||||
// Set back the unselected states
|
||||
this.model.setUnselected();
|
||||
}
|
||||
},
|
||||
|
||||
enrollSuccess: function() {
|
||||
var courseKey = this.model.get('course_key');
|
||||
var courseRunKey = this.model.get('course_run_key');
|
||||
if (this.trackSelectionUrl) {
|
||||
// Go to track selection page
|
||||
this.redirect(this.trackSelectionUrl + courseKey);
|
||||
// Go to track selection page
|
||||
this.redirect(this.trackSelectionUrl + courseRunKey);
|
||||
} else {
|
||||
this.model.set({
|
||||
is_enrolled: true
|
||||
@@ -98,7 +96,7 @@
|
||||
* 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'));
|
||||
this.redirect(this.trackSelectionUrl + this.model.get('course_run_key'));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
|
||||
attributes: function() {
|
||||
return {
|
||||
'aria-labelledby': 'program-' + this.model.get('id'),
|
||||
'aria-labelledby': 'program-' + this.model.get('uuid'),
|
||||
'role': 'group'
|
||||
};
|
||||
},
|
||||
@@ -33,14 +33,14 @@
|
||||
this.progressCollection = data.context.progressCollection;
|
||||
if (this.progressCollection) {
|
||||
this.progressModel = this.progressCollection.findWhere({
|
||||
id: this.model.get('id')
|
||||
uuid: this.model.get('uuid')
|
||||
});
|
||||
}
|
||||
this.render();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var orgList = _.map(this.model.get('organizations'), function(org) {
|
||||
var orgList = _.map(this.model.get('authoring_organizations'), function(org) {
|
||||
return gettext(org.key);
|
||||
}),
|
||||
data = $.extend(
|
||||
@@ -56,7 +56,7 @@
|
||||
postRender: function() {
|
||||
// Add describedby to parent only if progess is present
|
||||
if (this.progressModel) {
|
||||
this.$el.attr('aria-describedby', 'status-' + this.model.get('id'));
|
||||
this.$el.attr('aria-describedby', 'status-' + this.model.get('uuid'));
|
||||
}
|
||||
|
||||
if (navigator.userAgent.indexOf('MSIE') !== -1 ||
|
||||
@@ -73,19 +73,14 @@
|
||||
var progress = this.progressModel ? this.progressModel.toJSON() : false;
|
||||
|
||||
if (progress) {
|
||||
progress.total = {
|
||||
completed: progress.completed.length,
|
||||
in_progress: progress.in_progress.length,
|
||||
not_started: progress.not_started.length
|
||||
};
|
||||
|
||||
progress.total.courses = progress.total.completed +
|
||||
progress.total.in_progress +
|
||||
progress.total.not_started;
|
||||
progress.total = progress.completed +
|
||||
progress.in_progress +
|
||||
progress.not_started;
|
||||
|
||||
progress.percentage = {
|
||||
completed: this.getWidth(progress.total.completed, progress.total.courses),
|
||||
in_progress: this.getWidth(progress.total.in_progress, progress.total.courses)
|
||||
completed: this.getWidth(progress.completed, progress.total),
|
||||
in_progress: this.getWidth(progress.in_progress, progress.total)
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
this.options = options;
|
||||
this.programModel = new Backbone.Model(this.options.programData);
|
||||
this.courseCardCollection = new CourseCardCollection(
|
||||
this.programModel.get('course_codes'),
|
||||
this.programModel.get('courses'),
|
||||
this.options.userPreferences
|
||||
);
|
||||
this.render();
|
||||
|
||||
@@ -5,10 +5,9 @@ define([
|
||||
'js/learner_dashboard/collections/program_collection',
|
||||
'js/learner_dashboard/views/collection_list_view',
|
||||
'js/learner_dashboard/collections/program_progress_collection'
|
||||
], function(Backbone, $, ProgramCardView, ProgramCollection, CollectionListView,
|
||||
ProgressCollection) {
|
||||
], function(Backbone, $, ProgramCardView, ProgramCollection, CollectionListView, ProgressCollection) {
|
||||
'use strict';
|
||||
/* jslint maxlen: 500 */
|
||||
/* jslint maxlen: 500 */
|
||||
|
||||
describe('Collection List View', function() {
|
||||
var view = null,
|
||||
@@ -17,62 +16,90 @@ define([
|
||||
context = {
|
||||
programsData: [
|
||||
{
|
||||
category: 'xseries',
|
||||
status: 'active',
|
||||
subtitle: 'program 1',
|
||||
name: 'test program 1',
|
||||
organizations: [
|
||||
{
|
||||
display_name: 'edX',
|
||||
key: 'edx'
|
||||
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||
title: 'Food Security and Sustainability',
|
||||
subtitle: 'Learn how to feed all people in the world in a sustainable way.',
|
||||
type: 'XSeries',
|
||||
detail_url: 'https://www.edx.org/foo/bar',
|
||||
banner_image: {
|
||||
medium: {
|
||||
height: 242,
|
||||
width: 726,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg'
|
||||
},
|
||||
'x-small': {
|
||||
height: 116,
|
||||
width: 348,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg'
|
||||
},
|
||||
small: {
|
||||
height: 145,
|
||||
width: 435,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg'
|
||||
},
|
||||
large: {
|
||||
height: 480,
|
||||
width: 1440,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg'
|
||||
}
|
||||
],
|
||||
created: '2016-03-03T19:18:50.061136Z',
|
||||
modified: '2016-03-25T13:45:21.220732Z',
|
||||
marketing_slug: 'p_2?param=haha&test=b',
|
||||
id: 146,
|
||||
marketing_url: 'http://www.edx.org/xseries/p_2?param=haha&test=b',
|
||||
banner_image_urls: {
|
||||
w348h116: 'http://www.edx.org/images/org1/test1',
|
||||
w435h145: 'http://www.edx.org/images/org1/test2',
|
||||
w726h242: 'http://www.edx.org/images/org1/test3'
|
||||
}
|
||||
},
|
||||
authoring_organizations: [
|
||||
{
|
||||
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
||||
key: 'WageningenX',
|
||||
name: 'Wageningen University & Research'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
category: 'xseries',
|
||||
status: 'active',
|
||||
subtitle: 'fda',
|
||||
name: 'fda',
|
||||
organizations: [
|
||||
{
|
||||
display_name: 'edX',
|
||||
key: 'edx'
|
||||
uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
|
||||
title: 'edX Course Creator',
|
||||
subtitle: 'Become an expert in creating courses for the edX platform.',
|
||||
type: 'XSeries',
|
||||
detail_url: 'https://www.edx.org/foo/bar',
|
||||
banner_image: {
|
||||
medium: {
|
||||
height: 242,
|
||||
width: 726,
|
||||
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.medium.jpg'
|
||||
},
|
||||
'x-small': {
|
||||
height: 116,
|
||||
width: 348,
|
||||
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.x-small.jpg'
|
||||
},
|
||||
small: {
|
||||
height: 145,
|
||||
width: 435,
|
||||
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.small.jpg'
|
||||
},
|
||||
large: {
|
||||
height: 480,
|
||||
width: 1440,
|
||||
url: 'https://example.com/91d144d2-1bb1-4afe-90df-d5cff63fa6e2.large.jpg'
|
||||
}
|
||||
],
|
||||
created: '2016-03-09T14:30:41.484848Z',
|
||||
modified: '2016-03-09T14:30:52.840898Z',
|
||||
marketing_slug: 'gdaf',
|
||||
id: 147,
|
||||
marketing_url: 'http://www.edx.org/xseries/gdaf',
|
||||
banner_image_urls: {
|
||||
w348h116: 'http://www.edx.org/images/org2/test1',
|
||||
w435h145: 'http://www.edx.org/images/org2/test2',
|
||||
w726h242: 'http://www.edx.org/images/org2/test3'
|
||||
}
|
||||
},
|
||||
authoring_organizations: [
|
||||
{
|
||||
uuid: '4f8cb2c9-589b-4d1e-88c1-b01a02db3a9c',
|
||||
key: 'edX',
|
||||
name: 'edX'
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
userProgress: [
|
||||
{
|
||||
id: 146,
|
||||
completed: ['courses', 'the', 'user', 'completed'],
|
||||
in_progress: ['in', 'progress'],
|
||||
not_started: ['courses', 'not', 'yet', 'started']
|
||||
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||
completed: 4,
|
||||
in_progress: 2,
|
||||
not_started: 4
|
||||
},
|
||||
{
|
||||
id: 147,
|
||||
completed: ['Course 1'],
|
||||
in_progress: [],
|
||||
not_started: ['Course 2', 'Course 3', 'Course 4']
|
||||
uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
|
||||
completed: 1,
|
||||
in_progress: 0,
|
||||
not_started: 3
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -105,7 +132,8 @@ define([
|
||||
var $cards = view.$el.find('.program-card');
|
||||
expect($cards.length).toBe(2);
|
||||
$cards.each(function(index, el) {
|
||||
expect($(el).find('.title').html().trim()).toEqual(context.programsData[index].name);
|
||||
// eslint-disable-next-line newline-per-chained-call
|
||||
expect($(el).find('.title').html().trim()).toEqual(context.programsData[index].title);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -116,13 +144,14 @@ define([
|
||||
view = new CollectionListView({
|
||||
el: '.program-cards-container',
|
||||
childView: ProgramCardView,
|
||||
context: {'xseriesUrl': '/programs'},
|
||||
context: {},
|
||||
collection: programCollection
|
||||
});
|
||||
view.render();
|
||||
$cards = view.$el.find('.program-card');
|
||||
expect($cards.length).toBe(0);
|
||||
});
|
||||
|
||||
it('should have no title when title not provided', function() {
|
||||
var $title;
|
||||
setFixtures('<div class="test-container"><div class="program-cards-container"></div></div>');
|
||||
@@ -132,15 +161,18 @@ define([
|
||||
$title = view.$el.parent().find('.collection-title');
|
||||
expect($title.html()).not.toBeDefined();
|
||||
});
|
||||
|
||||
it('should display screen reader header when provided', function() {
|
||||
var $title, titleContext = {el: 'h2', title: 'list start'};
|
||||
var titleContext = {el: 'h2', title: 'list start'},
|
||||
$title;
|
||||
|
||||
view.remove();
|
||||
setFixtures('<div class="test-container"><div class="program-cards-container"></div></div>');
|
||||
programCollection = new ProgramCollection(context.programsData);
|
||||
view = new CollectionListView({
|
||||
el: '.program-cards-container',
|
||||
childView: ProgramCardView,
|
||||
context: {'xseriesUrl': '/programs'},
|
||||
context: context,
|
||||
collection: programCollection,
|
||||
titleContext: titleContext
|
||||
});
|
||||
|
||||
@@ -9,12 +9,14 @@ define([
|
||||
describe('Course Card View', function() {
|
||||
var view = null,
|
||||
courseCardModel,
|
||||
context,
|
||||
course,
|
||||
startDate = 'Feb 28, 2017',
|
||||
endDate = 'May 30, 2017',
|
||||
|
||||
setupView = function(data, isEnrolled) {
|
||||
var programData = $.extend({}, data);
|
||||
|
||||
programData.run_modes[0].is_enrolled = isEnrolled;
|
||||
programData.course_runs[0].is_enrolled = isEnrolled;
|
||||
setFixtures('<div class="course-card card"></div>');
|
||||
courseCardModel = new CourseCardModel(programData);
|
||||
view = new CourseCardView({
|
||||
@@ -24,48 +26,49 @@ define([
|
||||
|
||||
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.$('.header-img').attr('src')).toEqual(course.course_runs[0].image.src);
|
||||
expect(view.$('.course-details .course-title-link').text().trim()).toEqual(course.title);
|
||||
expect(view.$('.course-details .course-title-link').attr('href')).toEqual(
|
||||
context.run_modes[0].marketing_url
|
||||
course.course_runs[0].marketing_url
|
||||
);
|
||||
expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key);
|
||||
expect(view.$('.course-details .course-text .course-key').html()).toEqual(course.key);
|
||||
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
|
||||
context.run_modes[0].start_date + ' - ' + context.run_modes[0].end_date
|
||||
startDate + ' - ' + endDate
|
||||
);
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
// Redefine this data prior to each test case so that tests can't
|
||||
// break each other by modifying data copied by reference.
|
||||
context = {
|
||||
course_modes: [],
|
||||
display_name: 'Astrophysics: Exploring Exoplanets',
|
||||
key: 'ANU-ASTRO1x',
|
||||
organization: {
|
||||
display_name: 'Australian National University',
|
||||
key: 'ANUx'
|
||||
},
|
||||
run_modes: [{
|
||||
certificate_url: '',
|
||||
course_image_url: 'http://test.com/image1',
|
||||
course_key: 'course-v1:ANUx+ANU-ASTRO1x+3T2015',
|
||||
course_started: true,
|
||||
course_url: 'https://courses.example.com/courses/course-v1:edX+DemoX+Demo_Course',
|
||||
end_date: 'Jun 13, 2019',
|
||||
enrollment_open_date: 'Apr 1, 2016',
|
||||
is_course_ended: false,
|
||||
is_enrolled: true,
|
||||
is_enrollment_open: true,
|
||||
marketing_url: 'https://www.example.com/marketing/site',
|
||||
mode_slug: 'verified',
|
||||
run_key: '2T2016',
|
||||
start_date: 'Apr 25, 2016',
|
||||
upgrade_url: ''
|
||||
}]
|
||||
// NOTE: This data is redefined prior to each test case so that tests
|
||||
// can't break each other by modifying data copied by reference.
|
||||
course = {
|
||||
key: 'WageningenX+FFESx',
|
||||
uuid: '9f8562eb-f99b-45c7-b437-799fd0c15b6a',
|
||||
title: 'Systems thinking and environmental sustainability',
|
||||
course_runs: [
|
||||
{
|
||||
key: 'course-v1:WageningenX+FFESx+1T2017',
|
||||
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
||||
image: {
|
||||
src: 'https://example.com/9f8562eb-f99b-45c7-b437-799fd0c15b6a.jpg'
|
||||
},
|
||||
marketing_url: 'https://www.edx.org/course/food-security-sustainability',
|
||||
start: '2017-02-28T05:00:00Z',
|
||||
end: '2017-05-30T23:00:00Z',
|
||||
enrollment_start: '2017-01-18T00:00:00Z',
|
||||
enrollment_end: null,
|
||||
type: 'verified',
|
||||
certificate_url: '',
|
||||
course_url: 'https://courses.example.com/courses/course-v1:WageningenX+FFESx+1T2017',
|
||||
enrollment_open_date: 'Jan 18, 2016',
|
||||
is_course_ended: false,
|
||||
is_enrolled: true,
|
||||
is_enrollment_open: true,
|
||||
upgrade_url: ''
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
setupView(context, false);
|
||||
setupView(course, false);
|
||||
});
|
||||
|
||||
afterEach(function() {
|
||||
@@ -78,7 +81,7 @@ define([
|
||||
|
||||
it('should render the course card based on the data enrolled', function() {
|
||||
view.remove();
|
||||
setupView(context, true);
|
||||
setupView(course, true);
|
||||
validateCourseInfoDisplay();
|
||||
});
|
||||
|
||||
@@ -94,11 +97,11 @@ define([
|
||||
});
|
||||
|
||||
it('should show the course advertised start date', function() {
|
||||
var advertisedStart = 'This is an advertised start';
|
||||
context.run_modes[0].advertised_start = advertisedStart;
|
||||
setupView(context, false);
|
||||
var advertisedStart = 'A long time ago...';
|
||||
course.course_runs[0].advertised_start = advertisedStart;
|
||||
setupView(course, false);
|
||||
expect(view.$('.course-details .course-text .run-period').html()).toEqual(
|
||||
advertisedStart + ' - ' + context.run_modes[0].end_date
|
||||
advertisedStart + ' - ' + endDate
|
||||
);
|
||||
});
|
||||
|
||||
@@ -108,8 +111,8 @@ define([
|
||||
expect(view.$('.certificate-status').length).toEqual(0);
|
||||
view.remove();
|
||||
|
||||
context.run_modes[0].certificate_url = certUrl;
|
||||
setupView(context, false);
|
||||
course.course_runs[0].certificate_url = certUrl;
|
||||
setupView(course, false);
|
||||
expect(view.$('.certificate-status').length).toEqual(1);
|
||||
expect(view.$('.certificate-status .cta-secondary').attr('href')).toEqual(certUrl);
|
||||
});
|
||||
@@ -120,53 +123,53 @@ define([
|
||||
expect(view.$('.upgrade-message').length).toEqual(0);
|
||||
view.remove();
|
||||
|
||||
context.run_modes[0].upgrade_url = upgradeUrl;
|
||||
setupView(context, false);
|
||||
course.course_runs[0].upgrade_url = upgradeUrl;
|
||||
setupView(course, false);
|
||||
expect(view.$('.upgrade-message').length).toEqual(1);
|
||||
expect(view.$('.upgrade-message .cta-primary').attr('href')).toEqual(upgradeUrl);
|
||||
});
|
||||
|
||||
it('should not show both the upgrade message and certificate status sections', function() {
|
||||
// Verify that no empty elements are left in the DOM.
|
||||
context.run_modes[0].upgrade_url = '';
|
||||
context.run_modes[0].certificate_url = '';
|
||||
setupView(context, false);
|
||||
// Verify that no empty elements are left in the DOM.
|
||||
course.course_runs[0].upgrade_url = '';
|
||||
course.course_runs[0].certificate_url = '';
|
||||
setupView(course, false);
|
||||
expect(view.$('.upgrade-message').length).toEqual(0);
|
||||
expect(view.$('.certificate-status').length).toEqual(0);
|
||||
view.remove();
|
||||
|
||||
// Verify that the upgrade message takes priority.
|
||||
context.run_modes[0].upgrade_url = '/path/to/upgrade';
|
||||
context.run_modes[0].certificate_url = '/path/to/certificate';
|
||||
setupView(context, false);
|
||||
// Verify that the upgrade message takes priority.
|
||||
course.course_runs[0].upgrade_url = '/path/to/upgrade';
|
||||
course.course_runs[0].certificate_url = '/path/to/certificate';
|
||||
setupView(course, false);
|
||||
expect(view.$('.upgrade-message').length).toEqual(1);
|
||||
expect(view.$('.certificate-status').length).toEqual(0);
|
||||
});
|
||||
|
||||
it('should show a message if an there is an upcoming course run', function() {
|
||||
context.run_modes[0].is_enrollment_open = false;
|
||||
course.course_runs[0].is_enrollment_open = false;
|
||||
|
||||
setupView(context, false);
|
||||
setupView(course, false);
|
||||
|
||||
expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url);
|
||||
expect(view.$('.course-details .course-title').text().trim()).toEqual(context.display_name);
|
||||
expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key);
|
||||
expect(view.$('.header-img').attr('src')).toEqual(course.course_runs[0].image.src);
|
||||
expect(view.$('.course-details .course-title').text().trim()).toEqual(course.title);
|
||||
expect(view.$('.course-details .course-text .course-key').html()).toEqual(course.key);
|
||||
expect(view.$('.course-details .course-text .run-period').length).toBe(0);
|
||||
expect(view.$('.no-action-message').text().trim()).toBe('Coming Soon');
|
||||
expect(view.$('.enrollment-open-date').text().trim()).toEqual(
|
||||
context.run_modes[0].enrollment_open_date
|
||||
);
|
||||
course.course_runs[0].enrollment_open_date
|
||||
);
|
||||
});
|
||||
|
||||
it('should show a message if there are no known upcoming course runs', function() {
|
||||
context.run_modes[0].is_enrollment_open = false;
|
||||
context.run_modes[0].is_course_ended = true;
|
||||
it('should show a message if there are no upcoming course runs', function() {
|
||||
course.course_runs[0].is_enrollment_open = false;
|
||||
course.course_runs[0].is_course_ended = true;
|
||||
|
||||
setupView(context, false);
|
||||
setupView(course, false);
|
||||
|
||||
expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url);
|
||||
expect(view.$('.course-details .course-title').text().trim()).toEqual(context.display_name);
|
||||
expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key);
|
||||
expect(view.$('.header-img').attr('src')).toEqual(course.course_runs[0].image.src);
|
||||
expect(view.$('.course-details .course-title').text().trim()).toEqual(course.title);
|
||||
expect(view.$('.course-details .course-text .course-key').html()).toEqual(course.key);
|
||||
expect(view.$('.course-details .course-text .run-period').length).toBe(0);
|
||||
expect(view.$('.no-action-message').text().trim()).toBe('Not Currently Available');
|
||||
expect(view.$('.enrollment-opens').length).toEqual(0);
|
||||
@@ -174,23 +177,23 @@ define([
|
||||
|
||||
it('should link to the marketing site when a URL is available', function() {
|
||||
$.each(['.course-image-link', '.course-title-link'], function(index, selector) {
|
||||
expect(view.$(selector).attr('href')).toEqual(context.run_modes[0].marketing_url);
|
||||
expect(view.$(selector).attr('href')).toEqual(course.course_runs[0].marketing_url);
|
||||
});
|
||||
});
|
||||
|
||||
it('should link to the course home when no marketing URL is available', function() {
|
||||
context.run_modes[0].marketing_url = null;
|
||||
setupView(context, false);
|
||||
course.course_runs[0].marketing_url = null;
|
||||
setupView(course, false);
|
||||
|
||||
$.each(['.course-image-link', '.course-title-link'], function(index, selector) {
|
||||
expect(view.$(selector).attr('href')).toEqual(context.run_modes[0].course_url);
|
||||
expect(view.$(selector).attr('href')).toEqual(course.course_runs[0].course_url);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not link to the marketing site or the course home if neither URL is available', function() {
|
||||
context.run_modes[0].marketing_url = null;
|
||||
context.run_modes[0].course_url = null;
|
||||
setupView(context, false);
|
||||
course.course_runs[0].marketing_url = null;
|
||||
course.course_runs[0].course_url = null;
|
||||
setupView(course, false);
|
||||
|
||||
$.each(['.course-image-link', '.course-title-link'], function(index, selector) {
|
||||
expect(view.$(selector).length).toEqual(0);
|
||||
|
||||
@@ -13,75 +13,102 @@ define([
|
||||
courseEnrollModel,
|
||||
urlModel,
|
||||
setupView,
|
||||
singleRunModeList,
|
||||
multiRunModeList,
|
||||
context = {
|
||||
display_name: 'Edx Demo course',
|
||||
key: 'edX+DemoX+Demo_Course',
|
||||
organization: {
|
||||
display_name: 'edx.org',
|
||||
key: 'edX'
|
||||
}
|
||||
singleCourseRunList,
|
||||
multiCourseRunList,
|
||||
course = {
|
||||
key: 'WageningenX+FFESx',
|
||||
uuid: '9f8562eb-f99b-45c7-b437-799fd0c15b6a',
|
||||
title: 'Systems thinking and environmental sustainability',
|
||||
owners: [
|
||||
{
|
||||
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
||||
key: 'WageningenX',
|
||||
name: 'Wageningen University & Research'
|
||||
}
|
||||
]
|
||||
},
|
||||
urls = {
|
||||
dashboard_url: '/dashboard',
|
||||
id_verification_url: '/verify_student/start_flow/',
|
||||
commerce_api_url: '/commerce',
|
||||
track_selection_url: '/select_track/course/'
|
||||
};
|
||||
|
||||
beforeEach(function() {
|
||||
// Redefine this data prior to each test case so that tests can't
|
||||
// break each other by modifying data copied by reference.
|
||||
singleRunModeList = [{
|
||||
start_date: 'Apr 25, 2016',
|
||||
end_date: 'Jun 13, 2016',
|
||||
course_key: 'course-v1:course-v1:edX+DemoX+Demo_Course',
|
||||
course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info',
|
||||
course_image_url: 'http://test.com/image1',
|
||||
marketing_url: 'http://test.com/image2',
|
||||
// NOTE: This data is redefined prior to each test case so that tests
|
||||
// can't break each other by modifying data copied by reference.
|
||||
singleCourseRunList = [{
|
||||
key: 'course-v1:WageningenX+FFESx+1T2017',
|
||||
uuid: '2f2edf03-79e6-4e39-aef0-65436a6ee344',
|
||||
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
||||
image: {
|
||||
src: 'https://example.com/2f2edf03-79e6-4e39-aef0-65436a6ee344.jpg'
|
||||
},
|
||||
marketing_url: 'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-ffesx',
|
||||
start: '2017-02-28T05:00:00Z',
|
||||
end: '2017-05-30T23:00:00Z',
|
||||
enrollment_start: '2017-01-18T00:00:00Z',
|
||||
enrollment_end: null,
|
||||
type: 'verified',
|
||||
certificate_url: '',
|
||||
course_url: 'https://courses.example.com/courses/course-v1:edX+DemoX+Demo_Course',
|
||||
enrollment_open_date: 'Jan 18, 2016',
|
||||
is_course_ended: false,
|
||||
mode_slug: 'audit',
|
||||
run_key: '2T2016',
|
||||
is_enrolled: false,
|
||||
is_enrollment_open: true
|
||||
is_enrollment_open: true,
|
||||
upgrade_url: ''
|
||||
}];
|
||||
|
||||
multiRunModeList = [{
|
||||
start_date: 'May 21, 2015',
|
||||
end_date: 'Sep 21, 2015',
|
||||
course_key: 'course-v1:course-v1:edX+DemoX+Demo_Course',
|
||||
course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info',
|
||||
course_image_url: 'http://test.com/run_2_image_1',
|
||||
marketing_url: 'http://test.com/run_2_image_2',
|
||||
mode_slug: 'verified',
|
||||
multiCourseRunList = [{
|
||||
key: 'course-v1:WageningenX+FFESx+2T2016',
|
||||
uuid: '9bbb7844-4848-44ab-8e20-0be6604886e9',
|
||||
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
||||
image: {
|
||||
src: 'https://example.com/9bbb7844-4848-44ab-8e20-0be6604886e9.jpg'
|
||||
},
|
||||
short_description: 'Learn how to apply systems thinking to improve food production systems.',
|
||||
marketing_url: 'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-stesx',
|
||||
start: '2016-09-08T04:00:00Z',
|
||||
end: '2016-11-11T00:00:00Z',
|
||||
enrollment_start: null,
|
||||
enrollment_end: null,
|
||||
pacing_type: 'instructor_paced',
|
||||
type: 'verified',
|
||||
certificate_url: '',
|
||||
course_url: 'https://courses.example.com/courses/course-v1:WageningenX+FFESx+2T2016',
|
||||
enrollment_open_date: 'Jan 18, 2016',
|
||||
is_course_ended: false,
|
||||
run_key: '1T2015',
|
||||
is_enrolled: false,
|
||||
is_enrollment_open: true
|
||||
}, {
|
||||
start_date: 'Sep 22, 2015',
|
||||
end_date: 'Dec 28, 2015',
|
||||
course_key: 'course-v1:course-v1:edX+DemoX+Demo_Course',
|
||||
course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info',
|
||||
course_image_url: 'http://test.com/run_3_image_1',
|
||||
marketing_url: 'http://test.com/run_3_image_2',
|
||||
key: 'course-v1:WageningenX+FFESx+1T2017',
|
||||
uuid: '2f2edf03-79e6-4e39-aef0-65436a6ee344',
|
||||
title: 'Food Security and Sustainability: Systems thinking and environmental sustainability',
|
||||
image: {
|
||||
src: 'https://example.com/2f2edf03-79e6-4e39-aef0-65436a6ee344.jpg'
|
||||
},
|
||||
marketing_url: 'https://www.edx.org/course/food-security-sustainability-systems-wageningenx-ffesx',
|
||||
start: '2017-02-28T05:00:00Z',
|
||||
end: '2017-05-30T23:00:00Z',
|
||||
enrollment_start: '2017-01-18T00:00:00Z',
|
||||
enrollment_end: null,
|
||||
type: 'verified',
|
||||
certificate_url: '',
|
||||
course_url: 'https://courses.example.com/courses/course-v1:WageningenX+FFESx+1T2017',
|
||||
enrollment_open_date: 'Jan 18, 2016',
|
||||
is_course_ended: false,
|
||||
mode_slug: 'verified',
|
||||
run_key: '2T2015',
|
||||
is_enrolled: false,
|
||||
is_enrollment_open: true
|
||||
}];
|
||||
});
|
||||
|
||||
setupView = function(runModes, urls) {
|
||||
context.run_modes = runModes;
|
||||
setupView = function(courseRuns, urlMap) {
|
||||
course.course_runs = courseRuns;
|
||||
setFixtures('<div class="course-actions"></div>');
|
||||
courseCardModel = new CourseCardModel(context);
|
||||
courseCardModel = new CourseCardModel(course);
|
||||
courseEnrollModel = new CourseEnrollModel({}, {
|
||||
courseId: courseCardModel.get('course_key')
|
||||
courseId: courseCardModel.get('course_run_key')
|
||||
});
|
||||
if (urls) {
|
||||
urlModel = new Backbone.Model(urls);
|
||||
if (urlMap) {
|
||||
urlModel = new Backbone.Model(urlMap);
|
||||
}
|
||||
view = new CourseEnrollView({
|
||||
$parentEl: $('.course-actions'),
|
||||
@@ -99,143 +126,183 @@ define([
|
||||
});
|
||||
|
||||
it('should exist', function() {
|
||||
setupView(singleRunModeList);
|
||||
setupView(singleCourseRunList);
|
||||
expect(view).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render the course enroll view based on not enrolled data', function() {
|
||||
setupView(singleRunModeList);
|
||||
expect(view.$('.enrollment-info').html().trim()).toEqual('not enrolled');
|
||||
it('should render the course enroll view when not enrolled', function() {
|
||||
setupView(singleCourseRunList);
|
||||
|
||||
expect(view.$('.enrollment-info').html().trim()).toEqual('Not Enrolled');
|
||||
expect(view.$('.enroll-button').text().trim()).toEqual('Enroll Now');
|
||||
expect(view.$('.run-select').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should render the course enroll view based on enrolled data', function() {
|
||||
singleRunModeList[0].is_enrolled = true;
|
||||
it('should render the course enroll view when enrolled', function() {
|
||||
singleCourseRunList[0].is_enrolled = true;
|
||||
|
||||
setupView(singleRunModeList);
|
||||
setupView(singleCourseRunList);
|
||||
|
||||
expect(view.$('.enrollment-info').html().trim()).toEqual('enrolled');
|
||||
expect(view.$('.view-course-link').attr('href')).toEqual(context.run_modes[0].course_url);
|
||||
expect(view.$('.view-course-link').attr('href')).toEqual(course.course_runs[0].course_url);
|
||||
expect(view.$('.view-course-link').text().trim()).toEqual('View Course');
|
||||
expect(view.$('.run-select').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should allow the learner to view an archived course', function() {
|
||||
// Regression test for ECOM-4974.
|
||||
singleRunModeList[0].is_enrolled = true;
|
||||
singleRunModeList[0].is_enrollment_open = false;
|
||||
singleRunModeList[0].is_course_ended = true;
|
||||
// Regression test for ECOM-4974.
|
||||
singleCourseRunList[0].is_enrolled = true;
|
||||
singleCourseRunList[0].is_enrollment_open = false;
|
||||
singleCourseRunList[0].is_course_ended = true;
|
||||
|
||||
setupView(singleRunModeList);
|
||||
setupView(singleCourseRunList);
|
||||
|
||||
expect(view.$('.view-course-link').text().trim()).toEqual('View Archived Course');
|
||||
});
|
||||
|
||||
it('should not render anything if run modes is empty', function() {
|
||||
it('should not render anything if course runs are empty', function() {
|
||||
setupView([]);
|
||||
|
||||
expect(view.$('.enrollment-info').length).toBe(0);
|
||||
expect(view.$('.run-select').length).toBe(0);
|
||||
expect(view.$('.enroll-button').length).toBe(0);
|
||||
});
|
||||
|
||||
it('should render run selection drop down if mulitple run available', function() {
|
||||
setupView(multiRunModeList);
|
||||
it('should render run selection dropdown if multiple course runs are available', function() {
|
||||
setupView(multiCourseRunList);
|
||||
|
||||
expect(view.$('.run-select').length).toBe(1);
|
||||
expect(view.$('.run-select').val()).toEqual('');
|
||||
expect(view.$('.run-select option').length).toBe(3);
|
||||
});
|
||||
|
||||
it('should switch run context if dropdown selection changed', function() {
|
||||
setupView(multiRunModeList);
|
||||
spyOn(courseCardModel, 'updateRun').and.callThrough();
|
||||
it('should switch course run context if an option is selected from the dropdown', function() {
|
||||
setupView(multiCourseRunList);
|
||||
|
||||
spyOn(courseCardModel, 'updateCourseRun').and.callThrough();
|
||||
|
||||
expect(view.$('.run-select').val()).toEqual('');
|
||||
view.$('.run-select').val(multiRunModeList[1].run_key);
|
||||
|
||||
view.$('.run-select').val(multiCourseRunList[1].key);
|
||||
view.$('.run-select').trigger('change');
|
||||
expect(view.$('.run-select').val()).toEqual(multiRunModeList[1].run_key);
|
||||
expect(courseCardModel.updateRun)
|
||||
.toHaveBeenCalledWith(multiRunModeList[1].run_key);
|
||||
expect(courseCardModel.get('run_key')).toEqual(multiRunModeList[1].run_key);
|
||||
|
||||
expect(view.$('.run-select').val()).toEqual(multiCourseRunList[1].key);
|
||||
expect(courseCardModel.updateCourseRun)
|
||||
.toHaveBeenCalledWith(multiCourseRunList[1].key);
|
||||
expect(courseCardModel.get('course_key')).toEqual(course.key);
|
||||
});
|
||||
|
||||
it('should enroll learner when enroll button clicked', function() {
|
||||
setupView(singleRunModeList);
|
||||
it('should enroll learner when enroll button is clicked with one course run available', function() {
|
||||
setupView(singleCourseRunList);
|
||||
|
||||
expect(view.$('.enroll-button').length).toBe(1);
|
||||
|
||||
spyOn(courseEnrollModel, 'save');
|
||||
|
||||
view.$('.enroll-button').click();
|
||||
|
||||
expect(courseEnrollModel.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should enroll learner into the updated run with button click', function() {
|
||||
setupView(multiRunModeList);
|
||||
it('should enroll learner when enroll button is clicked with multiple course runs available', function() {
|
||||
setupView(multiCourseRunList);
|
||||
|
||||
spyOn(courseEnrollModel, 'save');
|
||||
view.$('.run-select').val(multiRunModeList[1].run_key);
|
||||
|
||||
view.$('.run-select').val(multiCourseRunList[1].key);
|
||||
view.$('.run-select').trigger('change');
|
||||
view.$('.enroll-button').click();
|
||||
|
||||
expect(courseEnrollModel.save).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to trackSelectionUrl when enrollment success for audit track', function() {
|
||||
singleRunModeList[0].is_enrolled = false;
|
||||
singleRunModeList[0].mode_slug = 'audit';
|
||||
setupView(singleRunModeList, urls);
|
||||
it('should redirect to track selection when audit enrollment succeeds', function() {
|
||||
singleCourseRunList[0].is_enrolled = false;
|
||||
singleCourseRunList[0].mode_slug = 'audit';
|
||||
|
||||
setupView(singleCourseRunList, urls);
|
||||
|
||||
expect(view.$('.enroll-button').length).toBe(1);
|
||||
expect(view.trackSelectionUrl).toBeDefined();
|
||||
|
||||
spyOn(view, 'redirect');
|
||||
|
||||
view.enrollSuccess();
|
||||
|
||||
expect(view.redirect).toHaveBeenCalledWith(
|
||||
view.trackSelectionUrl + courseCardModel.get('course_key'));
|
||||
view.trackSelectionUrl + courseCardModel.get('course_run_key'));
|
||||
});
|
||||
|
||||
it('should redirect to track selection when enrollment in an unspecified mode is attempted', function() {
|
||||
singleCourseRunList[0].is_enrolled = false;
|
||||
singleCourseRunList[0].mode_slug = null;
|
||||
|
||||
setupView(singleCourseRunList, urls);
|
||||
|
||||
it('should redirect when enrollment success for no track', function() {
|
||||
singleRunModeList[0].is_enrolled = false;
|
||||
singleRunModeList[0].mode_slug = null;
|
||||
setupView(singleRunModeList, urls);
|
||||
expect(view.$('.enroll-button').length).toBe(1);
|
||||
expect(view.trackSelectionUrl).toBeDefined();
|
||||
|
||||
spyOn(view, 'redirect');
|
||||
|
||||
view.enrollSuccess();
|
||||
|
||||
expect(view.redirect).toHaveBeenCalledWith(
|
||||
view.trackSelectionUrl + courseCardModel.get('course_key'));
|
||||
view.trackSelectionUrl + courseCardModel.get('course_run_key')
|
||||
);
|
||||
});
|
||||
|
||||
it('should not redirect when urls not provided', function() {
|
||||
singleRunModeList[0].is_enrolled = false;
|
||||
singleRunModeList[0].mode_slug = 'verified';
|
||||
setupView(singleRunModeList);
|
||||
it('should not redirect when urls are not provided', function() {
|
||||
singleCourseRunList[0].is_enrolled = false;
|
||||
singleCourseRunList[0].mode_slug = 'verified';
|
||||
|
||||
setupView(singleCourseRunList);
|
||||
|
||||
expect(view.$('.enroll-button').length).toBe(1);
|
||||
expect(view.verificationUrl).not.toBeDefined();
|
||||
expect(view.dashboardUrl).not.toBeDefined();
|
||||
expect(view.trackSelectionUrl).not.toBeDefined();
|
||||
|
||||
spyOn(view, 'redirect');
|
||||
|
||||
view.enrollSuccess();
|
||||
|
||||
expect(view.redirect).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should redirect to track selection on error', function() {
|
||||
setupView(singleRunModeList, urls);
|
||||
setupView(singleCourseRunList, urls);
|
||||
|
||||
expect(view.$('.enroll-button').length).toBe(1);
|
||||
expect(view.trackSelectionUrl).toBeDefined();
|
||||
|
||||
spyOn(view, 'redirect');
|
||||
|
||||
view.enrollError(courseEnrollModel, {status: 500});
|
||||
expect(view.redirect).toHaveBeenCalledWith(
|
||||
view.trackSelectionUrl + courseCardModel.get('course_key'));
|
||||
view.trackSelectionUrl + courseCardModel.get('course_run_key')
|
||||
);
|
||||
});
|
||||
|
||||
it('should redirect to login on 403 error', function() {
|
||||
var response = {
|
||||
status: 403,
|
||||
responseJSON: {
|
||||
user_message_url: 'test_url/haha'
|
||||
}};
|
||||
setupView(singleRunModeList, urls);
|
||||
user_message_url: 'redirect/to/this'
|
||||
}
|
||||
};
|
||||
|
||||
setupView(singleCourseRunList, urls);
|
||||
|
||||
expect(view.$('.enroll-button').length).toBe(1);
|
||||
expect(view.trackSelectionUrl).toBeDefined();
|
||||
|
||||
spyOn(view, 'redirect');
|
||||
|
||||
view.enrollError(courseEnrollModel, response);
|
||||
|
||||
expect(view.redirect).toHaveBeenCalledWith(
|
||||
response.responseJSON.user_message_url);
|
||||
response.responseJSON.user_message_url
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,58 +1,73 @@
|
||||
define([
|
||||
'backbone',
|
||||
'underscore',
|
||||
'jquery',
|
||||
'js/learner_dashboard/collections/program_progress_collection',
|
||||
'js/learner_dashboard/models/program_model',
|
||||
'js/learner_dashboard/views/program_card_view'
|
||||
], function(Backbone, $, ProgressCollection, ProgramModel, ProgramCardView) {
|
||||
], function(Backbone, _, $, ProgressCollection, ProgramModel, ProgramCardView) {
|
||||
'use strict';
|
||||
/* jslint maxlen: 500 */
|
||||
/* jslint maxlen: 500 */
|
||||
|
||||
describe('Program card View', function() {
|
||||
var view = null,
|
||||
programModel,
|
||||
program = {
|
||||
category: 'FooBar',
|
||||
status: 'active',
|
||||
subtitle: 'program 1',
|
||||
name: 'test program 1',
|
||||
organizations: [
|
||||
{
|
||||
display_name: 'edX',
|
||||
key: 'edx'
|
||||
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||
title: 'Food Security and Sustainability',
|
||||
subtitle: 'Learn how to feed all people in the world in a sustainable way.',
|
||||
type: 'XSeries',
|
||||
detail_url: 'https://www.edx.org/foo/bar',
|
||||
banner_image: {
|
||||
medium: {
|
||||
height: 242,
|
||||
width: 726,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg'
|
||||
},
|
||||
'x-small': {
|
||||
height: 116,
|
||||
width: 348,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg'
|
||||
},
|
||||
small: {
|
||||
height: 145,
|
||||
width: 435,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg'
|
||||
},
|
||||
large: {
|
||||
height: 480,
|
||||
width: 1440,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg'
|
||||
}
|
||||
],
|
||||
created: '2016-03-03T19:18:50.061136Z',
|
||||
modified: '2016-03-25T13:45:21.220732Z',
|
||||
marketing_slug: 'p_2?param=haha&test=b',
|
||||
id: 146,
|
||||
detail_url: 'http://courses.edx.org/dashboard/programs/1/foo',
|
||||
banner_image_urls: {
|
||||
w348h116: 'http://www.edx.org/images/test1',
|
||||
w435h145: 'http://www.edx.org/images/test2',
|
||||
w726h242: 'http://www.edx.org/images/test3'
|
||||
}
|
||||
},
|
||||
authoring_organizations: [
|
||||
{
|
||||
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
||||
key: 'WageningenX',
|
||||
name: 'Wageningen University & Research'
|
||||
}
|
||||
]
|
||||
},
|
||||
userProgress = [
|
||||
{
|
||||
id: 146,
|
||||
completed: ['courses', 'the', 'user', 'completed'],
|
||||
in_progress: ['in', 'progress'],
|
||||
not_started: ['courses', 'not', 'yet', 'started']
|
||||
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||
completed: 4,
|
||||
in_progress: 2,
|
||||
not_started: 4
|
||||
},
|
||||
{
|
||||
id: 147,
|
||||
completed: ['Course 1'],
|
||||
in_progress: [],
|
||||
not_started: ['Course 2', 'Course 3', 'Course 4']
|
||||
uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
|
||||
completed: 1,
|
||||
in_progress: 0,
|
||||
not_started: 3
|
||||
}
|
||||
],
|
||||
progressCollection = new ProgressCollection(),
|
||||
cardRenders = function($card) {
|
||||
expect($card).toBeDefined();
|
||||
expect($card.find('.title').html().trim()).toEqual(program.name);
|
||||
expect($card.find('.category span').html().trim()).toEqual(program.category);
|
||||
expect($card.find('.organization').html().trim()).toEqual(program.organizations[0].key);
|
||||
expect($card.find('.title').html().trim()).toEqual(program.title);
|
||||
expect($card.find('.category span').html().trim()).toEqual(program.type);
|
||||
expect($card.find('.organization').html().trim()).toEqual(program.authoring_organizations[0].key);
|
||||
expect($card.find('.card-link').attr('href')).toEqual(program.detail_url);
|
||||
};
|
||||
|
||||
@@ -87,12 +102,16 @@ define([
|
||||
});
|
||||
|
||||
it('should handle exceptions from reEvaluatePicture', function() {
|
||||
var message = 'Picturefill had exceptions';
|
||||
|
||||
spyOn(view, 'reEvaluatePicture').and.callFake(function() {
|
||||
throw {name: 'Picturefill had exceptions'};
|
||||
var error = {name: message};
|
||||
|
||||
throw error;
|
||||
});
|
||||
view.reLoadBannerImage();
|
||||
expect(view.reEvaluatePicture).toHaveBeenCalled();
|
||||
expect(view.reLoadBannerImage).not.toThrow('Picturefill had exceptions');
|
||||
expect(view.reLoadBannerImage).not.toThrow(message);
|
||||
});
|
||||
|
||||
it('should calculate the correct percentages for progress bars', function() {
|
||||
@@ -101,11 +120,12 @@ define([
|
||||
});
|
||||
|
||||
it('should display the correct completed courses message', function() {
|
||||
var program = _.findWhere(userProgress, {id: 146}),
|
||||
completed = program.completed.length,
|
||||
total = completed + program.in_progress.length + program.not_started.length;
|
||||
var programProgress = _.findWhere(userProgress, {uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8'}),
|
||||
completed = programProgress.completed,
|
||||
total = completed + programProgress.in_progress + programProgress.not_started;
|
||||
|
||||
expect(view.$('.certificate-status .status-text').not('.secondary').html()).toEqual('You have earned certificates in ' + completed + ' of the ' + total + ' courses so far.');
|
||||
expect(view.$('.certificate-status .status-text').not('.secondary').html())
|
||||
.toEqual('You have earned certificates in ' + completed + ' of the ' + total + ' courses so far.');
|
||||
});
|
||||
|
||||
it('should render cards if there is no progressData', function() {
|
||||
|
||||
@@ -12,23 +12,42 @@ define([
|
||||
program_listing_url: '/dashboard/programs'
|
||||
},
|
||||
programData: {
|
||||
uuid: '12-ab',
|
||||
name: 'Astrophysics',
|
||||
subtitle: 'Learn contemporary astrophysics from the leaders in the field.',
|
||||
category: 'xseries',
|
||||
organizations: [
|
||||
{
|
||||
display_name: 'Australian National University',
|
||||
img: 'common/test/data/static/picture1.jpg',
|
||||
key: 'ANUx'
|
||||
uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
|
||||
title: 'Food Security and Sustainability',
|
||||
subtitle: 'Learn how to feed all people in the world in a sustainable way.',
|
||||
type: 'XSeries',
|
||||
detail_url: 'https://www.edx.org/foo/bar',
|
||||
banner_image: {
|
||||
medium: {
|
||||
height: 242,
|
||||
width: 726,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.medium.jpg'
|
||||
},
|
||||
'x-small': {
|
||||
height: 116,
|
||||
width: 348,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.x-small.jpg'
|
||||
},
|
||||
small: {
|
||||
height: 145,
|
||||
width: 435,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.small.jpg'
|
||||
},
|
||||
large: {
|
||||
height: 480,
|
||||
width: 1440,
|
||||
url: 'https://example.com/a87e5eac-3c93-45a1-a8e1-4c79ca8401c8.large.jpg'
|
||||
}
|
||||
],
|
||||
banner_image_urls: {
|
||||
w1440h480: 'common/test/data/static/picture1.jpg',
|
||||
w726h242: 'common/test/data/static/picture2.jpg',
|
||||
w348h116: 'common/test/data/static/picture3.jpg'
|
||||
},
|
||||
program_details_url: '/dashboard/programs'
|
||||
authoring_organizations: [
|
||||
{
|
||||
uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
|
||||
key: 'WageningenX',
|
||||
name: 'Wageningen University & Research',
|
||||
certificate_logo_image_url: 'https://example.com/org-certificate-logo.jpg',
|
||||
logo_image_url: 'https://example.com/org-logo.jpg'
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
|
||||
@@ -51,13 +70,13 @@ define([
|
||||
it('should render the header based on the passed in model', function() {
|
||||
var programListUrl = view.$('.breadcrumb-list .crumb:nth-of-type(2) .crumb-link').attr('href');
|
||||
|
||||
expect(view.$('.title').html()).toEqual(context.programData.name);
|
||||
expect(view.$('.title').html()).toEqual(context.programData.title);
|
||||
expect(view.$('.subtitle').html()).toEqual(context.programData.subtitle);
|
||||
expect(view.$('.org-logo').length).toEqual(context.programData.organizations.length);
|
||||
expect(view.$('.org-logo').attr('src')).toEqual(context.programData.organizations[0].img);
|
||||
expect(view.$('.org-logo').attr('alt')).toEqual(
|
||||
context.programData.organizations[0].display_name + '\'s logo'
|
||||
);
|
||||
expect(view.$('.org-logo').length).toEqual(context.programData.authoring_organizations.length);
|
||||
expect(view.$('.org-logo').attr('src'))
|
||||
.toEqual(context.programData.authoring_organizations[0].certificate_logo_image_url);
|
||||
expect(view.$('.org-logo').attr('alt'))
|
||||
.toEqual(context.programData.authoring_organizations[0].name + '\'s logo');
|
||||
expect(programListUrl).toEqual(context.urls.program_listing_url);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -98,7 +98,7 @@ from openedx.core.djangolib.markup import HTML, Text
|
||||
<% 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 = programs_by_run.get(unicode(enrollment.course_id)) %>
|
||||
<% related_programs = inverted_programs.get(unicode(enrollment.course_id)) %>
|
||||
<%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, show_refund_option=show_refund_option, 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' />
|
||||
% endfor
|
||||
|
||||
|
||||
@@ -300,8 +300,8 @@ from student.helpers import (
|
||||
<ul>
|
||||
% for program in related_programs:
|
||||
<li>
|
||||
<span class="category-icon ${program['category'].lower()}-icon" aria-hidden="true"></span>
|
||||
<span><a href="${program['detail_url']}">${u'{name} {category}'.format(name=program['name'], category=program['category'])}</a></span>
|
||||
<span class="category-icon ${program['type'].lower()}-icon" aria-hidden="true"></span>
|
||||
<span><a href="${program['detail_url']}">${u'{title} {type}'.format(title=program['title'], type=program['type'])}</a></span>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
@@ -397,12 +397,6 @@ from student.helpers import (
|
||||
</div>
|
||||
%endif
|
||||
|
||||
% if course_program_info and course_program_info.get('category'):
|
||||
%for program_data in course_program_info.get('course_program_list', []):
|
||||
<%include file = "_dashboard_program_info.html" args="program_data=program_data, enrollment_mode=enrollment.mode, category=course_program_info['category']" />
|
||||
%endfor
|
||||
% endif
|
||||
|
||||
% if is_course_blocked:
|
||||
<p id="block-course-msg" class="course-block">
|
||||
${Text(_("You can no longer access this course because payment has not yet been received. "
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
class="header-img"
|
||||
src="<%- course_image_url %>"
|
||||
<% // safe-lint: disable=underscore-not-escaped %>
|
||||
alt="<%= interpolate(gettext('%(courseName)s Home Page.'), {courseName: display_name}, true) %>"/>
|
||||
alt="<%= interpolate(gettext('%(courseName)s Home Page.'), {courseName: title}, true) %>"/>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<img class="header-img" src="<%- course_image_url %>" alt=""/>
|
||||
@@ -18,10 +18,10 @@
|
||||
<h3 class="course-title">
|
||||
<% if ( marketing_url || course_url ) { %>
|
||||
<a href="<%- marketing_url || course_url %>" class="course-title-link">
|
||||
<%- display_name %>
|
||||
<%- title %>
|
||||
</a>
|
||||
<% } else { %>
|
||||
<%- display_name %>
|
||||
<%- title %>
|
||||
<% } %>
|
||||
</h3>
|
||||
<div class="course-text">
|
||||
@@ -29,7 +29,7 @@
|
||||
<span class="run-period"><%- start_date %> - <%- end_date %></span>
|
||||
-
|
||||
<% } %>
|
||||
<span class="course-key"><%- key %></span>
|
||||
<span class="course-key"><%- course_key %></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -10,9 +10,9 @@
|
||||
</a>
|
||||
<% } %>
|
||||
<% } else { %>
|
||||
<% if (enrollable_run_modes.length > 0) { %>
|
||||
<div class="enrollment-info"><%- gettext('not enrolled') %></div>
|
||||
<% if (enrollable_run_modes.length > 1) { %>
|
||||
<% if (enrollable_course_runs.length > 0) { %>
|
||||
<div class="enrollment-info"><%- gettext('Not Enrolled') %></div>
|
||||
<% if (enrollable_course_runs.length > 1) { %>
|
||||
<div class="run-select-container">
|
||||
<div class="select-error">
|
||||
<%- gettext('Please select a course date') %>
|
||||
@@ -24,16 +24,16 @@
|
||||
<option value="" selected="selected">
|
||||
<%- gettext('Choose Course Date') %>
|
||||
</option>
|
||||
<% _.each (enrollable_run_modes, function(runMode) { %>
|
||||
<% _.each (enrollable_course_runs, function(courseRun) { %>
|
||||
<option
|
||||
value="<%- runMode.run_key %>"
|
||||
<% if (run_key === runMode.run_key) { %>
|
||||
value="<%- courseRun.key %>"
|
||||
<% if (key === courseRun.key) { %>
|
||||
selected="selected"
|
||||
<% }%>
|
||||
>
|
||||
<%= interpolate(
|
||||
gettext('Starts %(start)s'),
|
||||
{ start: runMode.start_date },
|
||||
{ start: courseRun.start_date },
|
||||
true)
|
||||
%>
|
||||
</option>
|
||||
@@ -44,14 +44,14 @@
|
||||
<button type="button" class="btn-brand btn cta-primary enroll-button">
|
||||
<%- gettext('Enroll Now') %>
|
||||
</button>
|
||||
<% } else if (upcoming_run_modes.length > 0) {%>
|
||||
<% } else if (upcoming_course_runs.length > 0) {%>
|
||||
<div class="no-action-message">
|
||||
<%- gettext('Coming Soon') %>
|
||||
</div>
|
||||
<div class="enrollment-opens">
|
||||
<%- gettext('Enrollment Opens on') %>
|
||||
<span class="enrollment-open-date">
|
||||
<%- upcoming_run_modes[0].enrollment_open_date %>
|
||||
<%- upcoming_course_runs[0].enrollment_open_date %>
|
||||
</span>
|
||||
</div>
|
||||
<% } else { %>
|
||||
|
||||
@@ -1,35 +1,35 @@
|
||||
<div class="text-section">
|
||||
<h3 id="program-<%- id %>" class="title hd-3"><%- gettext(name) %></h3>
|
||||
<h3 id="program-<%- uuid %>" class="title hd-3"><%- gettext(title) %></h3>
|
||||
<div class="meta-info grid-container">
|
||||
<div class="organization col"><%- orgList %></div>
|
||||
<div class="category col col-last">
|
||||
<span class="category-text"><%- gettext(category) %></span>
|
||||
<span class="category-icon <%- category.toLowerCase() %>-icon" aria-hidden="true"></span>
|
||||
<span class="category-text"><%- gettext(type) %></span>
|
||||
<span class="category-icon <%- type.toLowerCase() %>-icon" aria-hidden="true"></span>
|
||||
</div>
|
||||
</div>
|
||||
<% if (progress) { %>
|
||||
<p class="certificate-status">
|
||||
<a href="<%- detailUrl %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate(
|
||||
<a href="<%- detailUrl %>" class="status-text secondary" aria-describedby="program-<%- uuid %>"><%= interpolate(
|
||||
ngettext(
|
||||
'%(count)s course is in progress.',
|
||||
'%(count)s courses are in progress.',
|
||||
progress.total.in_progress
|
||||
progress.in_progress
|
||||
),
|
||||
{count: progress.total.in_progress}, true
|
||||
{count: progress.in_progress}, true
|
||||
) %></a>
|
||||
|
||||
<a href="<%- detailUrl %>" class="status-text secondary" aria-describedby="program-<%- id %>"><%= interpolate(
|
||||
<a href="<%- detailUrl %>" class="status-text secondary" aria-describedby="program-<%- uuid %>"><%= interpolate(
|
||||
ngettext(
|
||||
'%(count)s course has not been started.',
|
||||
'%(count)s courses have not been started.',
|
||||
progress.total.not_started
|
||||
progress.not_started
|
||||
),
|
||||
{count: progress.total.not_started}, true
|
||||
{count: progress.not_started}, true
|
||||
) %></a>
|
||||
|
||||
<span id="status-<%- id %>" class="status-text"><%= interpolate(
|
||||
<span id="status-<%- uuid %>" class="status-text"><%= interpolate(
|
||||
gettext('You have earned certificates in %(completed_courses)s of the %(total_courses)s courses so far.'),
|
||||
{completed_courses: progress.total.completed, total_courses: progress.total.courses}, true
|
||||
{completed_courses: progress.completed, total_courses: progress.total}, true
|
||||
) %></span>
|
||||
</p>
|
||||
<% } %>
|
||||
@@ -44,11 +44,11 @@
|
||||
<a href="<%- detailUrl %>" class="card-link">
|
||||
<div class="banner-image-container">
|
||||
<picture>
|
||||
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.tiny %>)">
|
||||
<source srcset="<%- mediumBannerUrl %>" media="(max-width: <%- breakpoints.max.small %>)">
|
||||
<source srcset="<%- largeBannerUrl %>" media="(max-width: <%- breakpoints.max.medium %>)">
|
||||
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.large %>)">
|
||||
<img class="banner-image" srcset="<%- mediumBannerUrl %>" alt="<%= interpolate(gettext('%(programName)s Home Page.'), {programName: name}, true)%>">
|
||||
<source srcset="<%- xsmallBannerUrl %>" media="(max-width: <%- breakpoints.max.xsmall %>)">
|
||||
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.small %>)">
|
||||
<source srcset="<%- mediumBannerUrl %>" media="(max-width: <%- breakpoints.max.medium %>)">
|
||||
<source srcset="<%- xsmallBannerUrl %>" media="(max-width: <%- breakpoints.max.large %>)">
|
||||
<img class="banner-image" srcset="<%- smallBannerUrl %>" alt="<%= interpolate(gettext('%(programName)s Home Page.'), {programName: title}, true)%>">
|
||||
</picture>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
<div class="banner-background-wrapper">
|
||||
<picture>
|
||||
<source srcset="<%- programData.banner_image_urls.w1440h480 %>" media="(min-width: <%- breakpoints.min.large %>)">
|
||||
<source srcset="<%- programData.banner_image_urls.w726h242 %>" media="(min-width: <%- breakpoints.min.medium %>)">
|
||||
<img class="banner-background-image" srcset="<%- programData.banner_image_urls.w348h116 %>" alt="">
|
||||
<source srcset="<%- programData.banner_image.large.url %>" media="(min-width: <%- breakpoints.min.large %>)">
|
||||
<source srcset="<%- programData.banner_image.medium.url %>" media="(min-width: <%- breakpoints.min.medium %>)">
|
||||
<img class="banner-background-image" srcset="<%- programData.banner_image['x-small'].url %>" alt="">
|
||||
</picture>
|
||||
|
||||
<div class="banner-content grid-container">
|
||||
<h2 class="hd-1 title row"><%- programData.name %></h2>
|
||||
<h2 class="hd-1 title row"><%- programData.title %></h2>
|
||||
<p class="hd-4 subtitle row"><%- programData.subtitle %></p>
|
||||
<% if (programData.organizations.length) { %>
|
||||
<% if (programData.authoring_organizations.length) { %>
|
||||
<div class="org-wrapper">
|
||||
<% _.each(programData.organizations, function(org) { %>
|
||||
<img src="<%- org.img %>" class="org-logo" alt="<%- StringUtils.interpolate(
|
||||
<% _.each(programData.authoring_organizations, function(org) { %>
|
||||
<img src="<%- org.certificate_logo_image_url || org.logo_image_url %>" class="org-logo" alt="<%- StringUtils.interpolate(
|
||||
gettext('{organization}\'s logo'),
|
||||
{organization: org.display_name}
|
||||
{organization: org.name}
|
||||
) %>">
|
||||
<% }) %>
|
||||
</div>
|
||||
@@ -33,7 +33,7 @@
|
||||
<span class="crumb-separator fa fa-chevron-right" aria-hidden="true"></span>
|
||||
</li>
|
||||
<li class="crumb active">
|
||||
<%- programData.name %>
|
||||
<%- programData.title %>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
@@ -13,7 +13,6 @@ from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory, Prog
|
||||
from openedx.core.djangoapps.catalog.tests.mixins import CatalogIntegrationMixin
|
||||
from openedx.core.djangoapps.catalog.utils import (
|
||||
get_programs,
|
||||
munge_catalog_program,
|
||||
get_program_types,
|
||||
get_programs_with_type_logo,
|
||||
)
|
||||
@@ -131,63 +130,6 @@ class TestGetPrograms(CatalogIntegrationMixin, TestCase):
|
||||
self.assertEqual(data, [])
|
||||
|
||||
|
||||
class TestMungeCatalogProgram(TestCase):
|
||||
def setUp(self):
|
||||
super(TestMungeCatalogProgram, self).setUp()
|
||||
|
||||
self.catalog_program = ProgramFactory()
|
||||
|
||||
def assert_munged(self, program):
|
||||
munged = munge_catalog_program(program)
|
||||
expected = {
|
||||
'id': program['uuid'],
|
||||
'name': program['title'],
|
||||
'subtitle': program['subtitle'],
|
||||
'category': program['type'],
|
||||
'marketing_slug': program['marketing_slug'],
|
||||
'organizations': [
|
||||
{
|
||||
'display_name': organization['name'],
|
||||
'key': organization['key']
|
||||
} for organization in program['authoring_organizations']
|
||||
],
|
||||
'course_codes': [
|
||||
{
|
||||
'display_name': course['title'],
|
||||
'key': course['key'],
|
||||
'organization': {
|
||||
'display_name': course['owners'][0]['name'],
|
||||
'key': course['owners'][0]['key']
|
||||
},
|
||||
'run_modes': [
|
||||
{
|
||||
'course_key': course_run['key'],
|
||||
'run_key': CourseKey.from_string(course_run['key']).run,
|
||||
'mode_slug': course_run['type'],
|
||||
'marketing_url': course_run['marketing_url'],
|
||||
} for course_run in course['course_runs']
|
||||
],
|
||||
} for course in program['courses']
|
||||
],
|
||||
'banner_image_urls': {
|
||||
'w1440h480': program['banner_image']['large']['url'],
|
||||
'w726h242': program['banner_image']['medium']['url'],
|
||||
'w435h145': program['banner_image']['small']['url'],
|
||||
'w348h116': program['banner_image']['x-small']['url'],
|
||||
},
|
||||
'detail_url': program.get('detail_url'),
|
||||
}
|
||||
|
||||
self.assertEqual(munged, expected)
|
||||
|
||||
def test_munge_catalog_program(self):
|
||||
self.assert_munged(self.catalog_program)
|
||||
|
||||
def test_munge_with_detail_url(self):
|
||||
self.catalog_program['detail_url'] = 'foo'
|
||||
self.assert_munged(self.catalog_program)
|
||||
|
||||
|
||||
@skip_unless_lms
|
||||
@mock.patch(UTILS_MODULE + '.get_edx_api_data')
|
||||
class TestGetProgramTypes(CatalogIntegrationMixin, TestCase):
|
||||
|
||||
@@ -66,64 +66,6 @@ def get_programs(uuid=None, type=None): # pylint: disable=redefined-builtin
|
||||
return []
|
||||
|
||||
|
||||
def munge_catalog_program(catalog_program):
|
||||
"""
|
||||
Make a program from the catalog service look like it came from the programs service.
|
||||
|
||||
We want to display programs from the catalog service on the LMS. The LMS
|
||||
originally retrieved all program data from the deprecated programs service.
|
||||
This temporary utility is here to help incrementally swap out the backend.
|
||||
|
||||
Clean up of this debt is tracked by ECOM-4418.
|
||||
|
||||
Arguments:
|
||||
catalog_program (dict): The catalog service's representation of a program.
|
||||
|
||||
Return:
|
||||
dict, imitating the schema used by the programs service.
|
||||
"""
|
||||
return {
|
||||
'id': catalog_program['uuid'],
|
||||
'name': catalog_program['title'],
|
||||
'subtitle': catalog_program['subtitle'],
|
||||
'category': catalog_program['type'],
|
||||
'marketing_slug': catalog_program['marketing_slug'],
|
||||
'organizations': [
|
||||
{
|
||||
'display_name': organization['name'],
|
||||
'key': organization['key']
|
||||
} for organization in catalog_program['authoring_organizations']
|
||||
],
|
||||
'course_codes': [
|
||||
{
|
||||
'display_name': course['title'],
|
||||
'key': course['key'],
|
||||
'organization': {
|
||||
# The Programs schema only supports one organization here.
|
||||
'display_name': course['owners'][0]['name'],
|
||||
'key': course['owners'][0]['key']
|
||||
} if course['owners'] else {},
|
||||
'run_modes': [
|
||||
{
|
||||
'course_key': course_run['key'],
|
||||
'run_key': CourseKey.from_string(course_run['key']).run,
|
||||
'mode_slug': course_run['type'],
|
||||
'marketing_url': course_run['marketing_url'],
|
||||
} for course_run in course['course_runs']
|
||||
],
|
||||
} for course in catalog_program['courses']
|
||||
],
|
||||
'banner_image_urls': {
|
||||
'w1440h480': catalog_program['banner_image']['large']['url'],
|
||||
'w726h242': catalog_program['banner_image']['medium']['url'],
|
||||
'w435h145': catalog_program['banner_image']['small']['url'],
|
||||
'w348h116': catalog_program['banner_image']['x-small']['url'],
|
||||
},
|
||||
# If a detail URL has been added, we don't want to lose it.
|
||||
'detail_url': catalog_program.get('detail_url'),
|
||||
}
|
||||
|
||||
|
||||
def get_program_types():
|
||||
"""Retrieve all program types from the catalog service.
|
||||
|
||||
|
||||
@@ -4,14 +4,11 @@ import factory
|
||||
from faker import Faker
|
||||
|
||||
|
||||
fake = Faker()
|
||||
|
||||
|
||||
class ProgressFactory(factory.Factory):
|
||||
class Meta(object):
|
||||
model = dict
|
||||
|
||||
uuid = factory.Faker('uuid4')
|
||||
completed = []
|
||||
in_progress = []
|
||||
not_started = []
|
||||
completed = 0
|
||||
in_progress = 0
|
||||
not_started = 0
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
"""Tests covering Programs utilities."""
|
||||
# pylint: disable=no-member
|
||||
import datetime
|
||||
import json
|
||||
import uuid
|
||||
@@ -15,7 +16,6 @@ from pytz import utc
|
||||
|
||||
from lms.djangoapps.certificates.api import MODES
|
||||
from lms.djangoapps.commerce.tests.test_utils import update_commerce_config
|
||||
from openedx.core.djangoapps.catalog.utils import munge_catalog_program
|
||||
from openedx.core.djangoapps.catalog.tests.factories import (
|
||||
generate_course_run_key,
|
||||
ProgramFactory,
|
||||
@@ -60,10 +60,6 @@ class TestProgramProgressMeter(TestCase):
|
||||
"""Variadic helper used to verify progress calculations."""
|
||||
self.assertEqual(meter.progress, list(progresses))
|
||||
|
||||
def _extract_titles(self, program, *indices):
|
||||
"""Construct a list containing the titles of the indicated courses."""
|
||||
return [program['courses'][index]['title'] for index in indices]
|
||||
|
||||
def _attach_detail_url(self, programs):
|
||||
"""Add expected detail URLs to a list of program dicts."""
|
||||
for program in programs:
|
||||
@@ -118,10 +114,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
self.assertEqual(meter.engaged_programs, [program])
|
||||
self._assert_progress(
|
||||
meter,
|
||||
ProgressFactory(
|
||||
uuid=program['uuid'],
|
||||
in_progress=self._extract_titles(program, 0)
|
||||
)
|
||||
ProgressFactory(uuid=program['uuid'], in_progress=1)
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
@@ -160,10 +153,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
self.assertEqual(meter.engaged_programs, programs)
|
||||
self._assert_progress(
|
||||
meter,
|
||||
*(
|
||||
ProgressFactory(uuid=program['uuid'], in_progress=self._extract_titles(program, 0))
|
||||
for program in programs
|
||||
)
|
||||
*(ProgressFactory(uuid=program['uuid'], in_progress=1) for program in programs)
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
@@ -208,10 +198,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
self.assertEqual(meter.engaged_programs, programs)
|
||||
self._assert_progress(
|
||||
meter,
|
||||
*(
|
||||
ProgressFactory(uuid=program['uuid'], in_progress=self._extract_titles(program, 0))
|
||||
for program in programs
|
||||
)
|
||||
*(ProgressFactory(uuid=program['uuid'], in_progress=1) for program in programs)
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
@@ -245,11 +232,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
program, program_uuid = data[0], data[0]['uuid']
|
||||
self._assert_progress(
|
||||
meter,
|
||||
ProgressFactory(
|
||||
uuid=program_uuid,
|
||||
in_progress=self._extract_titles(program, 0),
|
||||
not_started=self._extract_titles(program, 1)
|
||||
)
|
||||
ProgressFactory(uuid=program_uuid, in_progress=1, not_started=1)
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
@@ -258,10 +241,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
meter = ProgramProgressMeter(self.user)
|
||||
self._assert_progress(
|
||||
meter,
|
||||
ProgressFactory(
|
||||
uuid=program_uuid,
|
||||
in_progress=self._extract_titles(program, 0, 1)
|
||||
)
|
||||
ProgressFactory(uuid=program_uuid, in_progress=2)
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
@@ -272,11 +252,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
meter = ProgramProgressMeter(self.user)
|
||||
self._assert_progress(
|
||||
meter,
|
||||
ProgressFactory(
|
||||
uuid=program_uuid,
|
||||
completed=self._extract_titles(program, 0),
|
||||
in_progress=self._extract_titles(program, 1)
|
||||
)
|
||||
ProgressFactory(uuid=program_uuid, completed=1, in_progress=1)
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
@@ -288,11 +264,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
meter = ProgramProgressMeter(self.user)
|
||||
self._assert_progress(
|
||||
meter,
|
||||
ProgressFactory(
|
||||
uuid=program_uuid,
|
||||
completed=self._extract_titles(program, 0),
|
||||
in_progress=self._extract_titles(program, 1)
|
||||
)
|
||||
ProgressFactory(uuid=program_uuid, completed=1, in_progress=1)
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [])
|
||||
|
||||
@@ -304,10 +276,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
meter = ProgramProgressMeter(self.user)
|
||||
self._assert_progress(
|
||||
meter,
|
||||
ProgressFactory(
|
||||
uuid=program_uuid,
|
||||
completed=self._extract_titles(program, 0, 1)
|
||||
)
|
||||
ProgressFactory(uuid=program_uuid, completed=2)
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [program_uuid])
|
||||
|
||||
@@ -340,7 +309,7 @@ class TestProgramProgressMeter(TestCase):
|
||||
program, program_uuid = data[0], data[0]['uuid']
|
||||
self._assert_progress(
|
||||
meter,
|
||||
ProgressFactory(uuid=program_uuid, completed=self._extract_titles(program, 0))
|
||||
ProgressFactory(uuid=program_uuid, completed=1)
|
||||
)
|
||||
self.assertEqual(meter.completed_programs, [program_uuid])
|
||||
|
||||
@@ -418,57 +387,56 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
"""Tests of the program data extender utility class."""
|
||||
maxDiff = None
|
||||
sku = 'abc123'
|
||||
password = 'test'
|
||||
checkout_path = '/basket'
|
||||
|
||||
def setUp(self):
|
||||
super(TestProgramDataExtender, self).setUp()
|
||||
|
||||
self.user = UserFactory()
|
||||
self.client.login(username=self.user.username, password=self.password)
|
||||
|
||||
self.course = ModuleStoreCourseFactory()
|
||||
self.course.start = datetime.datetime.now(utc) - datetime.timedelta(days=1)
|
||||
self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=1)
|
||||
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
|
||||
self.course = self.update_course(self.course, self.user.id)
|
||||
|
||||
organization = OrganizationFactory()
|
||||
course_run = CourseRunFactory(key=unicode(self.course.id)) # pylint: disable=no-member
|
||||
course = CourseFactory(course_runs=[course_run])
|
||||
program = ProgramFactory(authoring_organizations=[organization], courses=[course])
|
||||
|
||||
self.program = munge_catalog_program(program)
|
||||
self.course_code = self.program['course_codes'][0]
|
||||
self.run_mode = self.course_code['run_modes'][0]
|
||||
self.course_run = CourseRunFactory(key=unicode(self.course.id))
|
||||
self.catalog_course = CourseFactory(course_runs=[self.course_run])
|
||||
self.program = ProgramFactory(courses=[self.catalog_course])
|
||||
|
||||
def _assert_supplemented(self, actual, **kwargs):
|
||||
"""DRY helper used to verify that program data is extended correctly."""
|
||||
course_overview = CourseOverview.get_from_id(self.course.id) # pylint: disable=no-member
|
||||
run_mode = dict(
|
||||
{
|
||||
'certificate_url': None,
|
||||
'course_image_url': course_overview.course_image_url,
|
||||
'course_key': unicode(self.course.id), # pylint: disable=no-member
|
||||
'course_url': reverse('course_root', args=[self.course.id]), # pylint: disable=no-member
|
||||
'end_date': self.course.end.replace(tzinfo=utc),
|
||||
'enrollment_open_date': strftime_localized(DEFAULT_ENROLLMENT_START_DATE, 'SHORT_DATE'),
|
||||
'is_course_ended': self.course.end < datetime.datetime.now(utc),
|
||||
'is_enrolled': False,
|
||||
'is_enrollment_open': True,
|
||||
'marketing_url': self.run_mode['marketing_url'],
|
||||
'mode_slug': 'verified',
|
||||
'start_date': self.course.start.replace(tzinfo=utc),
|
||||
'upgrade_url': None,
|
||||
'advertised_start': None,
|
||||
},
|
||||
**kwargs
|
||||
self.course_run.update(
|
||||
dict(
|
||||
{
|
||||
'certificate_url': None,
|
||||
'course_url': reverse('course_root', args=[self.course.id]),
|
||||
'enrollment_open_date': strftime_localized(DEFAULT_ENROLLMENT_START_DATE, 'SHORT_DATE'),
|
||||
'is_course_ended': self.course.end < datetime.datetime.now(utc),
|
||||
'is_enrolled': False,
|
||||
'is_enrollment_open': True,
|
||||
'upgrade_url': None,
|
||||
'advertised_start': None,
|
||||
},
|
||||
**kwargs
|
||||
)
|
||||
)
|
||||
|
||||
self.course_code['run_modes'] = [run_mode]
|
||||
self.program['course_codes'] = [self.course_code]
|
||||
self.catalog_course['course_runs'] = [self.course_run]
|
||||
self.program['courses'] = [self.catalog_course]
|
||||
|
||||
self.assertEqual(actual, self.program)
|
||||
|
||||
@ddt.data(-1, 0, 1)
|
||||
def test_is_enrollment_open(self, days_offset):
|
||||
"""
|
||||
Verify that changes to the course run end date do not affect our
|
||||
assessment of the course run being open for enrollment.
|
||||
"""
|
||||
self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=days_offset)
|
||||
self.course = self.update_course(self.course, self.user.id)
|
||||
|
||||
data = ProgramDataExtender(self.program, self.user).extend()
|
||||
|
||||
self._assert_supplemented(data)
|
||||
|
||||
@ddt.data(
|
||||
(False, None, False),
|
||||
(True, MODES.audit, True),
|
||||
@@ -491,7 +459,7 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
mock_get_mode.return_value = mock_mode
|
||||
|
||||
if is_enrolled:
|
||||
CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=enrolled_mode) # pylint: disable=no-member
|
||||
CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=enrolled_mode)
|
||||
|
||||
data = ProgramDataExtender(self.program, self.user).extend()
|
||||
|
||||
@@ -503,12 +471,14 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
|
||||
@ddt.data(MODES.audit, MODES.verified)
|
||||
def test_inactive_enrollment_no_upgrade(self, enrolled_mode):
|
||||
"""Verify that a student with an inactive enrollment isn't encouraged to upgrade."""
|
||||
"""
|
||||
Verify that a student with an inactive enrollment isn't encouraged to upgrade.
|
||||
"""
|
||||
update_commerce_config(enabled=True, checkout_page=self.checkout_path)
|
||||
|
||||
CourseEnrollmentFactory(
|
||||
user=self.user,
|
||||
course_id=self.course.id, # pylint: disable=no-member
|
||||
course_id=self.course.id,
|
||||
mode=enrolled_mode,
|
||||
is_active=False,
|
||||
)
|
||||
@@ -519,14 +489,16 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
|
||||
@mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course')
|
||||
def test_ecommerce_disabled(self, mock_get_mode):
|
||||
"""Verify that the utility can operate when the ecommerce service is disabled."""
|
||||
"""
|
||||
Verify that the utility can operate when the ecommerce service is disabled.
|
||||
"""
|
||||
update_commerce_config(enabled=False, checkout_page=self.checkout_path)
|
||||
|
||||
mock_mode = mock.Mock()
|
||||
mock_mode.sku = self.sku
|
||||
mock_get_mode.return_value = mock_mode
|
||||
|
||||
CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=MODES.audit) # pylint: disable=no-member
|
||||
CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=MODES.audit)
|
||||
|
||||
data = ProgramDataExtender(self.program, self.user).extend()
|
||||
|
||||
@@ -537,14 +509,14 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
(1, -1, True),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_course_enrollment_status(self, start_offset, end_offset, is_enrollment_open):
|
||||
def test_course_run_enrollment_status(self, start_offset, end_offset, is_enrollment_open):
|
||||
"""
|
||||
Verify that course enrollment status is reflected correctly.
|
||||
Verify that course run enrollment status is reflected correctly.
|
||||
"""
|
||||
self.course.enrollment_start = datetime.datetime.now(utc) - datetime.timedelta(days=start_offset)
|
||||
self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=end_offset)
|
||||
|
||||
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
|
||||
self.course = self.update_course(self.course, self.user.id)
|
||||
|
||||
data = ProgramDataExtender(self.program, self.user).extend()
|
||||
|
||||
@@ -555,12 +527,12 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
)
|
||||
|
||||
def test_no_enrollment_start_date(self):
|
||||
"""Verify that a closed course with no explicit enrollment start date doesn't cause an error.
|
||||
|
||||
Regression test for ECOM-4973.
|
||||
"""
|
||||
Verify that a closed course run with no explicit enrollment start date
|
||||
doesn't cause an error. Regression test for ECOM-4973.
|
||||
"""
|
||||
self.course.enrollment_end = datetime.datetime.now(utc) - datetime.timedelta(days=1)
|
||||
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
|
||||
self.course = self.update_course(self.course, self.user.id)
|
||||
|
||||
data = ProgramDataExtender(self.program, self.user).extend()
|
||||
|
||||
@@ -573,7 +545,10 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
@mock.patch(UTILS_MODULE + '.certificate_api.certificate_downloadable_status')
|
||||
@mock.patch(CERTIFICATES_API_MODULE + '.has_html_certificates_enabled')
|
||||
def test_certificate_url_retrieval(self, is_uuid_available, mock_html_certs_enabled, mock_get_cert_data):
|
||||
"""Verify that the student's run mode certificate is included, when available."""
|
||||
"""
|
||||
Verify that the student's run mode certificate is included,
|
||||
when available.
|
||||
"""
|
||||
test_uuid = uuid.uuid4().hex
|
||||
mock_get_cert_data.return_value = {'uuid': test_uuid} if is_uuid_available else {}
|
||||
mock_html_certs_enabled.return_value = True
|
||||
@@ -586,42 +561,3 @@ class TestProgramDataExtender(ModuleStoreTestCase):
|
||||
) if is_uuid_available else None
|
||||
|
||||
self._assert_supplemented(data, certificate_url=expected_url)
|
||||
|
||||
@ddt.data(-1, 0, 1)
|
||||
def test_course_course_ended(self, days_offset):
|
||||
self.course.end = datetime.datetime.now(utc) + datetime.timedelta(days=days_offset)
|
||||
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
|
||||
|
||||
data = ProgramDataExtender(self.program, self.user).extend()
|
||||
|
||||
self._assert_supplemented(data)
|
||||
|
||||
@mock.patch(UTILS_MODULE + '.get_organization_by_short_name')
|
||||
def test_organization_logo_exists(self, mock_get_organization_by_short_name):
|
||||
""" Verify the logo image is set from the organizations api """
|
||||
mock_logo_url = 'edx/logo.png'
|
||||
mock_image = mock.Mock()
|
||||
mock_image.url = mock_logo_url
|
||||
mock_get_organization_by_short_name.return_value = {
|
||||
'logo': mock_image
|
||||
}
|
||||
|
||||
data = ProgramDataExtender(self.program, self.user).extend()
|
||||
self.assertEqual(data['organizations'][0].get('img'), mock_logo_url)
|
||||
|
||||
@mock.patch(UTILS_MODULE + '.get_organization_by_short_name')
|
||||
def test_organization_missing(self, mock_get_organization_by_short_name):
|
||||
""" Verify the logo image is not set if the organizations api returns None """
|
||||
mock_get_organization_by_short_name.return_value = None
|
||||
data = ProgramDataExtender(self.program, self.user).extend()
|
||||
self.assertEqual(data['organizations'][0].get('img'), None)
|
||||
|
||||
@mock.patch(UTILS_MODULE + '.get_organization_by_short_name')
|
||||
def test_organization_logo_missing(self, mock_get_organization_by_short_name):
|
||||
"""
|
||||
Verify the logo image is not set if the organizations api returns organization,
|
||||
but the logo is not available
|
||||
"""
|
||||
mock_get_organization_by_short_name.return_value = {'logo': None}
|
||||
data = ProgramDataExtender(self.program, self.user).extend()
|
||||
self.assertEqual(data['organizations'][0].get('img'), None)
|
||||
|
||||
@@ -18,7 +18,6 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi
|
||||
from openedx.core.lib.edx_api_utils import get_edx_api_data
|
||||
from student.models import CourseEnrollment
|
||||
from util.date_utils import strftime_localized
|
||||
from util.organizations_helpers import get_organization_by_short_name
|
||||
|
||||
|
||||
# The datetime module's strftime() methods require a year >= 1900.
|
||||
@@ -26,7 +25,7 @@ DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=utc)
|
||||
|
||||
|
||||
def get_program_marketing_url(programs_config):
|
||||
"""Build a URL to be used when linking to program details on a marketing site."""
|
||||
"""Build a URL used to link to programs on the marketing site."""
|
||||
return urljoin(settings.MKTG_URLS.get('ROOT'), programs_config.marketing_path).rstrip('/')
|
||||
|
||||
|
||||
@@ -47,18 +46,6 @@ def attach_program_detail_url(programs):
|
||||
return programs
|
||||
|
||||
|
||||
def munge_progress_map(progress_map):
|
||||
"""
|
||||
Temporary utility for making progress maps look like they were built using
|
||||
data from the deprecated programs service.
|
||||
|
||||
Clean up of this debt is tracked by ECOM-4418.
|
||||
"""
|
||||
progress_map['id'] = progress_map.pop('uuid')
|
||||
|
||||
return progress_map
|
||||
|
||||
|
||||
class ProgramProgressMeter(object):
|
||||
"""Utility for gauging a user's progress towards program completion.
|
||||
|
||||
@@ -139,19 +126,15 @@ class ProgramProgressMeter(object):
|
||||
"""
|
||||
progress = []
|
||||
for program in self.engaged_programs:
|
||||
completed, in_progress, not_started = [], [], []
|
||||
completed, in_progress, not_started = 0, 0, 0
|
||||
|
||||
for course in program['courses']:
|
||||
# TODO: What are these titles used for? If they're not used by
|
||||
# the front-end, pass integer counts instead.
|
||||
title = course['title']
|
||||
|
||||
if self._is_course_complete(course):
|
||||
completed.append(title)
|
||||
completed += 1
|
||||
elif self._is_course_in_progress(course):
|
||||
in_progress.append(title)
|
||||
in_progress += 1
|
||||
else:
|
||||
not_started.append(title)
|
||||
not_started += 1
|
||||
|
||||
progress.append({
|
||||
'uuid': program['uuid'],
|
||||
@@ -249,18 +232,18 @@ class ProgramProgressMeter(object):
|
||||
# pylint: disable=missing-docstring
|
||||
class ProgramDataExtender(object):
|
||||
"""
|
||||
Utility for extending program course codes with CourseOverview and
|
||||
CourseEnrollment data.
|
||||
Utility for extending program data meant for the program detail page with
|
||||
user-specific (e.g., CourseEnrollment) data.
|
||||
|
||||
Arguments:
|
||||
program_data (dict): Representation of a program. Note that this dict must
|
||||
be formatted as if it was returned by the deprecated program service.
|
||||
program_data (dict): Representation of a program.
|
||||
user (User): The user whose enrollments to inspect.
|
||||
"""
|
||||
def __init__(self, program_data, user):
|
||||
self.data = program_data
|
||||
self.user = user
|
||||
self.course_key = None
|
||||
|
||||
self.course_run_key = None
|
||||
self.course_overview = None
|
||||
self.enrollment_start = None
|
||||
|
||||
@@ -278,77 +261,62 @@ class ProgramDataExtender(object):
|
||||
"""Returns a generator yielding method names beginning with the given prefix."""
|
||||
return (name for name in cls.__dict__ if name.startswith(prefix))
|
||||
|
||||
def _extend_organizations(self):
|
||||
"""Execute organization data handlers."""
|
||||
for organization in self.data['organizations']:
|
||||
self._execute('_attach_organization', organization)
|
||||
|
||||
def _extend_run_modes(self):
|
||||
"""Execute run mode data handlers."""
|
||||
for course_code in self.data['course_codes']:
|
||||
for run_mode in course_code['run_modes']:
|
||||
def _extend_course_runs(self):
|
||||
"""Execute course run data handlers."""
|
||||
for course in self.data['courses']:
|
||||
for course_run in course['course_runs']:
|
||||
# State to be shared across handlers.
|
||||
self.course_key = CourseKey.from_string(run_mode['course_key'])
|
||||
self.course_overview = CourseOverview.get_from_id(self.course_key)
|
||||
self.course_run_key = CourseKey.from_string(course_run['key'])
|
||||
self.course_overview = CourseOverview.get_from_id(self.course_run_key)
|
||||
self.enrollment_start = self.course_overview.enrollment_start or DEFAULT_ENROLLMENT_START_DATE
|
||||
|
||||
self._execute('_attach_run_mode', run_mode)
|
||||
self._execute('_attach_course_run', course_run)
|
||||
|
||||
def _attach_organization_logo(self, organization):
|
||||
# TODO: Cache the results of the get_organization_by_short_name call so
|
||||
# the database is hit less frequently.
|
||||
org_obj = get_organization_by_short_name(organization['key'])
|
||||
if org_obj and org_obj.get('logo'):
|
||||
organization['img'] = org_obj['logo'].url
|
||||
|
||||
def _attach_run_mode_certificate_url(self, run_mode):
|
||||
certificate_data = certificate_api.certificate_downloadable_status(self.user, self.course_key)
|
||||
def _attach_course_run_certificate_url(self, run_mode):
|
||||
certificate_data = certificate_api.certificate_downloadable_status(self.user, self.course_run_key)
|
||||
certificate_uuid = certificate_data.get('uuid')
|
||||
run_mode['certificate_url'] = certificate_api.get_certificate_url(
|
||||
user_id=self.user.id, # Providing user_id allows us to fall back to PDF certificates
|
||||
# if web certificates are not configured for a given course.
|
||||
course_id=self.course_key,
|
||||
course_id=self.course_run_key,
|
||||
uuid=certificate_uuid,
|
||||
) if certificate_uuid else None
|
||||
|
||||
def _attach_run_mode_course_image_url(self, run_mode):
|
||||
run_mode['course_image_url'] = self.course_overview.course_image_url
|
||||
def _attach_course_run_course_url(self, run_mode):
|
||||
run_mode['course_url'] = reverse('course_root', args=[self.course_run_key])
|
||||
|
||||
def _attach_run_mode_course_url(self, run_mode):
|
||||
run_mode['course_url'] = reverse('course_root', args=[self.course_key])
|
||||
|
||||
def _attach_run_mode_end_date(self, run_mode):
|
||||
run_mode['end_date'] = self.course_overview.end
|
||||
|
||||
def _attach_run_mode_enrollment_open_date(self, run_mode):
|
||||
def _attach_course_run_enrollment_open_date(self, run_mode):
|
||||
run_mode['enrollment_open_date'] = strftime_localized(self.enrollment_start, 'SHORT_DATE')
|
||||
|
||||
def _attach_run_mode_is_course_ended(self, run_mode):
|
||||
def _attach_course_run_is_course_ended(self, run_mode):
|
||||
end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=utc)
|
||||
run_mode['is_course_ended'] = end_date < datetime.datetime.now(utc)
|
||||
|
||||
def _attach_run_mode_is_enrolled(self, run_mode):
|
||||
run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(self.user, self.course_key)
|
||||
def _attach_course_run_is_enrolled(self, run_mode):
|
||||
run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(self.user, self.course_run_key)
|
||||
|
||||
def _attach_run_mode_is_enrollment_open(self, run_mode):
|
||||
def _attach_course_run_is_enrollment_open(self, run_mode):
|
||||
enrollment_end = self.course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=utc)
|
||||
run_mode['is_enrollment_open'] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end
|
||||
|
||||
def _attach_run_mode_start_date(self, run_mode):
|
||||
run_mode['start_date'] = self.course_overview.start
|
||||
|
||||
def _attach_run_mode_advertised_start(self, run_mode):
|
||||
def _attach_course_run_advertised_start(self, run_mode):
|
||||
"""
|
||||
The advertised_start is text a course author can provide to be displayed
|
||||
instead of their course's start date. For example, if a course run were
|
||||
to start on December 1, 2016, the author might provide 'Winter 2016' as
|
||||
the advertised start.
|
||||
"""
|
||||
run_mode['advertised_start'] = self.course_overview.advertised_start
|
||||
|
||||
def _attach_run_mode_upgrade_url(self, run_mode):
|
||||
required_mode_slug = run_mode['mode_slug']
|
||||
enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_key)
|
||||
def _attach_course_run_upgrade_url(self, run_mode):
|
||||
required_mode_slug = run_mode['type']
|
||||
enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_run_key)
|
||||
is_mode_mismatch = required_mode_slug != enrolled_mode_slug
|
||||
is_upgrade_required = is_mode_mismatch and CourseEnrollment.is_enrolled(self.user, self.course_key)
|
||||
is_upgrade_required = is_mode_mismatch and CourseEnrollment.is_enrolled(self.user, self.course_run_key)
|
||||
|
||||
if is_upgrade_required:
|
||||
# Requires that the ecommerce service be in use.
|
||||
required_mode = CourseMode.mode_for_course(self.course_key, required_mode_slug)
|
||||
required_mode = CourseMode.mode_for_course(self.course_run_key, required_mode_slug)
|
||||
ecommerce = EcommerceService()
|
||||
sku = getattr(required_mode, 'sku', None)
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers
|
||||
<% 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 = programs_by_run.get(unicode(enrollment.course_id)) %>
|
||||
<% related_programs = inverted_programs.get(unicode(enrollment.course_id)) %>
|
||||
<%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, show_refund_option=show_refund_option, 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" />
|
||||
% endfor
|
||||
|
||||
|
||||
Reference in New Issue
Block a user