Commit c47311b1 by Ahsan Ulhaq Committed by Awais

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

parent 78680678
...@@ -7,6 +7,8 @@ import uuid ...@@ -7,6 +7,8 @@ import uuid
import json import json
import warnings import warnings
from collections import defaultdict from collections import defaultdict
from urlparse import urljoin
from pytz import UTC from pytz import UTC
from requests import HTTPError from requests import HTTPError
from ipware.ip import get_ip from ipware.ip import get_ip
...@@ -579,7 +581,7 @@ def dashboard(request): ...@@ -579,7 +581,7 @@ def dashboard(request):
# program-related information on the dashboard view. # program-related information on the dashboard view.
course_programs = {} course_programs = {}
if is_student_dashboard_programs_enabled(): 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 # Construct a dictionary of course mode information
# used to render the course list. We re-use the course modes dict # used to render the course list. We re-use the course modes dict
...@@ -2271,3 +2273,39 @@ def change_email_settings(request): ...@@ -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') track.views.server_track(request, "change-email-settings", {"receive_emails": "no", "course": course_id}, page='dashboard')
return JsonResponse({"success": True}) 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 @@ ...@@ -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 { %btn-pl-secondary-base {
@extend %btn-pl-default-base; @extend %btn-pl-default-base;
@include transition(border $tmg-f2 ease-in-out); @include transition(border $tmg-f2 ease-in-out);
......
...@@ -254,6 +254,27 @@ ...@@ -254,6 +254,27 @@
.course-container{ .course-container{
border: 1px solid $border-color-l4; border: 1px solid $border-color-l4;
border-radius: 3px; 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 { &:last-child {
margin-bottom: 0; margin-bottom: 0;
...@@ -776,6 +797,100 @@ ...@@ -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 { .actions {
.action { .action {
......
...@@ -95,7 +95,8 @@ import json ...@@ -95,7 +95,8 @@ import json
<% is_course_blocked = (enrollment.course_id in block_courses) %> <% is_course_blocked = (enrollment.course_id in block_courses) %>
<% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %> <% course_verification_status = verification_status_by_course.get(enrollment.course_id, {}) %>
<% course_requirements = courses_requirements_not_met.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 % endfor
</ul> </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 import urllib
...@@ -44,6 +44,12 @@ from student.helpers import ( ...@@ -44,6 +44,12 @@ from student.helpers import (
<% mode_class = '' %> <% mode_class = '' %>
% endif % endif
<div class="course-container"> <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}"> <article class="course${mode_class}">
<% course_target = reverse('info', args=[unicode(course_overview.id)]) %> <% course_target = reverse('info', args=[unicode(course_overview.id)]) %>
<section class="details"> <section class="details">
...@@ -342,6 +348,10 @@ from student.helpers import ( ...@@ -342,6 +348,10 @@ from student.helpers import (
</div> </div>
%endif %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: % if is_course_blocked:
<p id="block-course-msg" class="course-block"> <p id="block-course-msg" class="course-block">
${_("You can no longer access this course because payment has not yet been received. " ${_("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): ...@@ -33,35 +33,37 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
'status': 'active', 'status': 'active',
'subtitle': 'Dummy program 1 for testing', 'subtitle': 'Dummy program 1 for testing',
'name': 'First Program', 'name': 'First Program',
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'course_codes': [ 'course_codes': [
{ {
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'}, 'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'display_name': 'Demo XSeries Program 1', 'display_name': 'Demo XSeries Program 1',
'key': 'TEST_A', 'key': 'TEST_A',
'marketing_slug': 'fake-marketing-slug-xseries-1',
'run_modes': [ 'run_modes': [
{'sku': '', 'mode_slug': 'ABC_1', 'course_key': 'edX/DemoX_1/Run_1'}, {'sku': '', 'mode_slug': 'ABC_1', 'course_key': 'edX/DemoX_1/Run_1'},
{'sku': '', 'mode_slug': 'ABC_2', 'course_key': 'edX/DemoX_2/Run_2'}, {'sku': '', 'mode_slug': 'ABC_2', 'course_key': 'edX/DemoX_2/Run_2'},
] ]
} }
] ],
'marketing_slug': 'fake-marketing-slug-xseries-1',
}, },
{ {
'category': 'xseries', 'category': 'xseries',
'status': 'active', 'status': 'active',
'subtitle': 'Dummy program 2 for testing', 'subtitle': 'Dummy program 2 for testing',
'name': 'Second Program', 'name': 'Second Program',
'organization': {'display_name': 'Test Organization 2', 'key': 'edX'},
'course_codes': [ 'course_codes': [
{ {
'organization': {'display_name': 'Test Organization 2', 'key': 'edX'}, 'organization': {'display_name': 'Test Organization 2', 'key': 'edX'},
'display_name': 'Demo XSeries Program 2', 'display_name': 'Demo XSeries Program 2',
'key': 'TEST_B', 'key': 'TEST_B',
'marketing_slug': 'fake-marketing-slug-xseries-2',
'run_modes': [ 'run_modes': [
{'sku': '', 'mode_slug': 'XYZ_1', 'course_key': 'edX/Program/Program_Run'}, {'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): ...@@ -83,10 +85,11 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
'edX/DemoX_1/Run_1': { 'edX/DemoX_1/Run_1': {
'category': 'xseries', 'category': 'xseries',
'status': 'active', 'status': 'active',
'subtitle': 'Dummy program 1 for testing',
'name': 'First Program',
'course_codes': [ 'course_codes': [
{ {
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'}, 'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-1',
'display_name': 'Demo XSeries Program 1', 'display_name': 'Demo XSeries Program 1',
'key': 'TEST_A', 'key': 'TEST_A',
'run_modes': [ 'run_modes': [
...@@ -95,16 +98,17 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase): ...@@ -95,16 +98,17 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
] ]
} }
], ],
'subtitle': 'Dummy program 1 for testing', 'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'name': 'First Program' 'marketing_slug': 'fake-marketing-slug-xseries-1',
}, },
'edX/DemoX_2/Run_2': { 'edX/DemoX_2/Run_2': {
'category': 'xseries', 'category': 'xseries',
'status': 'active', 'status': 'active',
'subtitle': 'Dummy program 1 for testing',
'name': 'First Program',
'course_codes': [ 'course_codes': [
{ {
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'}, 'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-1',
'display_name': 'Demo XSeries Program 1', 'display_name': 'Demo XSeries Program 1',
'key': 'TEST_A', 'key': 'TEST_A',
'run_modes': [ 'run_modes': [
...@@ -113,8 +117,8 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase): ...@@ -113,8 +117,8 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
] ]
} }
], ],
'subtitle': 'Dummy program 1 for testing', 'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'name': 'First Program' 'marketing_slug': 'fake-marketing-slug-xseries-1',
}, },
} }
self.assertTrue(mock_get.called) self.assertTrue(mock_get.called)
...@@ -131,10 +135,11 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase): ...@@ -131,10 +135,11 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
expected_output['edX/Program/Program_Run'] = { expected_output['edX/Program/Program_Run'] = {
'category': 'xseries', 'category': 'xseries',
'status': 'active', 'status': 'active',
'subtitle': 'Dummy program 2 for testing',
'name': 'Second Program',
'course_codes': [ 'course_codes': [
{ {
'organization': {'display_name': 'Test Organization 2', 'key': 'edX'}, 'organization': {'display_name': 'Test Organization 2', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-2',
'display_name': 'Demo XSeries Program 2', 'display_name': 'Demo XSeries Program 2',
'key': 'TEST_B', 'key': 'TEST_B',
'run_modes': [ 'run_modes': [
...@@ -142,8 +147,8 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase): ...@@ -142,8 +147,8 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
] ]
} }
], ],
'subtitle': 'Dummy program 2 for testing', 'organization': {'display_name': 'Test Organization 2', 'key': 'edX'},
'name': 'Second Program' 'marketing_slug': 'fake-marketing-slug-xseries-2',
} }
self.assertTrue(mock_get.called) self.assertTrue(mock_get.called)
self.assertEqual(expected_output, programs) self.assertEqual(expected_output, programs)
...@@ -206,3 +211,37 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase): ...@@ -206,3 +211,37 @@ class TestGetXSeriesPrograms(ProgramsApiConfigMixin, TestCase):
get_course_programs_for_dashboard(self.user, ['edX/DemoX/Run']), {} get_course_programs_for_dashboard(self.user, ['edX/DemoX/Run']), {}
) )
self.assertTrue(mock_get.called) 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 ...@@ -60,9 +60,12 @@ def get_course_programs_for_dashboard(user, course_keys): # pylint: disable=in
# to # to
# course run -> program, ignoring course runs not present in the dashboard enrollments # course run -> program, ignoring course runs not present in the dashboard enrollments
for program in programs: for program in programs:
for course_code in program['course_codes']: try:
for run in course_code['run_modes']: for course_code in program['course_codes']:
if run['course_key'] in course_keys: for run in course_code['run_modes']:
course_programs[run['course_key']] = program 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 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