Commit dda0e03f by Renzo Lucioni Committed by GitHub

Merge pull request #14488 from edx/renzo/finish-catalog-transition

Finish transition to catalog for program data
parents c0e45249 0e06e905
...@@ -122,15 +122,14 @@ import newrelic_custom_metrics ...@@ -122,15 +122,14 @@ import newrelic_custom_metrics
# Note that this lives in LMS, so this dependency should be refactored. # Note that this lives in LMS, so this dependency should be refactored.
from notification_prefs.views import enable_notifications 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.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.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.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.utils import ProgramProgressMeter from openedx.core.djangoapps.programs.utils import ProgramProgressMeter
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers 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.theming import helpers as theming_helpers
from openedx.core.djangoapps.user_api.preferences import api as preferences_api 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") log = logging.getLogger("edx.student")
...@@ -670,9 +669,6 @@ def dashboard(request): ...@@ -670,9 +669,6 @@ def dashboard(request):
meter = ProgramProgressMeter(user, enrollments=course_enrollments) meter = ProgramProgressMeter(user, enrollments=course_enrollments)
inverted_programs = meter.invert_programs() 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 # Construct a dictionary of course mode information
# used to render the course list. We re-use the course modes dict # used to render the course list. We re-use the course modes dict
# we loaded earlier to avoid hitting the database. # we loaded earlier to avoid hitting the database.
...@@ -795,7 +791,7 @@ def dashboard(request): ...@@ -795,7 +791,7 @@ def dashboard(request):
'order_history_list': order_history_list, 'order_history_list': order_history_list,
'courses_requirements_not_met': courses_requirements_not_met, 'courses_requirements_not_met': courses_requirements_not_met,
'nav_hidden': True, 'nav_hidden': True,
'programs_by_run': inverted_programs, 'inverted_programs': inverted_programs,
'show_program_listing': ProgramsApiConfig.current().show_program_listing, 'show_program_listing': ProgramsApiConfig.current().show_program_listing,
'disable_courseware_js': True, 'disable_courseware_js': True,
'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard, 'display_course_modes_on_dashboard': enable_verified_certificates and display_course_modes_on_dashboard,
......
...@@ -16,7 +16,6 @@ import mock ...@@ -16,7 +16,6 @@ import mock
from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory, CourseFactory, CourseRunFactory 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.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.factories import UserCredential, ProgramCredential
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
...@@ -64,13 +63,7 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar ...@@ -64,13 +63,7 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
""" """
Helper function used to sort dictionaries representing programs. Helper function used to sort dictionaries representing programs.
""" """
try: return program['title']
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']
def credential_sort_key(self, credential): def credential_sort_key(self, credential):
""" """
...@@ -157,7 +150,7 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar ...@@ -157,7 +150,7 @@ class TestProgramListing(ProgramsApiConfigMixin, CredentialsApiConfigMixin, Shar
actual = sorted(actual, key=self.program_sort_key) actual = sorted(actual, key=self.program_sort_key)
for index, actual_program in enumerate(actual): 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) self.assert_dict_contains_subset(actual_program, expected_program)
def test_program_discovery(self, mock_get_programs): def test_program_discovery(self, mock_get_programs):
......
...@@ -6,12 +6,11 @@ from django.views.decorators.http import require_GET ...@@ -6,12 +6,11 @@ from django.views.decorators.http import require_GET
from edxmako.shortcuts import render_to_response from edxmako.shortcuts import render_to_response
from lms.djangoapps.learner_dashboard.utils import strip_course_id, FAKE_COURSE_KEY 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.credentials.utils import get_programs_credentials
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.utils import ( from openedx.core.djangoapps.programs.utils import (
get_program_marketing_url, get_program_marketing_url,
munge_progress_map,
ProgramProgressMeter, ProgramProgressMeter,
ProgramDataExtender, ProgramDataExtender,
) )
...@@ -27,16 +26,14 @@ def program_listing(request): ...@@ -27,16 +26,14 @@ def program_listing(request):
raise Http404 raise Http404
meter = ProgramProgressMeter(request.user) 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 = { context = {
'credentials': get_programs_credentials(request.user), 'credentials': get_programs_credentials(request.user),
'disable_courseware_js': True, 'disable_courseware_js': True,
'marketing_url': get_program_marketing_url(programs_config), 'marketing_url': get_program_marketing_url(programs_config),
'nav_hidden': True, 'nav_hidden': True,
'programs': engaged_programs, 'programs': meter.engaged_programs,
'progress': progress, 'progress': meter.progress,
'show_program_listing': programs_config.show_program_listing, 'show_program_listing': programs_config.show_program_listing,
'uses_pattern_library': True, 'uses_pattern_library': True,
} }
...@@ -56,7 +53,6 @@ def program_details(request, program_uuid): ...@@ -56,7 +53,6 @@ def program_details(request, program_uuid):
if not program_data: if not program_data:
raise Http404 raise Http404
program_data = munge_catalog_program(program_data)
program_data = ProgramDataExtender(program_data, request.user).extend() program_data = ProgramDataExtender(program_data, request.user).extend()
urls = { urls = {
......
...@@ -11,17 +11,17 @@ ...@@ -11,17 +11,17 @@
initialize: function(data) { initialize: function(data) {
if (data) { if (data) {
this.set({ this.set({
name: data.name, title: data.title,
category: data.category, type: data.type,
subtitle: data.subtitle, subtitle: data.subtitle,
organizations: data.organizations, authoring_organizations: data.authoring_organizations,
detailUrl: data.detail_url, detailUrl: data.detail_url,
smallBannerUrl: data.banner_image_urls.w348h116, xsmallBannerUrl: data.banner_image['x-small'].url,
mediumBannerUrl: data.banner_image_urls.w435h145, smallBannerUrl: data.banner_image.small.url,
largeBannerUrl: data.banner_image_urls.w726h242, mediumBannerUrl: data.banner_image.medium.url,
breakpoints: { breakpoints: {
max: { max: {
tiny: '320px', xsmall: '320px',
small: '540px', small: '540px',
medium: '768px', medium: '768px',
large: '979px' large: '979px'
......
...@@ -21,7 +21,7 @@ ...@@ -21,7 +21,7 @@
events: { events: {
'click .enroll-button': 'handleEnroll', 'click .enroll-button': 'handleEnroll',
'change .run-select': 'handleRunSelect' 'change .run-select': 'handleCourseRunSelect'
}, },
initialize: function(options) { initialize: function(options) {
...@@ -45,12 +45,12 @@ ...@@ -45,12 +45,12 @@
handleEnroll: function() { handleEnroll: function() {
// Enrollment click event handled here // Enrollment click event handled here
if (!this.model.get('course_key')) { if (!this.model.get('course_run_key')) {
this.$('.select-error').css('visibility', 'visible'); this.$('.select-error').css('visibility', 'visible');
} else if (!this.model.get('is_enrolled')) { } else if (!this.model.get('is_enrolled')) {
// actually enroll // Create the enrollment.
this.enrollModel.save({ this.enrollModel.save({
course_id: this.model.get('course_key') course_id: this.model.get('course_run_key')
}, { }, {
success: _.bind(this.enrollSuccess, this), success: _.bind(this.enrollSuccess, this),
error: _.bind(this.enrollError, this) error: _.bind(this.enrollError, this)
...@@ -58,24 +58,22 @@ ...@@ -58,24 +58,22 @@
} }
}, },
handleRunSelect: function(event) { handleCourseRunSelect: function(event) {
var runKey; var courseRunKey = $(event.target).val();
if (event.target) {
runKey = $(event.target).val(); if (courseRunKey) {
if (runKey) { this.model.updateCourseRun(courseRunKey);
this.model.updateRun(runKey); } else {
} else { // Set back the unselected states
// Set back the unselected states this.model.setUnselected();
this.model.setUnselected();
}
} }
}, },
enrollSuccess: function() { enrollSuccess: function() {
var courseKey = this.model.get('course_key'); var courseRunKey = this.model.get('course_run_key');
if (this.trackSelectionUrl) { if (this.trackSelectionUrl) {
// Go to track selection page // Go to track selection page
this.redirect(this.trackSelectionUrl + courseKey); this.redirect(this.trackSelectionUrl + courseRunKey);
} else { } else {
this.model.set({ this.model.set({
is_enrolled: true is_enrolled: true
...@@ -98,7 +96,7 @@ ...@@ -98,7 +96,7 @@
* This can occur, for example, when a course does not * This can occur, for example, when a course does not
* have a free enrollment mode, so we can't auto-enroll. * 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 @@ ...@@ -22,7 +22,7 @@
attributes: function() { attributes: function() {
return { return {
'aria-labelledby': 'program-' + this.model.get('id'), 'aria-labelledby': 'program-' + this.model.get('uuid'),
'role': 'group' 'role': 'group'
}; };
}, },
...@@ -33,14 +33,14 @@ ...@@ -33,14 +33,14 @@
this.progressCollection = data.context.progressCollection; this.progressCollection = data.context.progressCollection;
if (this.progressCollection) { if (this.progressCollection) {
this.progressModel = this.progressCollection.findWhere({ this.progressModel = this.progressCollection.findWhere({
id: this.model.get('id') uuid: this.model.get('uuid')
}); });
} }
this.render(); this.render();
}, },
render: function() { 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); return gettext(org.key);
}), }),
data = $.extend( data = $.extend(
...@@ -56,7 +56,7 @@ ...@@ -56,7 +56,7 @@
postRender: function() { postRender: function() {
// Add describedby to parent only if progess is present // Add describedby to parent only if progess is present
if (this.progressModel) { 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 || if (navigator.userAgent.indexOf('MSIE') !== -1 ||
...@@ -73,19 +73,14 @@ ...@@ -73,19 +73,14 @@
var progress = this.progressModel ? this.progressModel.toJSON() : false; var progress = this.progressModel ? this.progressModel.toJSON() : false;
if (progress) { 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 = progress.completed +
progress.total.in_progress + progress.in_progress +
progress.total.not_started; progress.not_started;
progress.percentage = { progress.percentage = {
completed: this.getWidth(progress.total.completed, progress.total.courses), completed: this.getWidth(progress.completed, progress.total),
in_progress: this.getWidth(progress.total.in_progress, progress.total.courses) in_progress: this.getWidth(progress.in_progress, progress.total)
}; };
} }
......
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
this.options = options; this.options = options;
this.programModel = new Backbone.Model(this.options.programData); this.programModel = new Backbone.Model(this.options.programData);
this.courseCardCollection = new CourseCardCollection( this.courseCardCollection = new CourseCardCollection(
this.programModel.get('course_codes'), this.programModel.get('courses'),
this.options.userPreferences this.options.userPreferences
); );
this.render(); this.render();
......
...@@ -5,10 +5,9 @@ define([ ...@@ -5,10 +5,9 @@ define([
'js/learner_dashboard/collections/program_collection', 'js/learner_dashboard/collections/program_collection',
'js/learner_dashboard/views/collection_list_view', 'js/learner_dashboard/views/collection_list_view',
'js/learner_dashboard/collections/program_progress_collection' 'js/learner_dashboard/collections/program_progress_collection'
], function(Backbone, $, ProgramCardView, ProgramCollection, CollectionListView, ], function(Backbone, $, ProgramCardView, ProgramCollection, CollectionListView, ProgressCollection) {
ProgressCollection) {
'use strict'; 'use strict';
/* jslint maxlen: 500 */ /* jslint maxlen: 500 */
describe('Collection List View', function() { describe('Collection List View', function() {
var view = null, var view = null,
...@@ -17,62 +16,90 @@ define([ ...@@ -17,62 +16,90 @@ define([
context = { context = {
programsData: [ programsData: [
{ {
category: 'xseries', uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
status: 'active', title: 'Food Security and Sustainability',
subtitle: 'program 1', subtitle: 'Learn how to feed all people in the world in a sustainable way.',
name: 'test program 1', type: 'XSeries',
organizations: [ 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'
}
},
authoring_organizations: [
{ {
display_name: 'edX', uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
key: 'edx' key: 'WageningenX',
name: 'Wageningen University & Research'
} }
], ]
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'
}
}, },
{ {
category: 'xseries', uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
status: 'active', title: 'edX Course Creator',
subtitle: 'fda', subtitle: 'Become an expert in creating courses for the edX platform.',
name: 'fda', type: 'XSeries',
organizations: [ 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'
}
},
authoring_organizations: [
{ {
display_name: 'edX', uuid: '4f8cb2c9-589b-4d1e-88c1-b01a02db3a9c',
key: 'edx' key: 'edX',
name: 'edX'
} }
], ]
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'
}
} }
], ],
userProgress: [ userProgress: [
{ {
id: 146, uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
completed: ['courses', 'the', 'user', 'completed'], completed: 4,
in_progress: ['in', 'progress'], in_progress: 2,
not_started: ['courses', 'not', 'yet', 'started'] not_started: 4
}, },
{ {
id: 147, uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
completed: ['Course 1'], completed: 1,
in_progress: [], in_progress: 0,
not_started: ['Course 2', 'Course 3', 'Course 4'] not_started: 3
} }
] ]
}; };
...@@ -105,7 +132,8 @@ define([ ...@@ -105,7 +132,8 @@ define([
var $cards = view.$el.find('.program-card'); var $cards = view.$el.find('.program-card');
expect($cards.length).toBe(2); expect($cards.length).toBe(2);
$cards.each(function(index, el) { $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([ ...@@ -116,13 +144,14 @@ define([
view = new CollectionListView({ view = new CollectionListView({
el: '.program-cards-container', el: '.program-cards-container',
childView: ProgramCardView, childView: ProgramCardView,
context: {'xseriesUrl': '/programs'}, context: {},
collection: programCollection collection: programCollection
}); });
view.render(); view.render();
$cards = view.$el.find('.program-card'); $cards = view.$el.find('.program-card');
expect($cards.length).toBe(0); expect($cards.length).toBe(0);
}); });
it('should have no title when title not provided', function() { it('should have no title when title not provided', function() {
var $title; var $title;
setFixtures('<div class="test-container"><div class="program-cards-container"></div></div>'); setFixtures('<div class="test-container"><div class="program-cards-container"></div></div>');
...@@ -132,15 +161,18 @@ define([ ...@@ -132,15 +161,18 @@ define([
$title = view.$el.parent().find('.collection-title'); $title = view.$el.parent().find('.collection-title');
expect($title.html()).not.toBeDefined(); expect($title.html()).not.toBeDefined();
}); });
it('should display screen reader header when provided', function() { 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(); view.remove();
setFixtures('<div class="test-container"><div class="program-cards-container"></div></div>'); setFixtures('<div class="test-container"><div class="program-cards-container"></div></div>');
programCollection = new ProgramCollection(context.programsData); programCollection = new ProgramCollection(context.programsData);
view = new CollectionListView({ view = new CollectionListView({
el: '.program-cards-container', el: '.program-cards-container',
childView: ProgramCardView, childView: ProgramCardView,
context: {'xseriesUrl': '/programs'}, context: context,
collection: programCollection, collection: programCollection,
titleContext: titleContext titleContext: titleContext
}); });
......
define([ define([
'backbone', 'backbone',
'underscore',
'jquery', 'jquery',
'js/learner_dashboard/collections/program_progress_collection', 'js/learner_dashboard/collections/program_progress_collection',
'js/learner_dashboard/models/program_model', 'js/learner_dashboard/models/program_model',
'js/learner_dashboard/views/program_card_view' 'js/learner_dashboard/views/program_card_view'
], function(Backbone, $, ProgressCollection, ProgramModel, ProgramCardView) { ], function(Backbone, _, $, ProgressCollection, ProgramModel, ProgramCardView) {
'use strict'; 'use strict';
/* jslint maxlen: 500 */ /* jslint maxlen: 500 */
describe('Program card View', function() { describe('Program card View', function() {
var view = null, var view = null,
programModel, programModel,
program = { program = {
category: 'FooBar', uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
status: 'active', title: 'Food Security and Sustainability',
subtitle: 'program 1', subtitle: 'Learn how to feed all people in the world in a sustainable way.',
name: 'test program 1', type: 'XSeries',
organizations: [ 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'
}
},
authoring_organizations: [
{ {
display_name: 'edX', uuid: '0c6e5fa2-96e8-40b2-9ebe-c8b0df2a3b22',
key: 'edx' key: 'WageningenX',
name: 'Wageningen University & Research'
} }
], ]
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'
}
}, },
userProgress = [ userProgress = [
{ {
id: 146, uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
completed: ['courses', 'the', 'user', 'completed'], completed: 4,
in_progress: ['in', 'progress'], in_progress: 2,
not_started: ['courses', 'not', 'yet', 'started'] not_started: 4
}, },
{ {
id: 147, uuid: '91d144d2-1bb1-4afe-90df-d5cff63fa6e2',
completed: ['Course 1'], completed: 1,
in_progress: [], in_progress: 0,
not_started: ['Course 2', 'Course 3', 'Course 4'] not_started: 3
} }
], ],
progressCollection = new ProgressCollection(), progressCollection = new ProgressCollection(),
cardRenders = function($card) { cardRenders = function($card) {
expect($card).toBeDefined(); expect($card).toBeDefined();
expect($card.find('.title').html().trim()).toEqual(program.name); expect($card.find('.title').html().trim()).toEqual(program.title);
expect($card.find('.category span').html().trim()).toEqual(program.category); expect($card.find('.category span').html().trim()).toEqual(program.type);
expect($card.find('.organization').html().trim()).toEqual(program.organizations[0].key); expect($card.find('.organization').html().trim()).toEqual(program.authoring_organizations[0].key);
expect($card.find('.card-link').attr('href')).toEqual(program.detail_url); expect($card.find('.card-link').attr('href')).toEqual(program.detail_url);
}; };
...@@ -87,12 +102,16 @@ define([ ...@@ -87,12 +102,16 @@ define([
}); });
it('should handle exceptions from reEvaluatePicture', function() { it('should handle exceptions from reEvaluatePicture', function() {
var message = 'Picturefill had exceptions';
spyOn(view, 'reEvaluatePicture').and.callFake(function() { spyOn(view, 'reEvaluatePicture').and.callFake(function() {
throw {name: 'Picturefill had exceptions'}; var error = {name: message};
throw error;
}); });
view.reLoadBannerImage(); view.reLoadBannerImage();
expect(view.reEvaluatePicture).toHaveBeenCalled(); 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() { it('should calculate the correct percentages for progress bars', function() {
...@@ -101,11 +120,12 @@ define([ ...@@ -101,11 +120,12 @@ define([
}); });
it('should display the correct completed courses message', function() { it('should display the correct completed courses message', function() {
var program = _.findWhere(userProgress, {id: 146}), var programProgress = _.findWhere(userProgress, {uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8'}),
completed = program.completed.length, completed = programProgress.completed,
total = completed + program.in_progress.length + program.not_started.length; 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() { it('should render cards if there is no progressData', function() {
......
...@@ -12,23 +12,42 @@ define([ ...@@ -12,23 +12,42 @@ define([
program_listing_url: '/dashboard/programs' program_listing_url: '/dashboard/programs'
}, },
programData: { programData: {
uuid: '12-ab', uuid: 'a87e5eac-3c93-45a1-a8e1-4c79ca8401c8',
name: 'Astrophysics', title: 'Food Security and Sustainability',
subtitle: 'Learn contemporary astrophysics from the leaders in the field.', subtitle: 'Learn how to feed all people in the world in a sustainable way.',
category: 'xseries', type: 'XSeries',
organizations: [ detail_url: 'https://www.edx.org/foo/bar',
{ banner_image: {
display_name: 'Australian National University', medium: {
img: 'common/test/data/static/picture1.jpg', height: 242,
key: 'ANUx' 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([ ...@@ -51,13 +70,13 @@ define([
it('should render the header based on the passed in model', function() { 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'); 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.$('.subtitle').html()).toEqual(context.programData.subtitle);
expect(view.$('.org-logo').length).toEqual(context.programData.organizations.length); expect(view.$('.org-logo').length).toEqual(context.programData.authoring_organizations.length);
expect(view.$('.org-logo').attr('src')).toEqual(context.programData.organizations[0].img); expect(view.$('.org-logo').attr('src'))
expect(view.$('.org-logo').attr('alt')).toEqual( .toEqual(context.programData.authoring_organizations[0].certificate_logo_image_url);
context.programData.organizations[0].display_name + '\'s logo' expect(view.$('.org-logo').attr('alt'))
); .toEqual(context.programData.authoring_organizations[0].name + '\'s logo');
expect(programListUrl).toEqual(context.urls.program_listing_url); expect(programListUrl).toEqual(context.urls.program_listing_url);
}); });
}); });
......
...@@ -98,7 +98,7 @@ from openedx.core.djangolib.markup import HTML, Text ...@@ -98,7 +98,7 @@ from openedx.core.djangolib.markup import HTML, Text
<% is_course_blocked = (enrollment.course_id in block_courses) %> <% is_course_blocked = (enrollment.course_id in block_courses) %>
<% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %> <% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %>
<% course_requirements = courses_requirements_not_met.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' /> <%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 % endfor
......
...@@ -300,8 +300,8 @@ from student.helpers import ( ...@@ -300,8 +300,8 @@ from student.helpers import (
<ul> <ul>
% for program in related_programs: % for program in related_programs:
<li> <li>
<span class="category-icon ${program['category'].lower()}-icon" aria-hidden="true"></span> <span class="category-icon ${program['type'].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><a href="${program['detail_url']}">${u'{title} {type}'.format(title=program['title'], type=program['type'])}</a></span>
</li> </li>
% endfor % endfor
</ul> </ul>
...@@ -397,12 +397,6 @@ from student.helpers import ( ...@@ -397,12 +397,6 @@ from student.helpers import (
</div> </div>
%endif %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: % if is_course_blocked:
<p id="block-course-msg" class="course-block"> <p id="block-course-msg" class="course-block">
${Text(_("You can no longer access this course because payment has not yet been received. " ${Text(_("You can no longer access this course because payment has not yet been received. "
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
class="header-img" class="header-img"
src="<%- course_image_url %>" src="<%- course_image_url %>"
<% // safe-lint: disable=underscore-not-escaped %> <% // 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> </a>
<% } else { %> <% } else { %>
<img class="header-img" src="<%- course_image_url %>" alt=""/> <img class="header-img" src="<%- course_image_url %>" alt=""/>
...@@ -18,10 +18,10 @@ ...@@ -18,10 +18,10 @@
<h3 class="course-title"> <h3 class="course-title">
<% if ( marketing_url || course_url ) { %> <% if ( marketing_url || course_url ) { %>
<a href="<%- marketing_url || course_url %>" class="course-title-link"> <a href="<%- marketing_url || course_url %>" class="course-title-link">
<%- display_name %> <%- title %>
</a> </a>
<% } else { %> <% } else { %>
<%- display_name %> <%- title %>
<% } %> <% } %>
</h3> </h3>
<div class="course-text"> <div class="course-text">
...@@ -29,7 +29,7 @@ ...@@ -29,7 +29,7 @@
<span class="run-period"><%- start_date %> - <%- end_date %></span> <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> </div>
</div> </div>
......
...@@ -10,9 +10,9 @@ ...@@ -10,9 +10,9 @@
</a> </a>
<% } %> <% } %>
<% } else { %> <% } else { %>
<% if (enrollable_run_modes.length > 0) { %> <% if (enrollable_course_runs.length > 0) { %>
<div class="enrollment-info"><%- gettext('not enrolled') %></div> <div class="enrollment-info"><%- gettext('Not Enrolled') %></div>
<% if (enrollable_run_modes.length > 1) { %> <% if (enrollable_course_runs.length > 1) { %>
<div class="run-select-container"> <div class="run-select-container">
<div class="select-error"> <div class="select-error">
<%- gettext('Please select a course date') %> <%- gettext('Please select a course date') %>
...@@ -24,16 +24,16 @@ ...@@ -24,16 +24,16 @@
<option value="" selected="selected"> <option value="" selected="selected">
<%- gettext('Choose Course Date') %> <%- gettext('Choose Course Date') %>
</option> </option>
<% _.each (enrollable_run_modes, function(runMode) { %> <% _.each (enrollable_course_runs, function(courseRun) { %>
<option <option
value="<%- runMode.run_key %>" value="<%- courseRun.key %>"
<% if (run_key === runMode.run_key) { %> <% if (key === courseRun.key) { %>
selected="selected" selected="selected"
<% }%> <% }%>
> >
<%= interpolate( <%= interpolate(
gettext('Starts %(start)s'), gettext('Starts %(start)s'),
{ start: runMode.start_date }, { start: courseRun.start_date },
true) true)
%> %>
</option> </option>
...@@ -44,14 +44,14 @@ ...@@ -44,14 +44,14 @@
<button type="button" class="btn-brand btn cta-primary enroll-button"> <button type="button" class="btn-brand btn cta-primary enroll-button">
<%- gettext('Enroll Now') %> <%- gettext('Enroll Now') %>
</button> </button>
<% } else if (upcoming_run_modes.length > 0) {%> <% } else if (upcoming_course_runs.length > 0) {%>
<div class="no-action-message"> <div class="no-action-message">
<%- gettext('Coming Soon') %> <%- gettext('Coming Soon') %>
</div> </div>
<div class="enrollment-opens"> <div class="enrollment-opens">
<%- gettext('Enrollment Opens on') %> <%- gettext('Enrollment Opens on') %>
<span class="enrollment-open-date"> <span class="enrollment-open-date">
<%- upcoming_run_modes[0].enrollment_open_date %> <%- upcoming_course_runs[0].enrollment_open_date %>
</span> </span>
</div> </div>
<% } else { %> <% } else { %>
......
<div class="text-section"> <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="meta-info grid-container">
<div class="organization col"><%- orgList %></div> <div class="organization col"><%- orgList %></div>
<div class="category col col-last"> <div class="category col col-last">
<span class="category-text"><%- gettext(category) %></span> <span class="category-text"><%- gettext(type) %></span>
<span class="category-icon <%- category.toLowerCase() %>-icon" aria-hidden="true"></span> <span class="category-icon <%- type.toLowerCase() %>-icon" aria-hidden="true"></span>
</div> </div>
</div> </div>
<% if (progress) { %> <% if (progress) { %>
<p class="certificate-status"> <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( ngettext(
'%(count)s course is in progress.', '%(count)s course is in progress.',
'%(count)s courses are 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>
<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( ngettext(
'%(count)s course has not been started.', '%(count)s course has not been started.',
'%(count)s courses have 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> ) %></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.'), 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> ) %></span>
</p> </p>
<% } %> <% } %>
...@@ -44,11 +44,11 @@ ...@@ -44,11 +44,11 @@
<a href="<%- detailUrl %>" class="card-link"> <a href="<%- detailUrl %>" class="card-link">
<div class="banner-image-container"> <div class="banner-image-container">
<picture> <picture>
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.tiny %>)"> <source srcset="<%- xsmallBannerUrl %>" media="(max-width: <%- breakpoints.max.xsmall %>)">
<source srcset="<%- mediumBannerUrl %>" media="(max-width: <%- breakpoints.max.small %>)"> <source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.small %>)">
<source srcset="<%- largeBannerUrl %>" media="(max-width: <%- breakpoints.max.medium %>)"> <source srcset="<%- mediumBannerUrl %>" media="(max-width: <%- breakpoints.max.medium %>)">
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.large %>)"> <source srcset="<%- xsmallBannerUrl %>" media="(max-width: <%- breakpoints.max.large %>)">
<img class="banner-image" srcset="<%- mediumBannerUrl %>" alt="<%= interpolate(gettext('%(programName)s Home Page.'), {programName: name}, true)%>"> <img class="banner-image" srcset="<%- smallBannerUrl %>" alt="<%= interpolate(gettext('%(programName)s Home Page.'), {programName: title}, true)%>">
</picture> </picture>
</div> </div>
</a> </a>
<div class="banner-background-wrapper"> <div class="banner-background-wrapper">
<picture> <picture>
<source srcset="<%- programData.banner_image_urls.w1440h480 %>" media="(min-width: <%- breakpoints.min.large %>)"> <source srcset="<%- programData.banner_image.large.url %>" media="(min-width: <%- breakpoints.min.large %>)">
<source srcset="<%- programData.banner_image_urls.w726h242 %>" media="(min-width: <%- breakpoints.min.medium %>)"> <source srcset="<%- programData.banner_image.medium.url %>" media="(min-width: <%- breakpoints.min.medium %>)">
<img class="banner-background-image" srcset="<%- programData.banner_image_urls.w348h116 %>" alt=""> <img class="banner-background-image" srcset="<%- programData.banner_image['x-small'].url %>" alt="">
</picture> </picture>
<div class="banner-content grid-container"> <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> <p class="hd-4 subtitle row"><%- programData.subtitle %></p>
<% if (programData.organizations.length) { %> <% if (programData.authoring_organizations.length) { %>
<div class="org-wrapper"> <div class="org-wrapper">
<% _.each(programData.organizations, function(org) { %> <% _.each(programData.authoring_organizations, function(org) { %>
<img src="<%- org.img %>" class="org-logo" alt="<%- StringUtils.interpolate( <img src="<%- org.certificate_logo_image_url || org.logo_image_url %>" class="org-logo" alt="<%- StringUtils.interpolate(
gettext('{organization}\'s logo'), gettext('{organization}\'s logo'),
{organization: org.display_name} {organization: org.name}
) %>"> ) %>">
<% }) %> <% }) %>
</div> </div>
...@@ -33,7 +33,7 @@ ...@@ -33,7 +33,7 @@
<span class="crumb-separator fa fa-chevron-right" aria-hidden="true"></span> <span class="crumb-separator fa fa-chevron-right" aria-hidden="true"></span>
</li> </li>
<li class="crumb active"> <li class="crumb active">
<%- programData.name %> <%- programData.title %>
</li> </li>
</ol> </ol>
</nav> </nav>
...@@ -13,7 +13,6 @@ from openedx.core.djangoapps.catalog.tests.factories import ProgramFactory, Prog ...@@ -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.tests.mixins import CatalogIntegrationMixin
from openedx.core.djangoapps.catalog.utils import ( from openedx.core.djangoapps.catalog.utils import (
get_programs, get_programs,
munge_catalog_program,
get_program_types, get_program_types,
get_programs_with_type_logo, get_programs_with_type_logo,
) )
...@@ -131,63 +130,6 @@ class TestGetPrograms(CatalogIntegrationMixin, TestCase): ...@@ -131,63 +130,6 @@ class TestGetPrograms(CatalogIntegrationMixin, TestCase):
self.assertEqual(data, []) 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 @skip_unless_lms
@mock.patch(UTILS_MODULE + '.get_edx_api_data') @mock.patch(UTILS_MODULE + '.get_edx_api_data')
class TestGetProgramTypes(CatalogIntegrationMixin, TestCase): class TestGetProgramTypes(CatalogIntegrationMixin, TestCase):
......
...@@ -66,64 +66,6 @@ def get_programs(uuid=None, type=None): # pylint: disable=redefined-builtin ...@@ -66,64 +66,6 @@ def get_programs(uuid=None, type=None): # pylint: disable=redefined-builtin
return [] 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(): def get_program_types():
"""Retrieve all program types from the catalog service. """Retrieve all program types from the catalog service.
......
...@@ -4,14 +4,11 @@ import factory ...@@ -4,14 +4,11 @@ import factory
from faker import Faker from faker import Faker
fake = Faker()
class ProgressFactory(factory.Factory): class ProgressFactory(factory.Factory):
class Meta(object): class Meta(object):
model = dict model = dict
uuid = factory.Faker('uuid4') uuid = factory.Faker('uuid4')
completed = [] completed = 0
in_progress = [] in_progress = 0
not_started = [] not_started = 0
...@@ -18,7 +18,6 @@ from openedx.core.djangoapps.content.course_overviews.models import CourseOvervi ...@@ -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 openedx.core.lib.edx_api_utils import get_edx_api_data
from student.models import CourseEnrollment from student.models import CourseEnrollment
from util.date_utils import strftime_localized 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. # 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) ...@@ -26,7 +25,7 @@ DEFAULT_ENROLLMENT_START_DATE = datetime.datetime(1900, 1, 1, tzinfo=utc)
def get_program_marketing_url(programs_config): 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('/') return urljoin(settings.MKTG_URLS.get('ROOT'), programs_config.marketing_path).rstrip('/')
...@@ -47,18 +46,6 @@ def attach_program_detail_url(programs): ...@@ -47,18 +46,6 @@ def attach_program_detail_url(programs):
return 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): class ProgramProgressMeter(object):
"""Utility for gauging a user's progress towards program completion. """Utility for gauging a user's progress towards program completion.
...@@ -139,19 +126,15 @@ class ProgramProgressMeter(object): ...@@ -139,19 +126,15 @@ class ProgramProgressMeter(object):
""" """
progress = [] progress = []
for program in self.engaged_programs: for program in self.engaged_programs:
completed, in_progress, not_started = [], [], [] completed, in_progress, not_started = 0, 0, 0
for course in program['courses']: 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): if self._is_course_complete(course):
completed.append(title) completed += 1
elif self._is_course_in_progress(course): elif self._is_course_in_progress(course):
in_progress.append(title) in_progress += 1
else: else:
not_started.append(title) not_started += 1
progress.append({ progress.append({
'uuid': program['uuid'], 'uuid': program['uuid'],
...@@ -249,18 +232,18 @@ class ProgramProgressMeter(object): ...@@ -249,18 +232,18 @@ class ProgramProgressMeter(object):
# pylint: disable=missing-docstring # pylint: disable=missing-docstring
class ProgramDataExtender(object): class ProgramDataExtender(object):
""" """
Utility for extending program course codes with CourseOverview and Utility for extending program data meant for the program detail page with
CourseEnrollment data. user-specific (e.g., CourseEnrollment) data.
Arguments: Arguments:
program_data (dict): Representation of a program. Note that this dict must program_data (dict): Representation of a program.
be formatted as if it was returned by the deprecated program service.
user (User): The user whose enrollments to inspect. user (User): The user whose enrollments to inspect.
""" """
def __init__(self, program_data, user): def __init__(self, program_data, user):
self.data = program_data self.data = program_data
self.user = user self.user = user
self.course_key = None
self.course_run_key = None
self.course_overview = None self.course_overview = None
self.enrollment_start = None self.enrollment_start = None
...@@ -278,77 +261,62 @@ class ProgramDataExtender(object): ...@@ -278,77 +261,62 @@ class ProgramDataExtender(object):
"""Returns a generator yielding method names beginning with the given prefix.""" """Returns a generator yielding method names beginning with the given prefix."""
return (name for name in cls.__dict__ if name.startswith(prefix)) return (name for name in cls.__dict__ if name.startswith(prefix))
def _extend_organizations(self): def _extend_course_runs(self):
"""Execute organization data handlers.""" """Execute course run data handlers."""
for organization in self.data['organizations']: for course in self.data['courses']:
self._execute('_attach_organization', organization) for course_run in course['course_runs']:
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']:
# State to be shared across handlers. # State to be shared across handlers.
self.course_key = CourseKey.from_string(run_mode['course_key']) self.course_run_key = CourseKey.from_string(course_run['key'])
self.course_overview = CourseOverview.get_from_id(self.course_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.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): def _attach_course_run_certificate_url(self, run_mode):
certificate_data = certificate_api.certificate_downloadable_status(self.user, self.course_key) certificate_data = certificate_api.certificate_downloadable_status(self.user, self.course_run_key)
certificate_uuid = certificate_data.get('uuid') certificate_uuid = certificate_data.get('uuid')
run_mode['certificate_url'] = certificate_api.get_certificate_url( 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 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. # if web certificates are not configured for a given course.
course_id=self.course_key, course_id=self.course_run_key,
uuid=certificate_uuid, uuid=certificate_uuid,
) if certificate_uuid else None ) if certificate_uuid else None
def _attach_run_mode_course_image_url(self, run_mode): def _attach_course_run_course_url(self, run_mode):
run_mode['course_image_url'] = self.course_overview.course_image_url 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') 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) end_date = self.course_overview.end or datetime.datetime.max.replace(tzinfo=utc)
run_mode['is_course_ended'] = end_date < datetime.datetime.now(utc) run_mode['is_course_ended'] = end_date < datetime.datetime.now(utc)
def _attach_run_mode_is_enrolled(self, run_mode): def _attach_course_run_is_enrolled(self, run_mode):
run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(self.user, self.course_key) 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) 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 run_mode['is_enrollment_open'] = self.enrollment_start <= datetime.datetime.now(utc) < enrollment_end
def _attach_run_mode_start_date(self, run_mode): def _attach_course_run_advertised_start(self, run_mode):
run_mode['start_date'] = self.course_overview.start """
The advertised_start is text a course author can provide to be displayed
def _attach_run_mode_advertised_start(self, run_mode): 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 run_mode['advertised_start'] = self.course_overview.advertised_start
def _attach_run_mode_upgrade_url(self, run_mode): def _attach_course_run_upgrade_url(self, run_mode):
required_mode_slug = run_mode['mode_slug'] required_mode_slug = run_mode['type']
enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_key) enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(self.user, self.course_run_key)
is_mode_mismatch = required_mode_slug != enrolled_mode_slug 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: if is_upgrade_required:
# Requires that the ecommerce service be in use. # 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() ecommerce = EcommerceService()
sku = getattr(required_mode, 'sku', None) sku = getattr(required_mode, 'sku', None)
......
...@@ -100,7 +100,7 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers ...@@ -100,7 +100,7 @@ from openedx.core.djangoapps.theming import helpers as theming_helpers
<% is_course_blocked = (enrollment.course_id in block_courses) %> <% is_course_blocked = (enrollment.course_id in block_courses) %>
<% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %> <% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %>
<% course_requirements = courses_requirements_not_met.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" /> <%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 % endfor
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment