Add the html and css and backend changes required for the xseries.
This commit is contained in:
@@ -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
|
||||
|
||||
BIN
lms/static/images/icon-sm-xseries-black.png
Normal file
BIN
lms/static/images/icon-sm-xseries-black.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 711 B |
BIN
lms/static/images/icon-sm-xseries-white.png
Normal file
BIN
lms/static/images/icon-sm-xseries-white.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 706 B |
@@ -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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<%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. "
|
||||
|
||||
35
lms/templates/dashboard/_dashboard_xseries_info.html
Normal file
35
lms/templates/dashboard/_dashboard_xseries_info.html
Normal file
@@ -0,0 +1,35 @@
|
||||
<%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
|
||||
|
||||
Reference in New Issue
Block a user