Commit d7381885 by Renzo Lucioni

Add upgrade section to program detail course cards.

Displays a message and link when a user is enrolled in a program's course run but must upgrade in order to be eligible for the program certificate. ECOM-4220.
parent 69410948
...@@ -67,8 +67,9 @@ def program_details(request, program_id): ...@@ -67,8 +67,9 @@ def program_details(request, program_id):
urls = { urls = {
'program_listing_url': reverse('program_listing_view'), 'program_listing_url': reverse('program_listing_view'),
'track_selection_url': strip_course_id( 'track_selection_url': strip_course_id(
reverse('course_modes_choose', kwargs={'course_id': FAKE_COURSE_KEY})), reverse('course_modes_choose', kwargs={'course_id': FAKE_COURSE_KEY})
'commerce_api_url': reverse('commerce_api:v0:baskets:create') ),
'commerce_api_url': reverse('commerce_api:v0:baskets:create'),
} }
context = { context = {
......
...@@ -65,17 +65,18 @@ ...@@ -65,17 +65,18 @@
course_key: runMode.course_key, course_key: runMode.course_key,
course_url: runMode.course_url || '', course_url: runMode.course_url || '',
display_name: this.context.display_name, display_name: this.context.display_name,
start_date: runMode.start_date,
end_date: runMode.end_date, end_date: runMode.end_date,
enrollable_run_modes: this.getEnrollableRunModes(),
enrollment_open_date: runMode.enrollment_open_date || '',
is_course_ended: runMode.is_course_ended,
is_enrolled: runMode.is_enrolled, is_enrolled: runMode.is_enrolled,
is_enrollment_open: runMode.is_enrollment_open, is_enrollment_open: runMode.is_enrollment_open,
key: this.context.key, key: this.context.key,
marketing_url: runMode.marketing_url || '', marketing_url: runMode.marketing_url || '',
is_course_ended: runMode.is_course_ended,
mode_slug: runMode.mode_slug, mode_slug: runMode.mode_slug,
run_key: runMode.run_key, run_key: runMode.run_key,
enrollment_open_date: runMode.enrollment_open_date || '', start_date: runMode.start_date,
enrollable_run_modes: this.getEnrollableRunModes() upgrade_url: runMode.upgrade_url
}); });
} }
}, },
......
...@@ -27,13 +27,10 @@ ...@@ -27,13 +27,10 @@
}, },
render: function() { render: function() {
var data = this.model.toJSON(), var data = this.model.toJSON();
$icons;
data = $.extend(data, {certificateSvg: this.iconTpl()});
HtmlUtils.setHtml(this.$el, this.statusTpl(data)); HtmlUtils.setHtml(this.$el, this.statusTpl(data));
$icons = this.$('.certificate-icon');
HtmlUtils.setHtml($icons, this.iconTpl());
} }
}); });
} }
......
...@@ -7,6 +7,7 @@ ...@@ -7,6 +7,7 @@
'gettext', 'gettext',
'edx-ui-toolkit/js/utils/html-utils', 'edx-ui-toolkit/js/utils/html-utils',
'js/learner_dashboard/models/course_enroll_model', 'js/learner_dashboard/models/course_enroll_model',
'js/learner_dashboard/views/upgrade_message_view',
'js/learner_dashboard/views/certificate_status_view', 'js/learner_dashboard/views/certificate_status_view',
'js/learner_dashboard/views/course_enroll_view', 'js/learner_dashboard/views/course_enroll_view',
'text!../../../templates/learner_dashboard/course_card.underscore' 'text!../../../templates/learner_dashboard/course_card.underscore'
...@@ -18,6 +19,7 @@ ...@@ -18,6 +19,7 @@
gettext, gettext,
HtmlUtils, HtmlUtils,
EnrollModel, EnrollModel,
UpgradeMessageView,
CertificateStatusView, CertificateStatusView,
CourseEnrollView, CourseEnrollView,
pageTpl pageTpl
...@@ -44,7 +46,8 @@ ...@@ -44,7 +46,8 @@
}, },
postRender: function(){ postRender: function(){
var $certStatus = this.$('.certificate-status'); var $upgradeMessage = this.$('.upgrade-message'),
$certStatus = this.$('.certificate-status');
this.enrollView = new CourseEnrollView({ this.enrollView = new CourseEnrollView({
$parentEl: this.$('.course-actions'), $parentEl: this.$('.course-actions'),
...@@ -53,13 +56,23 @@ ...@@ -53,13 +56,23 @@
enrollModel: this.enrollModel enrollModel: this.enrollModel
}); });
if ( this.model.get('certificate_url') ) { if ( this.model.get('upgrade_url') ) {
this.upgradeMessage = new UpgradeMessageView({
$el: $upgradeMessage,
model: this.model
});
$certStatus.remove();
} else if ( this.model.get('certificate_url') ) {
this.certificateStatus = new CertificateStatusView({ this.certificateStatus = new CertificateStatusView({
$el: $certStatus, $el: $certStatus,
model: this.model model: this.model
}); });
$upgradeMessage.remove();
} else { } else {
// Styles are applied to the element that show if it's empty // Styles are applied to these elements which will be visible if they're empty.
$upgradeMessage.remove();
$certStatus.remove(); $certStatus.remove();
} }
} }
......
;(function (define) {
'use strict';
define(['backbone',
'jquery',
'underscore',
'gettext',
'edx-ui-toolkit/js/utils/html-utils',
'text!../../../templates/learner_dashboard/upgrade_message.underscore',
'text!../../../templates/learner_dashboard/certificate_icon.underscore'
],
function(
Backbone,
$,
_,
gettext,
HtmlUtils,
upgradeMessageTpl,
certificateIconTpl
) {
return Backbone.View.extend({
messageTpl: HtmlUtils.template(upgradeMessageTpl),
iconTpl: HtmlUtils.template(certificateIconTpl),
initialize: function(options) {
this.$el = options.$el;
this.render();
},
render: function() {
var data = this.model.toJSON();
data = $.extend(data, {certificateSvg: this.iconTpl()});
HtmlUtils.setHtml(this.$el, this.messageTpl(data));
}
});
}
);
}).call(this, define || RequireJS.define);
...@@ -19,27 +19,30 @@ define([ ...@@ -19,27 +19,30 @@ define([
key: 'ANUx' key: 'ANUx'
}, },
run_modes: [{ run_modes: [{
start_date: 'Apr 25, 2016', certificate_url: '',
end_date: 'Jun 13, 2019', course_image_url: 'http://test.com/image1',
course_key: 'course-v1:ANUx+ANU-ASTRO1x+3T2015', course_key: 'course-v1:ANUx+ANU-ASTRO1x+3T2015',
course_started: true,
course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info', course_url: 'http://localhost:8000/courses/course-v1:edX+DemoX+Demo_Course/info',
end_date: 'Jun 13, 2019',
enrollment_open_date: 'Mar 03, 2016',
is_course_ended: false,
is_enrolled: true,
is_enrollment_open: true,
marketing_url: 'https://www.edx.org/course/astrophysics-exploring', marketing_url: 'https://www.edx.org/course/astrophysics-exploring',
course_image_url: 'http://test.com/image1',
mode_slug: 'verified', mode_slug: 'verified',
run_key: '2T2016', run_key: '2T2016',
course_started: true, start_date: 'Apr 25, 2016',
is_enrolled: true, upgrade_url: ''
is_course_ended: false,
is_enrollment_open: true,
certificate_url: '',
enrollment_open_date: 'Mar 03, 2016'
}] }]
}, },
setupView = function(data, isEnrolled){ setupView = function(data, isEnrolled){
data.run_modes[0].is_enrolled = isEnrolled; var programData = $.extend({}, data);
programData.run_modes[0].is_enrolled = isEnrolled;
setFixtures('<div class="course-card card"></div>'); setFixtures('<div class="course-card card"></div>');
courseCardModel = new CourseCardModel(data); courseCardModel = new CourseCardModel(programData);
view = new CourseCardView({ view = new CourseCardView({
model: courseCardModel model: courseCardModel
}); });
...@@ -86,37 +89,71 @@ define([ ...@@ -86,37 +89,71 @@ define([
}); });
it('should only show certificate status section if a certificate has been earned', function() { it('should only show certificate status section if a certificate has been earned', function() {
var data = context, var data = $.extend({}, context),
certUrl = 'sample-certificate'; certUrl = 'sample-certificate';
setupView(context, false); expect(view.$('.certificate-status').length).toEqual(0);
expect(view.$('certificate-status').length).toEqual(0);
view.remove(); view.remove();
data.run_modes[0].certificate_url = certUrl; data.run_modes[0].certificate_url = certUrl;
setupView(data, false); setupView(data, false);
expect(view.$('.certificate-status').length).toEqual(1); expect(view.$('.certificate-status').length).toEqual(1);
expect(view.$('.certificate-status .cta-secondary').attr('href')).toEqual(certUrl); expect(view.$('.certificate-status .cta-secondary').attr('href')).toEqual(certUrl);
}); });
it('should render the course card with coming soon', function(){ it('should only show upgrade message section if an upgrade is required', function() {
var data = $.extend({}, context),
upgradeUrl = '/path/to/upgrade';
expect(view.$('.upgrade-message').length).toEqual(0);
view.remove(); view.remove();
context.run_modes[0].is_enrollment_open = false;
setupView(context, false); data.run_modes[0].upgrade_url = upgradeUrl;
expect(view.$('.header-img').attr('src')).toEqual(context.run_modes[0].course_image_url); setupView(data, false);
expect(view.$('.course-details .course-title').text().trim()).toEqual(context.display_name); 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() {
var data = $.extend({}, context);
// Verify that no empty elements are left in the DOM.
data.run_modes[0].upgrade_url = '';
data.run_modes[0].certificate_url = '';
setupView(data, false);
expect(view.$('.upgrade-message').length).toEqual(0);
expect(view.$('.certificate-status').length).toEqual(0);
view.remove();
// Verify that the upgrade message takes priority.
data.run_modes[0].upgrade_url = '/path/to/upgrade';
data.run_modes[0].certificate_url = '/path/to/certificate';
setupView(data, false);
expect(view.$('.upgrade-message').length).toEqual(1);
expect(view.$('.certificate-status').length).toEqual(0);
});
it('should render the course card with coming soon', function(){
var data = $.extend({}, context);
data.run_modes[0].is_enrollment_open = false;
setupView(data, false);
expect(view.$('.header-img').attr('src')).toEqual(data.run_modes[0].course_image_url);
expect(view.$('.course-details .course-title').text().trim()).toEqual(data.display_name);
expect(view.$('.course-details .course-title-link').length).toBe(0); expect(view.$('.course-details .course-title-link').length).toBe(0);
expect(view.$('.course-details .course-text .course-key').html()).toEqual(context.key); expect(view.$('.course-details .course-text .course-key').html()).toEqual(data.key);
expect(view.$('.course-details .course-text .run-period').length).toBe(0); expect(view.$('.course-details .course-text .run-period').length).toBe(0);
expect(view.$('.no-action-message').text().trim()).toBe('Coming Soon'); expect(view.$('.no-action-message').text().trim()).toBe('Coming Soon');
expect(view.$('.enrollment-open-date').text().trim()) expect(view.$('.enrollment-open-date').text().trim())
.toEqual(context.run_modes[0].enrollment_open_date); .toEqual(data.run_modes[0].enrollment_open_date);
}); });
it('should render if enrollment_open_date is not provided', function(){ it('should render if enrollment_open_date is not provided', function(){
view.remove(); var data = $.extend({}, context);
context.run_modes[0].is_enrollment_open = true;
delete context.run_modes[0].enrollment_open_date; data.run_modes[0].is_enrollment_open = true;
setupView(context, false); delete data.run_modes[0].enrollment_open_date;
setupView(data, false);
validateCourseInfoDisplay(); validateCourseInfoDisplay();
}); });
}); });
......
...@@ -128,6 +128,7 @@ ...@@ -128,6 +128,7 @@
} }
} }
.upgrade-message,
.certificate-status { .certificate-status {
border-top: 1px solid palette(grayscale, x-trans); border-top: 1px solid palette(grayscale, x-trans);
padding-top: $baseline; padding-top: $baseline;
......
<div class="message col-12 md-col-8"> <div class="message col-12 md-col-8">
<span class="certificate-icon green-certificate-icon" aria-hidden="true"></span> <% // safe-lint: disable=underscore-not-escaped %>
<span class="certificate-icon green-certificate-icon" aria-hidden="true"><%= certificateSvg %></span>
<span class="card-msg"><%- gettext('Congratulations! You have earned a certificate for this course.') %></span> <span class="card-msg"><%- gettext('Congratulations! You have earned a certificate for this course.') %></span>
</div> </div>
<div class="action col-12 md-col-4"> <div class="action col-12 md-col-4">
<a href="<%- certificate_url %>" class="btn-brand cta-secondary"> <a href="<%- certificate_url %>" class="btn-brand cta-secondary">
<span class="certificate-icon blue-certificate-icon" aria-hidden="true"></span> <% // safe-lint: disable=underscore-not-escaped %>
<span class="certificate-icon blue-certificate-icon" aria-hidden="true"><%= certificateSvg %></span>
<%- gettext('View/Share Certificate') %> <%- gettext('View/Share Certificate') %>
</a> </a>
</div> </div>
...@@ -39,4 +39,5 @@ ...@@ -39,4 +39,5 @@
</div> </div>
</div> </div>
<div class="section action-msg-view"></div> <div class="section action-msg-view"></div>
<div class="section upgrade-message"></div>
<div class="section certificate-status"></div> <div class="section certificate-status"></div>
<div class="message col-12 md-col-8">
<% // safe-lint: disable=underscore-not-escaped %>
<span class="certificate-icon green-certificate-icon" aria-hidden="true"><%= certificateSvg %></span>
<span class="card-msg"><%- gettext('You need a certificate in this course to be eligible for a program certificate.') %></span>
</div>
<div class="action col-12 md-col-4">
<a href="<%- upgrade_url %>" class="btn-brand cta-primary">
<% // safe-lint: disable=underscore-not-escaped %>
<span class="certificate-icon green-certificate-icon" aria-hidden="true"><%= certificateSvg %></span>
<%- gettext('Upgrade Now') %>
</a>
</div>
...@@ -10,6 +10,7 @@ from django.conf import settings ...@@ -10,6 +10,7 @@ from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase from django.test import TestCase
from django.test.utils import override_settings
from django.utils import timezone from django.utils import timezone
import httpretty import httpretty
import mock import mock
...@@ -18,6 +19,7 @@ from edx_oauth2_provider.tests.factories import ClientFactory ...@@ -18,6 +19,7 @@ from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL from provider.constants import CONFIDENTIAL
from lms.djangoapps.certificates.api import MODES from lms.djangoapps.certificates.api import MODES
from lms.djangoapps.commerce.tests.test_utils import update_commerce_config
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credentials.tests import factories as credentials_factories from openedx.core.djangoapps.credentials.tests import factories as credentials_factories
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
...@@ -34,6 +36,7 @@ from xmodule.modulestore.tests.factories import CourseFactory ...@@ -34,6 +36,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
UTILS_MODULE = 'openedx.core.djangoapps.programs.utils' UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
CERTIFICATES_API_MODULE = 'lms.djangoapps.certificates.api' CERTIFICATES_API_MODULE = 'lms.djangoapps.certificates.api'
ECOMMERCE_URL_ROOT = 'http://example-ecommerce.com'
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
...@@ -672,11 +675,14 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase): ...@@ -672,11 +675,14 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
@ddt.ddt @ddt.ddt
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT=ECOMMERCE_URL_ROOT)
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') @skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
"""Tests of the utility function used to supplement program data.""" """Tests of the utility function used to supplement program data."""
password = 'test'
maxDiff = None maxDiff = None
sku = 'abc123'
password = 'test'
checkout_path = '/basket'
def setUp(self): def setUp(self):
super(TestSupplementProgramData, self).setUp() super(TestSupplementProgramData, self).setUp()
...@@ -704,15 +710,18 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -704,15 +710,18 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
course_overview = CourseOverview.get_from_id(self.course.id) # pylint: disable=no-member course_overview = CourseOverview.get_from_id(self.course.id) # pylint: disable=no-member
run_mode = dict( run_mode = dict(
factories.RunMode( factories.RunMode(
certificate_url=None,
course_image_url=course_overview.course_image_url,
course_key=unicode(self.course.id), # pylint: disable=no-member course_key=unicode(self.course.id), # pylint: disable=no-member
course_url=reverse('course_root', args=[self.course.id]), # pylint: disable=no-member course_url=reverse('course_root', args=[self.course.id]), # pylint: disable=no-member
course_image_url=course_overview.course_image_url,
start_date=strftime_localized(self.course.start, 'SHORT_DATE'),
end_date=strftime_localized(self.course.end, 'SHORT_DATE'), end_date=strftime_localized(self.course.end, 'SHORT_DATE'),
enrollment_open_date=None,
is_course_ended=self.course.end < timezone.now(), is_course_ended=self.course.end < timezone.now(),
is_enrolled=False, is_enrolled=False,
is_enrollment_open=True, is_enrollment_open=True,
marketing_url='', marketing_url=None,
start_date=strftime_localized(self.course.start, 'SHORT_DATE'),
upgrade_url=None,
), ),
**kwargs **kwargs
) )
...@@ -722,19 +731,72 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -722,19 +731,72 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
self.assertEqual(actual, expected) self.assertEqual(actual, expected)
@ddt.data(True, False) @ddt.data(
def test_student_enrollment_status(self, is_enrolled): (False, None, False),
"""Verify that program data is supplemented correctly.""" (True, MODES.audit, True),
(True, MODES.verified, False),
)
@ddt.unpack
@mock.patch(UTILS_MODULE + '.CourseMode.mode_for_course')
def test_student_enrollment_status(self, is_enrolled, enrolled_mode, is_upgrade_required, mock_get_mode):
"""Verify that program data is supplemented with the student's enrollment status."""
expected_upgrade_url = '{root}/{path}?sku={sku}'.format(
root=ECOMMERCE_URL_ROOT,
path=self.checkout_path.strip('/'),
sku=self.sku,
)
update_commerce_config(enabled=True, checkout_page=self.checkout_path)
mock_mode = mock.Mock()
mock_mode.sku = self.sku
mock_get_mode.return_value = mock_mode
if is_enrolled: if is_enrolled:
CourseEnrollmentFactory(user=self.user, course_id=self.course.id) # pylint: disable=no-member CourseEnrollmentFactory(user=self.user, course_id=self.course.id, mode=enrolled_mode) # pylint: disable=no-member
data = utils.supplement_program_data(self.program, self.user) data = utils.supplement_program_data(self.program, self.user)
self._assert_supplemented(data, is_enrolled=is_enrolled) self._assert_supplemented(
data,
is_enrolled=is_enrolled,
upgrade_url=expected_upgrade_url if is_upgrade_required else None
)
@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."""
update_commerce_config(enabled=True, checkout_page=self.checkout_path)
CourseEnrollmentFactory(
user=self.user,
course_id=self.course.id, # pylint: disable=no-member
mode=enrolled_mode,
is_active=False,
)
data = utils.supplement_program_data(self.program, self.user)
self._assert_supplemented(data)
@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."""
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
data = utils.supplement_program_data(self.program, self.user)
self._assert_supplemented(data, is_enrolled=True, upgrade_url=None)
@ddt.data( @ddt.data(
[1, 1, False], (1, 1, False),
[1, -1, True], (1, -1, True),
) )
@ddt.unpack @ddt.unpack
def test_course_enrollment_status(self, start_offset, end_offset, is_enrollment_open): def test_course_enrollment_status(self, start_offset, end_offset, is_enrollment_open):
...@@ -746,13 +808,15 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -746,13 +808,15 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
data = utils.supplement_program_data(self.program, self.user) data = utils.supplement_program_data(self.program, self.user)
if is_enrollment_open: if is_enrollment_open:
self._assert_supplemented(data, is_enrollment_open=is_enrollment_open) enrollment_open_date = None
else: else:
self._assert_supplemented( enrollment_open_date = strftime_localized(self.course.enrollment_start, 'SHORT_DATE')
data,
is_enrollment_open=is_enrollment_open, self._assert_supplemented(
enrollment_open_date=strftime_localized(self.course.enrollment_start, 'SHORT_DATE') data,
) is_enrollment_open=is_enrollment_open,
enrollment_open_date=enrollment_open_date,
)
@ddt.data(True, False) @ddt.data(True, False)
@mock.patch(UTILS_MODULE + '.certificate_api.certificate_downloadable_status') @mock.patch(UTILS_MODULE + '.certificate_api.certificate_downloadable_status')
...@@ -765,11 +829,21 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -765,11 +829,21 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
data = utils.supplement_program_data(self.program, self.user) data = utils.supplement_program_data(self.program, self.user)
if is_uuid_available: expected_url = reverse(
expected_url = reverse('certificates:render_cert_by_uuid', kwargs={'certificate_uuid': test_uuid}) 'certificates:render_cert_by_uuid',
self._assert_supplemented(data, certificate_url=expected_url) kwargs={'certificate_uuid': test_uuid}
else: ) if is_uuid_available else None
self._assert_supplemented(data)
self._assert_supplemented(data, certificate_url=expected_url)
@ddt.data(-1, 0, 1)
def test_course_course_ended(self, days_offset):
self.course.end = timezone.now() + datetime.timedelta(days=days_offset)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.supplement_program_data(self.program, self.user)
self._assert_supplemented(data)
@mock.patch(UTILS_MODULE + '.get_organization_by_short_name') @mock.patch(UTILS_MODULE + '.get_organization_by_short_name')
def test_organization_logo_exists(self, mock_get_organization_by_short_name): def test_organization_logo_exists(self, mock_get_organization_by_short_name):
...@@ -780,6 +854,7 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -780,6 +854,7 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
mock_get_organization_by_short_name.return_value = { mock_get_organization_by_short_name.return_value = {
'logo': mock_image 'logo': mock_image
} }
data = utils.supplement_program_data(self.program, self.user) data = utils.supplement_program_data(self.program, self.user)
self.assertEqual(data['organizations'][0].get('img'), mock_logo_url) self.assertEqual(data['organizations'][0].get('img'), mock_logo_url)
...@@ -799,11 +874,3 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase): ...@@ -799,11 +874,3 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
mock_get_organization_by_short_name.return_value = {'logo': None} mock_get_organization_by_short_name.return_value = {'logo': None}
data = utils.supplement_program_data(self.program, self.user) data = utils.supplement_program_data(self.program, self.user)
self.assertEqual(data['organizations'][0].get('img'), None) self.assertEqual(data['organizations'][0].get('img'), None)
@ddt.data(-1, 0, 1)
def test_course_course_ended(self, days_offset):
self.course.end = timezone.now() + datetime.timedelta(days=days_offset)
self.course = self.update_course(self.course, self.user.id) # pylint: disable=no-member
data = utils.supplement_program_data(self.program, self.user)
self._assert_supplemented(data)
...@@ -10,7 +10,9 @@ from django.utils.text import slugify ...@@ -10,7 +10,9 @@ from django.utils.text import slugify
from opaque_keys.edx.keys import CourseKey from opaque_keys.edx.keys import CourseKey
import pytz import pytz
from course_modes.models import CourseMode
from lms.djangoapps.certificates import api as certificate_api from lms.djangoapps.certificates import api as certificate_api
from lms.djangoapps.commerce.utils import EcommerceService
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.programs.models import ProgramsApiConfig from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.lib.edx_api_utils import get_edx_api_data from openedx.core.lib.edx_api_utils import get_edx_api_data
...@@ -322,6 +324,7 @@ class ProgramProgressMeter(object): ...@@ -322,6 +324,7 @@ class ProgramProgressMeter(object):
return parsed return parsed
# TODO: This function will benefit from being refactored as a class.
def supplement_program_data(program_data, user): def supplement_program_data(program_data, user):
"""Supplement program course codes with CourseOverview and CourseEnrollment data. """Supplement program course codes with CourseOverview and CourseEnrollment data.
...@@ -330,8 +333,8 @@ def supplement_program_data(program_data, user): ...@@ -330,8 +333,8 @@ def supplement_program_data(program_data, user):
user (User): The user whose enrollments to inspect. user (User): The user whose enrollments to inspect.
""" """
for organization in program_data['organizations']: for organization in program_data['organizations']:
# TODO cache the results of the get_organization_by_short_name call # TODO: Cache the results of the get_organization_by_short_name call so
# so we don't have to hit database that frequently # the database is hit less frequently.
org_obj = get_organization_by_short_name(organization['key']) org_obj = get_organization_by_short_name(organization['key'])
if org_obj and org_obj.get('logo'): if org_obj and org_obj.get('logo'):
organization['img'] = org_obj['logo'].url organization['img'] = org_obj['logo'].url
...@@ -341,34 +344,58 @@ def supplement_program_data(program_data, user): ...@@ -341,34 +344,58 @@ def supplement_program_data(program_data, user):
course_key = CourseKey.from_string(run_mode['course_key']) course_key = CourseKey.from_string(run_mode['course_key'])
course_overview = CourseOverview.get_from_id(course_key) course_overview = CourseOverview.get_from_id(course_key)
run_mode['course_url'] = reverse('course_root', args=[course_key]) course_url = reverse('course_root', args=[course_key])
run_mode['course_image_url'] = course_overview.course_image_url course_image_url = course_overview.course_image_url
run_mode['start_date'] = course_overview.start_datetime_text() start_date_string = course_overview.start_datetime_text()
run_mode['end_date'] = course_overview.end_datetime_text() end_date_string = course_overview.end_datetime_text()
end_date = course_overview.end or datetime.datetime.max.replace(tzinfo=pytz.UTC) end_date = course_overview.end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
run_mode['is_course_ended'] = end_date < timezone.now() is_course_ended = end_date < timezone.now()
run_mode['is_enrolled'] = CourseEnrollment.is_enrolled(user, course_key) is_enrolled = CourseEnrollment.is_enrolled(user, course_key)
enrollment_start = course_overview.enrollment_start or datetime.datetime.min.replace(tzinfo=pytz.UTC) enrollment_start = course_overview.enrollment_start or datetime.datetime.min.replace(tzinfo=pytz.UTC)
enrollment_end = course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=pytz.UTC) enrollment_end = course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
is_enrollment_open = enrollment_start <= timezone.now() < enrollment_end is_enrollment_open = enrollment_start <= timezone.now() < enrollment_end
run_mode['is_enrollment_open'] = is_enrollment_open
if not is_enrollment_open:
# Only render this enrollment open date if the enrollment open is in the future
run_mode['enrollment_open_date'] = strftime_localized(enrollment_start, 'SHORT_DATE')
# TODO: Currently unavailable on LMS. enrollment_open_date = None if is_enrollment_open else strftime_localized(enrollment_start, 'SHORT_DATE')
run_mode['marketing_url'] = ''
certificate_data = certificate_api.certificate_downloadable_status(user, course_key) certificate_data = certificate_api.certificate_downloadable_status(user, course_key)
certificate_uuid = certificate_data.get('uuid') certificate_uuid = certificate_data.get('uuid')
if certificate_uuid: certificate_url = certificate_api.get_certificate_url(
run_mode['certificate_url'] = certificate_api.get_certificate_url( course_id=course_key,
course_id=course_key, uuid=certificate_uuid,
uuid=certificate_uuid, ) if certificate_uuid else None
)
required_mode_slug = run_mode['mode_slug']
enrolled_mode_slug, _ = CourseEnrollment.enrollment_mode_for_user(user, course_key)
is_mode_mismatch = required_mode_slug != enrolled_mode_slug
is_upgrade_required = is_enrolled and is_mode_mismatch
# Requires that the ecommerce service be in use.
required_mode = CourseMode.mode_for_course(course_key, required_mode_slug)
ecommerce = EcommerceService()
sku = getattr(required_mode, 'sku', None)
if ecommerce.is_enabled(user) and sku:
upgrade_url = ecommerce.checkout_page_url(required_mode.sku) if is_upgrade_required else None
else:
upgrade_url = None
run_mode.update({
'certificate_url': certificate_url,
'course_image_url': course_image_url,
'course_url': course_url,
'end_date': end_date_string,
'enrollment_open_date': enrollment_open_date,
'is_course_ended': is_course_ended,
'is_enrolled': is_enrolled,
'is_enrollment_open': is_enrollment_open,
# TODO: Not currently available on LMS.
'marketing_url': None,
'start_date': start_date_string,
'upgrade_url': upgrade_url,
})
return program_data return program_data
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