Create the listing page of Programs (x-series) which learners have enrolled in
This commit is contained in:
@@ -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']
|
||||
|
||||
|
||||
@@ -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'),
|
||||
),
|
||||
]
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user