Commit 0e88668e by Awais Qureshi

Merge pull request #10372 from edx/awais786/ECOM-2451-xseries-to-dashboard

Add the html and css changes required for XSeries
parents 78680678 c47311b1
......@@ -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