Collect data needed for program progress sidebar

This includes a representation of the user's progress towards completing each course in the program and a list of any relevant course and/or program certificates the user has earned.

ECOM-7386
This commit is contained in:
Renzo Lucioni
2017-03-29 14:10:09 -04:00
parent d0fe7f8422
commit f00059f28c
9 changed files with 172 additions and 27 deletions

View File

@@ -10,7 +10,7 @@ from edx_rest_api_client.client import EdxRestApiClient
from provider.oauth2.models import Client
from openedx.core.djangoapps.credentials.models import CredentialsApiConfig
from openedx.core.djangoapps.credentials.utils import get_user_credentials
from openedx.core.djangoapps.credentials.utils import get_credentials
from openedx.core.djangoapps.programs.utils import ProgramProgressMeter
from openedx.core.lib.token_utils import JwtBuilder
@@ -83,7 +83,7 @@ def get_certified_programs(student):
"""
certified_programs = []
for credential in get_user_credentials(student):
for credential in get_credentials(student):
if 'program_uuid' in credential['credential']:
certified_programs.append(credential['credential']['program_uuid'])
return certified_programs

View File

@@ -69,19 +69,19 @@ class GetAwardedCertificateProgramsTestCase(TestCase):
result.update(**kwargs)
return result
@mock.patch(TASKS_MODULE + '.get_user_credentials')
def test_get_certified_programs(self, mock_get_user_credentials):
@mock.patch(TASKS_MODULE + '.get_credentials')
def test_get_certified_programs(self, mock_get_credentials):
"""
Ensure the API is called and results handled correctly.
"""
student = UserFactory(username='test-username')
mock_get_user_credentials.return_value = [
mock_get_credentials.return_value = [
self.make_credential_result(status='awarded', credential={'program_uuid': 1}),
self.make_credential_result(status='awarded', credential={'course_id': 2}),
]
result = tasks.get_certified_programs(student)
self.assertEqual(mock_get_user_credentials.call_args[0], (student, ))
self.assertEqual(mock_get_credentials.call_args[0], (student, ))
self.assertEqual(result, [1])

View File

@@ -22,7 +22,11 @@ from openedx.core.djangoapps.catalog.tests.factories import (
)
from openedx.core.djangoapps.programs.tests.factories import ProgressFactory
from openedx.core.djangoapps.programs.utils import (
DEFAULT_ENROLLMENT_START_DATE, ProgramProgressMeter, ProgramDataExtender, ProgramMarketingDataExtender
DEFAULT_ENROLLMENT_START_DATE,
ProgramProgressMeter,
ProgramDataExtender,
ProgramMarketingDataExtender,
get_certificates,
)
from openedx.core.djangolib.testing.utils import skip_unless_lms
from student.tests.factories import UserFactory, CourseEnrollmentFactory
@@ -591,6 +595,85 @@ class TestProgramDataExtender(ModuleStoreTestCase):
self._assert_supplemented(data, certificate_url=expected_url)
@skip_unless_lms
@mock.patch(UTILS_MODULE + '.get_credentials')
class TestGetCertificates(TestCase):
"""
Tests of the function used to get certificates associated with a program.
"""
def setUp(self):
super(TestGetCertificates, self).setUp()
self.user = UserFactory()
self.program = ProgramFactory()
self.course_certificate_url = 'fake-course-certificate-url'
self.program_certificate_url = 'fake-program-certificate-url'
def test_get_certificates(self, mock_get_credentials):
"""
Verify course and program certificates are found when present. Only one
course run certificate should be returned for each course when the user
has earned certificates in multiple runs of the same course.
"""
expected = []
for course in self.program['courses']:
# Give all course runs a certificate URL, but only expect one to come
# back. This verifies the break in the function under test that ensures
# only one certificate per course comes back.
for index, course_run in enumerate(course['course_runs']):
course_run['certificate_url'] = self.course_certificate_url
if index == 0:
expected.append({
'type': 'course',
'title': course_run['title'],
'url': self.course_certificate_url,
})
expected.append({
'type': 'program',
'title': self.program['title'],
'url': self.program_certificate_url,
})
mock_get_credentials.return_value = [{
'certificate_url': self.program_certificate_url
}]
certificates = get_certificates(self.user, self.program)
self.assertEqual(certificates, expected)
def test_course_run_certificates_missing(self, mock_get_credentials):
"""
Verify an empty list is returned when course run certificates are missing,
and that no attempt is made to retrieve program certificates.
"""
certificates = get_certificates(self.user, self.program)
self.assertEqual(certificates, [])
self.assertFalse(mock_get_credentials.called)
def test_program_certificate_missing(self, mock_get_credentials):
"""
Verify that the function can handle a missing program certificate.
"""
expected = []
for course in self.program['courses']:
for index, course_run in enumerate(course['course_runs']):
course_run['certificate_url'] = self.course_certificate_url
if index == 0:
expected.append({
'type': 'course',
'title': course_run['title'],
'url': self.course_certificate_url,
})
mock_get_credentials.return_value = []
certificates = get_certificates(self.user, self.program)
self.assertEqual(certificates, expected)
@ddt.ddt
@override_settings(ECOMMERCE_PUBLIC_URL_ROOT=ECOMMERCE_URL_ROOT)
@skip_unless_lms

View File

@@ -18,6 +18,7 @@ from lms.djangoapps.commerce.utils import EcommerceService
from lms.djangoapps.courseware.access import has_access
from openedx.core.djangoapps.catalog.utils import get_programs
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
from openedx.core.djangoapps.credentials.utils import get_credentials
from student.models import CourseEnrollment
from util.date_utils import strftime_localized
from xmodule.modulestore.django import modulestore
@@ -216,7 +217,7 @@ class ProgramProgressMeter(object):
return any(reshape(course_run) in self.completed_course_runs for course_run in course['course_runs'])
@property
@cached_property
def completed_course_runs(self):
"""
Determine which course runs have been completed by the user.
@@ -345,6 +346,51 @@ class ProgramDataExtender(object):
run_mode['upgrade_url'] = None
def get_certificates(user, extended_program):
"""
Find certificates a user has earned related to a given program.
Arguments:
user (User): The user whose enrollments to inspect.
extended_program (dict): The program for which to locate certificates.
This is expected to be an "extended" program whose course runs already
have certificate URLs attached.
Returns:
list: Contains dicts representing course run and program certificates the
given user has earned which are associated with the given program.
"""
certificates = []
for course in extended_program['courses']:
for course_run in course['course_runs']:
url = course_run.get('certificate_url')
if url:
certificates.append({
'type': 'course',
'title': course_run['title'],
'url': url,
})
# We only want one certificate per course to be returned.
break
# A user can only have earned a program certificate if they've earned certificates
# in associated course runs. If they haven't earned any course run certificates,
# they can't have earned a program certificate, and we can save a network call
# to the credentials service.
if certificates:
program_credentials = get_credentials(user, program_uuid=extended_program['uuid'])
if program_credentials:
certificates.append({
'type': 'program',
'title': extended_program['title'],
'url': program_credentials[0]['certificate_url'],
})
return certificates
# pylint: disable=missing-docstring
class ProgramMarketingDataExtender(ProgramDataExtender):
"""