Commit c47311b1 by Ahsan Ulhaq Committed by Awais

Add the html and css and backend changes required for the xseries.

parent 78680678
......@@ -2,19 +2,21 @@
"""
Miscellaneous tests for the student app.
"""
from datetime import datetime, timedelta
import ddt
import logging
import pytz
import unittest
import ddt
from datetime import datetime, timedelta
from urlparse import urljoin
import pytz
from mock import Mock, patch
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from django.conf import settings
from django.contrib.auth.models import User, AnonymousUser
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import Client
from mock import Mock, patch
from opaque_keys.edx.locations import SlashSeparatedCourseKey
from student.models import (
anonymous_id_for_user, user_by_anonymous_id, CourseEnrollment,
......@@ -24,6 +26,7 @@ from student.views import (
process_survey_link,
_cert_info,
complete_course_mode_info,
_get_course_programs
)
from student.tests.factories import UserFactory, CourseModeFactory
from util.testing import EventTestMixin
......@@ -38,10 +41,12 @@ from certificates.models import CertificateStatuses # pylint: disable=import-er
from certificates.tests.factories import GeneratedCertificateFactory # pylint: disable=import-error
from verify_student.models import SoftwareSecurePhotoVerification
import shoppingcart # pylint: disable=import-error
from openedx.core.djangoapps.programs.tests.mixins import ProgramsApiConfigMixin
# Explicitly import the cache from ConfigurationModel so we can reset it after each test
from config_models.models import cache
log = logging.getLogger(__name__)
......@@ -873,3 +878,237 @@ class AnonymousLookupTable(ModuleStoreTestCase):
real_user = user_by_anonymous_id(anonymous_id)
self.assertEqual(self.user, real_user)
self.assertEqual(anonymous_id, anonymous_id_for_user(self.user, course2.id, save=False))
@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms')
@ddt.ddt
class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
"""
Tests for dashboard for xseries program courses. Enroll student into
programs and then try different combinations to see xseries upsell
messages are appearing.
"""
def setUp(self):
super(DashboardTestXSeriesPrograms, self).setUp()
self.user = UserFactory.create(username="jack", email="jack@fake.edx.org", password='test')
self.course_1 = CourseFactory.create()
self.course_2 = CourseFactory.create()
self.course_3 = CourseFactory.create()
self.program_name = 'Testing Program'
self.category = 'xseries'
CourseModeFactory.create(
course_id=self.course_1.id,
mode_slug='verified',
mode_display_name='Verified',
expiration_datetime=datetime.now(pytz.UTC) + timedelta(days=1)
)
self.client = Client()
cache.clear()
def _create_program_data(self, data):
"""Dry method to create testing programs data."""
programs = {}
for course, program_status in data:
programs[unicode(course)] = {
'category': self.category,
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-1',
'status': program_status,
'course_codes': [
{
'display_name': 'Demo XSeries Program 1',
'key': unicode(course),
'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': unicode(course)}]
},
{
'display_name': 'Demo XSeries Program 2',
'key': 'edx/demo/course_2',
'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': 'edx/demo/course_2'}]
},
{
'display_name': 'Demo XSeries Program 3',
'key': 'edx/demo/course_3',
'run_modes': [{'sku': '', 'mode_slug': 'ABC', 'course_key': 'edx/demo/course_3'}]
}
],
'subtitle': 'sub',
'name': self.program_name
}
return programs
@ddt.data(
('active', [{'sku': ''}, {'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-1'),
('active', [{'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-2'),
('active', [], ''),
('unpublished', [{'sku': ''}, {'sku': ''}, {'sku': ''}, {'sku': ''}], 'marketing-slug-3'),
)
@ddt.unpack
def test_get_xseries_programs_method(self, program_status, course_codes, marketing_slug):
"""Verify that program data is parsed correctly for a given course"""
with patch('student.views.get_course_programs_for_dashboard') as mock_data:
mock_data.return_value = {
u'edx/demox/Run_1': {
'category': self.category,
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': marketing_slug,
'status': program_status,
'course_codes': course_codes,
'subtitle': 'sub',
'name': self.program_name
}
}
parse_data = _get_course_programs(
self.user, [
u'edx/demox/Run_1', u'valid/edX/Course'
]
)
if program_status == 'unpublished':
self.assertEqual({}, parse_data)
else:
self.assertEqual(
{
u'edx/demox/Run_1': {
'category': 'xseries',
'course_count': len(course_codes),
'display_name': self.program_name,
'program_marketing_url': urljoin(
settings.MKTG_URLS.get('ROOT'), 'xseries' + '/{}'
).format(marketing_slug),
'display_category': 'XSeries'
}
},
parse_data
)
def test_program_courses_on_dashboard_without_configuration(self):
"""If programs configuration is disabled then the xseries upsell messages
will not appear on student dashboard.
"""
CourseEnrollment.enroll(self.user, self.course_1.id)
self.client.login(username="jack", password="test")
with patch('student.views.get_course_programs_for_dashboard') as mock_method:
mock_method.return_value = self._create_program_data(
[(self.course_1.id, 'active')]
)
response = self.client.get(reverse('dashboard'))
# Verify that without the programs configuration the method
# 'get_course_programs_for_dashboard' should not be called
self.assertFalse(mock_method.called)
self.assertEquals(response.status_code, 200)
self.assertIn('Pursue a Certificate of Achievement to highlight', response.content)
self._assert_responses(response, 0)
@ddt.data('verified', 'honor')
def test_modes_program_courses_on_dashboard_with_configuration(self, course_mode):
"""Test that if program configuration is enabled than student can only
see those courses with xseries upsell messages which are active in
xseries programs.
"""
CourseEnrollment.enroll(self.user, self.course_1.id, mode=course_mode)
CourseEnrollment.enroll(self.user, self.course_2.id, mode=course_mode)
self.client.login(username="jack", password="test")
self.create_config(enabled=True, enable_student_dashboard=True)
with patch('student.views.get_course_programs_for_dashboard') as mock_data:
mock_data.return_value = self._create_program_data(
[(self.course_1.id, 'active'), (self.course_2.id, 'unpublished')]
)
response = self.client.get(reverse('dashboard'))
# count total courses appearing on student dashboard
self.assertContains(response, 'course-container', 2)
self._assert_responses(response, 1)
# for verified enrollment view the program detail button will have
# the class 'base-btn'
# for other modes view the program detail button will have have the
# class border-btn
if course_mode == 'verified':
self.assertIn('xseries-base-btn', response.content)
else:
self.assertIn('xseries-border-btn', response.content)
@ddt.data(
('unpublished', 'unpublished', 'unpublished', 0),
('active', 'unpublished', 'unpublished', 1),
('active', 'active', 'unpublished', 2),
('active', 'active', 'active', 3),
)
@ddt.unpack
def test_different_programs_on_dashboard(self, status_1, status_2, status_3, program_count):
"""Test the upsell on student dashboard with different programs
statuses.
"""
CourseEnrollment.enroll(self.user, self.course_1.id, mode='verified')
CourseEnrollment.enroll(self.user, self.course_2.id, mode='honor')
CourseEnrollment.enroll(self.user, self.course_3.id, mode='honor')
self.client.login(username="jack", password="test")
self.create_config(enabled=True, enable_student_dashboard=True)
with patch('student.views.get_course_programs_for_dashboard') as mock_data:
mock_data.return_value = self._create_program_data(
[(self.course_1.id, status_1),
(self.course_2.id, status_2),
(self.course_3.id, status_3)]
)
response = self.client.get(reverse('dashboard'))
# count total courses appearing on student dashboard
self.assertContains(response, 'course-container', 3)
self._assert_responses(response, program_count)
@patch('student.views.log.warning')
@ddt.data('', 'course_codes', 'marketing_slug', 'name')
def test_program_courses_with_invalid_data(self, key_remove, log_warn):
"""Test programs with invalid responses."""
CourseEnrollment.enroll(self.user, self.course_1.id)
self.client.login(username="jack", password="test")
self.create_config(enabled=True, enable_student_dashboard=True)
program_data = self._create_program_data([(self.course_1.id, 'active')])
if key_remove and key_remove in program_data[unicode(self.course_1.id)]:
del program_data[unicode(self.course_1.id)][key_remove]
with patch('student.views.get_course_programs_for_dashboard') as mock_data:
mock_data.return_value = program_data
response = self.client.get(reverse('dashboard'))
# if data is invalid then warning log will be recorded.
if key_remove:
log_warn.assert_called_with(
'Program structure is invalid, skipping display: %r', program_data[
unicode(self.course_1.id)
]
)
# verify that no programs related upsell messages appear on the
# student dashboard.
self._assert_responses(response, 0)
else:
# in case of valid data all upsell messages will appear on dashboard.
self._assert_responses(response, 1)
# verify that only normal courses (non-programs courses) appear on
# the student dashboard.
self.assertContains(response, 'course-container', 1)
self.assertIn('Pursue a Certificate of Achievement to highlight', response.content)
def _assert_responses(self, response, count):
"""Dry method to compare different programs related upsell messages,
classes.
"""
self.assertContains(response, 'label-xseries-association', count)
self.assertContains(response, 'btn xseries-', count)
self.assertContains(response, 'XSeries Program Course', count)
self.assertContains(response, 'XSeries Program: Interested in more courses in this subject?', count)
self.assertContains(response, 'This course is 1 of 3 courses in the', count)
self.assertContains(response, self.program_name, count)
self.assertContains(response, 'View XSeries Details', count)
......@@ -7,6 +7,8 @@ import uuid
import json
import warnings
from collections import defaultdict
from urlparse import urljoin
from pytz import UTC
from requests import HTTPError
from ipware.ip import get_ip
......@@ -579,7 +581,7 @@ def dashboard(request):
# program-related information on the dashboard view.
course_programs = {}
if is_student_dashboard_programs_enabled():
course_programs = get_course_programs_for_dashboard(user, show_courseware_links_for)
course_programs = _get_course_programs(user, show_courseware_links_for)
# Construct a dictionary of course mode information
# used to render the course list. We re-use the course modes dict
......@@ -2271,3 +2273,39 @@ def change_email_settings(request):
track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
return JsonResponse({"success": True})
def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invalid-name
""" Returns a dictionary of programs courses data require for the student
dashboard.
Given a user and an iterable of course keys, find all
the programs relevant to the user and return them in a
dictionary keyed by the course_key.
Arguments:
user (user object): Currently logged-in User
user_enrolled_courses (list): List of course keys in which user is
enrolled
Returns:
Dictionary response containing programs or {}
"""
course_programs = get_course_programs_for_dashboard(user, user_enrolled_courses)
programs_data = {}
for course_key, program in course_programs.viewitems():
if program.get('status') == 'active' and program.get('category') == 'xseries':
try:
programs_data[course_key] = {
'course_count': len(program['course_codes']),
'display_name': program['name'],
'category': program.get('category'),
'program_marketing_url': urljoin(
settings.MKTG_URLS.get('ROOT'), 'xseries' + '/{}'
).format(program['marketing_slug']),
'display_category': 'XSeries'
}
except KeyError:
log.warning('Program structure is invalid, skipping display: %r', program)
return programs_data
......@@ -345,6 +345,33 @@
}
}
%btn-pl-black-border {
@extend %btn-pl-default-base;
border: 1px solid $m-gray-d4;
background-color: transparent;
color: $base-font-color;
&:hover,
&:focus {
border: 1px solid darken($m-gray-d4,10%);
background-color: $m-gray-d4;
}
}
%btn-pl-black-base {
@extend %btn-pl-default-base;
border: 1px solid transparent;
background-color: $m-gray-d4;
color: $very-light-text;
&:hover,
&:focus {
border: 1px solid darken($m-gray-d4,10%);
background-color: transparent;
color: $base-font-color;
}
}
%btn-pl-secondary-base {
@extend %btn-pl-default-base;
@include transition(border $tmg-f2 ease-in-out);
......
......@@ -254,6 +254,27 @@
.course-container{
border: 1px solid $border-color-l4;
border-radius: 3px;
// CASE: Xseries associated course
.label-xseries-association{
@include margin($baseline/2, $baseline/5, 0, $baseline/2);
.xseries-icon{
@include float(left);
@include margin-right($baseline*0.4);
background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat;
background-color: transparent;
width: ($baseline*1.1);
height: ($baseline*1.1);
}
.message-copy{
padding-top: ($baseline/5);
@extend %t-action3;
}
}
}
&:last-child {
margin-bottom: 0;
......@@ -776,6 +797,100 @@
}
}
.xseries-action{
.xseries-msg{
@include float(left);
width: flex-grid(9, 12);
}
.message-copy{
@extend %t-demi-strong;
margin-top: 0;
}
.message-copy-bold{
@extend %t-strong;
}
.xseries-border-btn {
@extend %btn-pl-black-border;
@include float(right);
position: relative;
left: 10px;
padding: ($baseline*0.4) ($baseline*0.6);
background-image: none ;
text-shadow: none;
box-shadow: none;
text-transform: none;
.action-xseries-icon{
@include float(left);
display: inline;
@include margin-right($baseline*0.4);
background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat;
background-color: transparent;
width: ($baseline*1.1);
height: ($baseline*1.1);
}
&:hover,
&:focus {
.action-xseries-icon{
@include float(left);
display: inline;
@include margin-right($baseline*0.4);
background: url('#{$static-path}/images/icon-sm-xseries-white.png') no-repeat;
background-color: transparent;
width: ($baseline*1.1);
height: ($baseline*1.1);
}
}
}
.xseries-base-btn {
@extend %btn-pl-black-base;
@include float(right);
position: relative;
left: 10px;
padding: ($baseline*0.4) ($baseline*0.6);
background-image: none ;
text-shadow: none;
box-shadow: none;
text-transform: none;
.action-xseries-icon{
@include float(left);
display: inline;
@include margin-right($baseline*0.4);
background: url('#{$static-path}/images/icon-sm-xseries-white.png') no-repeat;
background-color: transparent;
width: ($baseline*1.1);
height: ($baseline*1.1);
}
&:hover,
&:focus {
.action-xseries-icon {
@include float(left);
display: inline;
@include margin-right($baseline*0.4);
background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat;
background-color: transparent;
width: ($baseline*1.1);
height: ($baseline*1.1);
}
}
}
}
.actions {
.action {
......
......@@ -95,7 +95,8 @@ import json
<% is_course_blocked = (enrollment.course_id in block_courses) %>
<% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %>
<% course_requirements = courses_requirements_not_met.get(enrollment.course_id) %>
<%include file='dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user" />
<% course_program_info = course_programs.get(unicode(enrollment.course_id)) %>
<%include file = 'dashboard/_dashboard_course_listing.html' args="course_overview=enrollment.course_overview, enrollment=enrollment, show_courseware_link=show_courseware_link, cert_status=cert_status, credit_status=credit_status, show_email_settings=show_email_settings, course_mode_info=course_mode_info, show_refund_option=show_refund_option, is_paid_course=is_paid_course, is_course_blocked=is_course_blocked, verification_status=course_verification_status, course_requirements=course_requirements, dashboard_index=dashboard_index, share_settings=share_settings, user=user, course_program_info=course_program_info" />
% endfor
</ul>
......
<%page args="course_overview, enrollment, show_courseware_link, cert_status, credit_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings" />
<%page args="course_overview, enrollment, show_courseware_link, cert_status, credit_status, show_email_settings, course_mode_info, show_refund_option, is_paid_course, is_course_blocked, verification_status, course_requirements, dashboard_index, share_settings, course_program_info" />
<%!
import urllib
......@@ -44,6 +44,12 @@ from student.helpers import (
<% mode_class = '' %>
% endif
<div class="course-container">
% if course_program_info and course_program_info['category']=='xseries':
<div class="label-xseries-association">
<i class="xseries-icon"></i>
<p class="message-copy">${_("{category} Program Course").format(category=course_program_info['display_category'])}</p>
</div>
% endif
<article class="course${mode_class}">
<% course_target = reverse('info', args=[unicode(course_overview.id)]) %>
<section class="details">
......@@ -342,6 +348,10 @@ from student.helpers import (
</div>
%endif
% if course_program_info and course_program_info['category']=='xseries':
<%include file = "_dashboard_xseries_info.html" args="course_program_info=course_program_info, enrollment_mode=enrollment.mode" />
% endif
% if is_course_blocked:
<p id="block-course-msg" class="course-block">
${_("You can no longer access this course because payment has not yet been received. "
......
<%page args="course_program_info, enrollment_mode" />
<%!
from django.utils.translation import ugettext as _
%>
<%namespace name='static' file='../static_content.html'/>
<div class="message message-status is-shown credit-message">
<div class="xseries-action">
<div class="message-copy xseries-msg">
<p>
<b class="message-copy-bold">${_("{category} Program: Interested in more courses in this subject?").format(category=course_program_info['display_category'])}</b>
<p>
<p class="message-copy">
${_("This course is 1 of {course_count} courses in the {link_start}{program_display_name}{link_end} {program_category}.").format(
course_count=course_program_info['course_count'],
link_start='<a href="{}">'.format(course_program_info['program_marketing_url']),
link_end='</a>',
program_display_name=course_program_info['display_name'],
program_category=course_program_info['display_category'],
)}
</p>
</div>
<%
xseries_btn_class = "xseries-border-btn"
if enrollment_mode == "verified":
xseries_btn_class = "xseries-base-btn";
%>
<a class="btn ${xseries_btn_class}" href="${course_program_info['program_marketing_url']}" target="_blank">
<i class="action-xseries-icon"></i>
<span>
${_("View {category} Details").format(category=course_program_info['display_category'])}
</span>
</a>
</div>
</div>
......@@ -33,35 +33,37 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
'status': 'active',
'subtitle': 'Dummy program 1 for testing',
'name': 'First Program',
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'course_codes': [
{
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'display_name': 'Demo XSeries Program 1',
'key': 'TEST_A',
'marketing_slug': 'fake-marketing-slug-xseries-1',
'run_modes': [
{'sku': '', 'mode_slug': 'ABC_1', 'course_key': 'edX/DemoX_1/Run_1'},
{'sku': '', 'mode_slug': 'ABC_2', 'course_key': 'edX/DemoX_2/Run_2'},
]
}
]
],
'marketing_slug': 'fake-marketing-slug-xseries-1',
},
{
'category': 'xseries',
'status': 'active',
'subtitle': 'Dummy program 2 for testing',
'name': 'Second Program',
'organization': {'display_name': 'Test Organization 2', 'key': 'edX'},
'course_codes': [
{
'organization': {'display_name': 'Test Organization 2', 'key': 'edX'},
'display_name': 'Demo XSeries Program 2',
'key': 'TEST_B',
'marketing_slug': 'fake-marketing-slug-xseries-2',
'run_modes': [
{'sku': '', 'mode_slug': 'XYZ_1', 'course_key': 'edX/Program/Program_Run'},
]
}
]
],
'marketing_slug': 'fake-marketing-slug-xseries-2',
}
]
}
......@@ -83,10 +85,11 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
'edX/DemoX_1/Run_1': {
'category': 'xseries',
'status': 'active',
'subtitle': 'Dummy program 1 for testing',
'name': 'First Program',
'course_codes': [
{
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-1',
'display_name': 'Demo XSeries Program 1',
'key': 'TEST_A',
'run_modes': [
......@@ -95,16 +98,17 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
]
}
],
'subtitle': 'Dummy program 1 for testing',
'name': 'First Program'
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-1',
},
'edX/DemoX_2/Run_2': {
'category': 'xseries',
'status': 'active',
'subtitle': 'Dummy program 1 for testing',
'name': 'First Program',
'course_codes': [
{
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-1',
'display_name': 'Demo XSeries Program 1',
'key': 'TEST_A',
'run_modes': [
......@@ -113,8 +117,8 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
]
}
],
'subtitle': 'Dummy program 1 for testing',
'name': 'First Program'
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-1',
},
}
self.assertTrue(mock_get.called)
......@@ -131,10 +135,11 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
expected_output['edX/Program/Program_Run'] = {
'category': 'xseries',
'status': 'active',
'subtitle': 'Dummy program 2 for testing',
'name': 'Second Program',
'course_codes': [
{
'organization': {'display_name': 'Test Organization 2', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-2',
'display_name': 'Demo XSeries Program 2',
'key': 'TEST_B',
'run_modes': [
......@@ -142,8 +147,8 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
]
}
],
'subtitle': 'Dummy program 2 for testing',
'name': 'Second Program'
'organization': {'display_name': 'Test Organization 2', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-2',
}
self.assertTrue(mock_get.called)
self.assertEqual(expected_output, programs)
......@@ -206,3 +211,37 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
get_course_programs_for_dashboard(self.user, ['edX/DemoX/Run']), {}
)
self.assertTrue(mock_get.called)
@patch('openedx.core.djangoapps.programs.views.log.exception')
def test_get_course_programs_with_invalid_response(self, log_exception):
""" Test that the method 'get_course_programs_for_dashboard' logs
the exception message if rest api client returns invalid data.
"""
program = {
'category': 'xseries',
'status': 'active',
'subtitle': 'Dummy program 1 for testing',
'name': 'First Program',
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'course_codes': [
{
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'display_name': 'Demo XSeries Program 1',
'key': 'TEST_A',
'run_modes': [
{'sku': '', 'mode_slug': 'ABC_2'},
]
}
],
'marketing_slug': 'fake-marketing-slug-xseries-1',
}
invalid_programs_api_response = {"results": [program]}
# mock the request call
with patch('slumber.Resource.get') as mock_get:
mock_get.return_value = invalid_programs_api_response
programs = get_course_programs_for_dashboard(self.user, ['edX/DemoX/Run'])
log_exception.assert_called_with(
'Unable to parse Programs API response: %r',
program
)
self.assertEqual(programs, {})
......@@ -60,9 +60,12 @@ def get_course_programs_for_dashboard(user, course_keys): # pylint: disable=in
# to
# course run -> program, ignoring course runs not present in the dashboard enrollments
for program in programs:
for course_code in program['course_codes']:
for run in course_code['run_modes']:
if run['course_key'] in course_keys:
course_programs[run['course_key']] = program
try:
for course_code in program['course_codes']:
for run in course_code['run_modes']:
if run['course_key'] in course_keys:
course_programs[run['course_key']] = program
except KeyError:
log.exception('Unable to parse Programs API response: %r', program)
return course_programs
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