Create the listing page of Programs (x-series) which learners have enrolled in

This commit is contained in:
Simon Chen
2016-03-11 11:18:19 -05:00
parent 374ab7d1ec
commit 21be72048a
32 changed files with 1023 additions and 26 deletions

View File

@@ -94,7 +94,7 @@ class TestCredentialsRetrieval(ProgramsApiConfigMixin, CredentialsApiConfigMixin
self.mock_credentials_api(self.user, reset_url=False)
actual = get_user_program_credentials(self.user)
expected = self.PROGRAMS_API_RESPONSE['results']
expected = self.PROGRAMS_API_RESPONSE['results'][:2]
expected[0]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[0]['certificate_url']
expected[1]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[1]['certificate_url']

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('programs', '0005_programsapiconfig_max_retries'),
]
operations = [
migrations.AddField(
model_name='programsapiconfig',
name='xseries_ad_enabled',
field=models.BooleanField(default=False, verbose_name='Do we want to show xseries program advertising'),
),
]

View File

@@ -74,6 +74,11 @@ class ProgramsApiConfig(ConfigurationModel):
)
)
xseries_ad_enabled = models.BooleanField(
verbose_name=_("Do we want to show xseries program advertising"),
default=False
)
@property
def internal_api_url(self):
"""
@@ -132,3 +137,10 @@ class ProgramsApiConfig(ConfigurationModel):
certificates for Program completion.
"""
return self.enabled and self.enable_certification
@property
def show_xseries_ad(self):
"""
Indicates whether we should show xseries add
"""
return self.enabled and self.xseries_ad_enabled

View File

@@ -20,6 +20,7 @@ class ProgramsApiConfigMixin(object):
'enable_student_dashboard': True,
'enable_studio_tab': True,
'enable_certification': True,
'xseries_ad_enabled': True,
}
def create_programs_config(self, **kwargs):
@@ -35,6 +36,7 @@ class ProgramsDataMixin(object):
PROGRAM_NAMES = [
'Test Program A',
'Test Program B',
'Test Program C',
]
COURSE_KEYS = [
@@ -48,6 +50,7 @@ class ProgramsDataMixin(object):
'organization-b/course-d/winter',
]
# TODO: Use factory-boy.
PROGRAMS_API_RESPONSE = {
'results': [
{
@@ -56,7 +59,7 @@ class ProgramsDataMixin(object):
'subtitle': 'A program used for testing purposes',
'category': 'xseries',
'status': 'unpublished',
'marketing_slug': '',
'marketing_slug': '{}_test_url'.format(PROGRAM_NAMES[0].replace(' ', '_')),
'organizations': [
{
'display_name': 'Test Organization A',
@@ -122,7 +125,7 @@ class ProgramsDataMixin(object):
'subtitle': 'Another program used for testing purposes',
'category': 'xseries',
'status': 'unpublished',
'marketing_slug': '',
'marketing_slug': '{}_test_url'.format(PROGRAM_NAMES[1].replace(' ', '_')),
'organizations': [
{
'display_name': 'Test Organization B',
@@ -181,6 +184,41 @@ class ProgramsDataMixin(object):
],
'created': '2015-10-26T19:59:03.064000Z',
'modified': '2015-10-26T19:59:18.536000Z'
},
{
'id': 3,
'name': PROGRAM_NAMES[2],
'subtitle': 'A third program used for testing purposes',
'category': 'xseries',
'status': 'unpublished',
'marketing_slug': '{}_test_url'.format(PROGRAM_NAMES[2].replace(' ', '_')),
'organizations': [
{
'display_name': 'Test Organization B',
'key': 'organization-b'
}
],
'course_codes': [
{
'display_name': 'Test Course D',
'key': 'course-d',
'organization': {
'display_name': 'Test Organization B',
'key': 'organization-b'
},
'run_modes': [
{
'course_key': COURSE_KEYS[7],
'mode_slug': 'verified',
'sku': '',
'start_date': '2015-11-05T07:39:02.791741Z',
'run_key': 'winter'
}
]
}
],
'created': '2015-10-26T19:59:03.064000Z',
'modified': '2015-10-26T19:59:18.536000Z'
}
]
}

View File

@@ -14,9 +14,12 @@ from openedx.core.djangoapps.credentials.tests.mixins import CredentialsApiConfi
from openedx.core.djangoapps.programs.models import ProgramsApiConfig
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin, ProgramsDataMixin
from openedx.core.djangoapps.programs.utils import (
get_programs, get_programs_for_credentials, get_programs_for_dashboard
get_programs,
get_programs_for_dashboard,
get_programs_for_credentials,
get_engaged_programs,
)
from student.tests.factories import UserFactory
from student.tests.factories import UserFactory, CourseEnrollmentFactory
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@@ -146,7 +149,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
self.mock_programs_api()
actual = get_programs_for_credentials(self.user, self.PROGRAMS_CREDENTIALS_DATA)
expected = self.PROGRAMS_API_RESPONSE['results']
expected = self.PROGRAMS_API_RESPONSE['results'][:2]
expected[0]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[0]['certificate_url']
expected[1]['credential_url'] = self.PROGRAMS_CREDENTIALS_DATA[1]['certificate_url']
@@ -185,3 +188,92 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
]
actual = get_programs_for_credentials(self.user, credential_data)
self.assertEqual(actual, [])
def _create_enrollments(self, *course_ids):
"""Variadic helper method used to create course enrollments."""
return [CourseEnrollmentFactory(user=self.user, course_id=c) for c in course_ids]
@httpretty.activate
def test_get_engaged_programs(self):
"""
Verify that correct programs are returned in the correct order when the user
has multiple enrollments.
"""
self.create_programs_config()
self.mock_programs_api()
enrollments = self._create_enrollments(*self.COURSE_KEYS)
actual = get_engaged_programs(self.user, enrollments)
programs = self.PROGRAMS_API_RESPONSE['results']
# get_engaged_programs iterates across a list returned by the programs
# API to create flattened lists keyed by course ID. These lists are
# joined in order of enrollment creation time when constructing the
# list of engaged programs. As such, two programs sharing an enrollment
# should be returned in the same order found in the API response. In this
# case, the most recently created enrollment is for a run mode present in
# the last two test programs.
expected = [
programs[1],
programs[2],
programs[0],
]
self.assertEqual(expected, actual)
@httpretty.activate
def test_get_engaged_programs_single_program(self):
"""
Verify that correct program is returned when the user has a single enrollment
appearing in one program.
"""
self.create_programs_config()
self.mock_programs_api()
enrollments = self._create_enrollments(self.COURSE_KEYS[0])
actual = get_engaged_programs(self.user, enrollments)
programs = self.PROGRAMS_API_RESPONSE['results']
expected = [programs[0]]
self.assertEqual(expected, actual)
@httpretty.activate
def test_get_engaged_programs_shared_enrollment(self):
"""
Verify that correct programs are returned when the user has a single enrollment
appearing in multiple programs.
"""
self.create_programs_config()
self.mock_programs_api()
enrollments = self._create_enrollments(self.COURSE_KEYS[-1])
actual = get_engaged_programs(self.user, enrollments)
programs = self.PROGRAMS_API_RESPONSE['results']
expected = programs[-2:]
self.assertEqual(expected, actual)
@httpretty.activate
def test_get_engaged_no_enrollments(self):
"""Verify that no programs are returned when the user has no enrollments."""
self.create_programs_config()
self.mock_programs_api()
actual = get_engaged_programs(self.user, [])
expected = []
self.assertEqual(expected, actual)
@httpretty.activate
def test_get_engaged_no_programs(self):
"""Verify that no programs are returned when no programs exist."""
self.create_programs_config()
self.mock_programs_api(data=[])
enrollments = self._create_enrollments(*self.COURSE_KEYS)
actual = get_engaged_programs(self.user, enrollments)
expected = []
self.assertEqual(expected, actual)

View File

@@ -25,10 +25,34 @@ def get_programs(user):
# Bypass caching for staff users, who may be creating Programs and want
# to see them displayed immediately.
cache_key = programs_config.CACHE_KEY if programs_config.is_cache_enabled and not user.is_staff else None
return get_edx_api_data(programs_config, user, 'programs', cache_key=cache_key)
def flatten_programs(programs, course_ids):
"""Flatten the result returned by the Programs API.
Arguments:
programs (list): Serialized programs
course_ids (list): Course IDs to key on.
Returns:
dict, programs keyed by course ID
"""
flattened = {}
for program in programs:
try:
for course_code in program['course_codes']:
for run in course_code['run_modes']:
run_id = run['course_key']
if run_id in course_ids:
flattened.setdefault(run_id, []).append(program)
except KeyError:
log.exception('Unable to parse Programs API response: %r', program)
return flattened
def get_programs_for_dashboard(user, course_keys):
"""Build a dictionary of programs, keyed by course.
@@ -55,23 +79,8 @@ def get_programs_for_dashboard(user, course_keys):
log.debug('No programs found for the user with ID %d.', user.id)
return course_programs
# Convert course keys to Unicode representation for efficient lookup.
course_keys = map(unicode, course_keys)
# Reindex the result returned by the Programs API from:
# program -> course code -> course run
# to:
# course run -> program_array
# Ignore course runs not present in the user's active enrollments.
for program in programs:
try:
for course_code in program['course_codes']:
for run in course_code['run_modes']:
course_key = run['course_key']
if course_key in course_keys:
course_programs.setdefault(course_key, []).append(program)
except KeyError:
log.exception('Unable to parse Programs API response: %r', program)
course_ids = [unicode(c) for c in course_keys]
course_programs = flatten_programs(programs, course_ids)
return course_programs
@@ -102,3 +111,30 @@ def get_programs_for_credentials(user, programs_credentials):
certificate_programs.append(program)
return certificate_programs
def get_engaged_programs(user, enrollments):
"""Derive a list of programs in which the given user is engaged.
Arguments:
user (User): The user for which to find programs.
enrollments (list): The user's enrollments.
Returns:
list of serialized programs, ordered by most recent enrollment
"""
programs = get_programs(user)
enrollments = sorted(enrollments, key=lambda e: e.created, reverse=True)
# enrollment.course_id is really a course key.
course_ids = [unicode(e.course_id) for e in enrollments]
flattened = flatten_programs(programs, course_ids)
engaged_programs = []
for course_id in course_ids:
for program in flattened.get(course_id, []):
if program not in engaged_programs:
engaged_programs.append(program)
return engaged_programs