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.
This commit is contained in:
Renzo Lucioni
2016-06-27 11:59:17 -04:00
parent 69410948c9
commit d738188550
12 changed files with 289 additions and 92 deletions

View File

@@ -10,6 +10,7 @@ from django.conf import settings
from django.core.cache import cache
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.utils import override_settings
from django.utils import timezone
import httpretty
import mock
@@ -18,6 +19,7 @@ from edx_oauth2_provider.tests.factories import ClientFactory
from provider.constants import CONFIDENTIAL
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.credentials.tests import factories as credentials_factories
from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfigMixin, CredentialsDataMixin
@@ -34,6 +36,7 @@ from xmodule.modulestore.tests.factories import CourseFactory
UTILS_MODULE = 'openedx.core.djangoapps.programs.utils'
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')
@@ -672,11 +675,14 @@ class TestProgramProgressMeter(ProgramsApiConfigMixin, TestCase):
@ddt.ddt
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT=ECOMMERCE_URL_ROOT)
@skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
"""Tests of the utility function used to supplement program data."""
password = 'test'
maxDiff = None
sku = 'abc123'
password = 'test'
checkout_path = '/basket'
def setUp(self):
super(TestSupplementProgramData, self).setUp()
@@ -704,15 +710,18 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
course_overview = CourseOverview.get_from_id(self.course.id) # pylint: disable=no-member
run_mode = dict(
factories.RunMode(
certificate_url=None,
course_image_url=course_overview.course_image_url,
course_key=unicode(self.course.id), # pylint: disable=no-member
course_url=reverse('course_root', args=[self.course.id]), # pylint: disable=no-member
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'),
enrollment_open_date=None,
is_course_ended=self.course.end < timezone.now(),
is_enrolled=False,
is_enrollment_open=True,
marketing_url='',
marketing_url=None,
start_date=strftime_localized(self.course.start, 'SHORT_DATE'),
upgrade_url=None,
),
**kwargs
)
@@ -722,19 +731,72 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
self.assertEqual(actual, expected)
@ddt.data(True, False)
def test_student_enrollment_status(self, is_enrolled):
"""Verify that program data is supplemented correctly."""
@ddt.data(
(False, None, False),
(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:
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)
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(
[1, 1, False],
[1, -1, True],
(1, 1, False),
(1, -1, True),
)
@ddt.unpack
def test_course_enrollment_status(self, start_offset, end_offset, is_enrollment_open):
@@ -746,13 +808,15 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
data = utils.supplement_program_data(self.program, self.user)
if is_enrollment_open:
self._assert_supplemented(data, is_enrollment_open=is_enrollment_open)
enrollment_open_date = None
else:
self._assert_supplemented(
data,
is_enrollment_open=is_enrollment_open,
enrollment_open_date=strftime_localized(self.course.enrollment_start, 'SHORT_DATE')
)
enrollment_open_date = strftime_localized(self.course.enrollment_start, 'SHORT_DATE')
self._assert_supplemented(
data,
is_enrollment_open=is_enrollment_open,
enrollment_open_date=enrollment_open_date,
)
@ddt.data(True, False)
@mock.patch(UTILS_MODULE + '.certificate_api.certificate_downloadable_status')
@@ -765,11 +829,21 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
data = utils.supplement_program_data(self.program, self.user)
if is_uuid_available:
expected_url = reverse('certificates:render_cert_by_uuid', kwargs={'certificate_uuid': test_uuid})
self._assert_supplemented(data, certificate_url=expected_url)
else:
self._assert_supplemented(data)
expected_url = reverse(
'certificates:render_cert_by_uuid',
kwargs={'certificate_uuid': test_uuid}
) if is_uuid_available else None
self._assert_supplemented(data, certificate_url=expected_url)
@ddt.data(-1, 0, 1)
def test_course_course_ended(self, days_offset):
self.course.end = 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')
def test_organization_logo_exists(self, mock_get_organization_by_short_name):
@@ -780,6 +854,7 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
mock_get_organization_by_short_name.return_value = {
'logo': mock_image
}
data = utils.supplement_program_data(self.program, self.user)
self.assertEqual(data['organizations'][0].get('img'), mock_logo_url)
@@ -799,11 +874,3 @@ class TestSupplementProgramData(ProgramsApiConfigMixin, ModuleStoreTestCase):
mock_get_organization_by_short_name.return_value = {'logo': None}
data = utils.supplement_program_data(self.program, self.user)
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)

View File

@@ -10,7 +10,9 @@ from django.utils.text import slugify
from opaque_keys.edx.keys import CourseKey
import pytz
from course_modes.models import CourseMode
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.programs.models import ProgramsApiConfig
from openedx.core.lib.edx_api_utils import get_edx_api_data
@@ -322,6 +324,7 @@ class ProgramProgressMeter(object):
return parsed
# TODO: This function will benefit from being refactored as a class.
def supplement_program_data(program_data, user):
"""Supplement program course codes with CourseOverview and CourseEnrollment data.
@@ -330,8 +333,8 @@ def supplement_program_data(program_data, user):
user (User): The user whose enrollments to inspect.
"""
for organization in program_data['organizations']:
# TODO cache the results of the get_organization_by_short_name call
# so we don't have to hit database that frequently
# 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
@@ -341,34 +344,58 @@ def supplement_program_data(program_data, user):
course_key = CourseKey.from_string(run_mode['course_key'])
course_overview = CourseOverview.get_from_id(course_key)
run_mode['course_url'] = reverse('course_root', args=[course_key])
run_mode['course_image_url'] = course_overview.course_image_url
course_url = reverse('course_root', args=[course_key])
course_image_url = course_overview.course_image_url
run_mode['start_date'] = course_overview.start_datetime_text()
run_mode['end_date'] = course_overview.end_datetime_text()
start_date_string = course_overview.start_datetime_text()
end_date_string = course_overview.end_datetime_text()
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_end = course_overview.enrollment_end or datetime.datetime.max.replace(tzinfo=pytz.UTC)
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.
run_mode['marketing_url'] = ''
enrollment_open_date = None if is_enrollment_open else strftime_localized(enrollment_start, 'SHORT_DATE')
certificate_data = certificate_api.certificate_downloadable_status(user, course_key)
certificate_uuid = certificate_data.get('uuid')
if certificate_uuid:
run_mode['certificate_url'] = certificate_api.get_certificate_url(
course_id=course_key,
uuid=certificate_uuid,
)
certificate_url = certificate_api.get_certificate_url(
course_id=course_key,
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