Merge pull request #14488 from edx/renzo/finish-catalog-transition
Finish transition to catalog for program data
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