Commit 498ab887 by Simon Chen

Merge pull request #12044 from edx/schen/ECOM-3198

ECOM-3198 Add banner image to the program listing program cards
parents fd3a87a2 149412d2
......@@ -919,6 +919,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
self.course_3 = CourseFactory.create()
self.program_name = 'Testing Program'
self.category = 'xseries'
self.display_category = 'XSeries'
CourseModeFactory.create(
course_id=self.course_1.id,
......@@ -938,6 +939,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
programs[unicode(course)] = [{
'id': _id,
'category': self.category,
'display_category': self.display_category,
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': 'fake-marketing-slug-xseries-1',
'status': program_status,
......@@ -980,6 +982,7 @@ class DashboardTestXSeriesPrograms(ModuleStoreTestCase, ProgramsApiConfigMixin):
u'edx/demox/Run_1': [{
'id': 0,
'category': self.category,
'display_category': self.display_category,
'organization': {'display_name': 'Test Organization 1', 'key': 'edX'},
'marketing_slug': marketing_slug,
'status': program_status,
......
......@@ -2446,7 +2446,7 @@ def _get_course_programs(user, user_enrolled_courses): # pylint: disable=invali
'xseries' + '/{}'
).format(program['marketing_slug'])
})
programs_for_course['display_category'] = 'XSeries'
programs_for_course['display_category'] = program.get('display_category')
programs_for_course['category'] = program.get('category')
except KeyError:
log.warning('Program structure is invalid, skipping display: %r', program)
......
......@@ -1249,6 +1249,8 @@ base_vendor_js = [
'js/vendor/url.min.js',
'common/js/vendor/underscore.js',
'common/js/vendor/underscore.string.js',
'js/vendor/underscore.string.min.js',
'common/js/vendor/picturefill.min.js',
# Make some edX UI Toolkit utilities available in the global "edx" namespace
'edx-ui-toolkit/js/utils/global-loader.js',
......
......@@ -12,10 +12,21 @@
if (data){
this.set({
name: data.name,
category: data.category,
type: data.display_category + ' Program',
subtitle: data.subtitle,
organizations: data.organizations,
marketingUrl: data.marketing_url
marketingUrl: data.marketing_url,
smallBannerUrl: data.banner_image_urls.w348h116,
mediumBannerUrl: data.banner_image_urls.w435h145,
largeBannerUrl: data.banner_image_urls.w726h242,
breakpoints: {
max: {
tiny: '320px',
small: '540px',
medium: '768px',
large: '979px'
}
}
});
}
}
......
......@@ -5,14 +5,16 @@
'jquery',
'underscore',
'gettext',
'text!../../../templates/learner_dashboard/program_card.underscore'
'text!../../../templates/learner_dashboard/program_card.underscore',
'picturefill'
],
function(
Backbone,
$,
_,
gettext,
programCardTpl
programCardTpl,
picturefill
) {
return Backbone.View.extend({
className: 'program-card',
......@@ -23,6 +25,35 @@
render: function() {
var templated = this.tpl(this.model.toJSON());
this.$el.html(templated);
this.postRender();
},
postRender: function() {
if(navigator.userAgent.indexOf('MSIE') !== -1 ||
navigator.appVersion.indexOf('Trident/') > 0){
/* Microsoft Internet Explorer detected in. */
window.setTimeout( function() {
this.reLoadBannerImage();
}.bind(this), 100);
}
},
// Defer loading the rest of the page to limit FOUC
reLoadBannerImage: function() {
var $img = this.$('.program_card .banner-image'),
imgSrcAttr = $img ? $img.attr('src') : {};
if (!imgSrcAttr || imgSrcAttr.length < 0) {
try{
this.reEvaluatePicture();
}catch(err){
//Swallow the error here
}
}
},
reEvaluatePicture: function(){
picturefill({
reevaluate: true
});
}
});
}
......
......@@ -29,7 +29,12 @@ define([
modified: '2016-03-25T13:45:21.220732Z',
marketing_slug: 'p_2?param=haha&test=b',
id: 146,
marketing_url: 'http://www.edx.org/xseries/p_2?param=haha&test=b'
marketing_url: 'http://www.edx.org/xseries/p_2?param=haha&test=b',
banner_image_urls: {
w348h116: 'http://www.edx.org/images/org1/test1',
w435h145: 'http://www.edx.org/images/org1/test2',
w726h242: 'http://www.edx.org/images/org1/test3'
}
},
{
category: 'xseries',
......@@ -46,7 +51,12 @@ define([
modified: '2016-03-09T14:30:52.840898Z',
marketing_slug: 'gdaf',
id: 147,
marketing_url: 'http://www.edx.org/xseries/gdaf'
marketing_url: 'http://www.edx.org/xseries/gdaf',
banner_image_urls: {
w348h116: 'http://www.edx.org/images/org2/test1',
w435h145: 'http://www.edx.org/images/org2/test2',
w726h242: 'http://www.edx.org/images/org2/test3'
}
}
]
};
......
......@@ -13,6 +13,7 @@ define([
programModel,
program = {
category: 'xseries',
display_category: 'XSeries',
status: 'active',
subtitle: 'program 1',
name: 'test program 1',
......@@ -26,7 +27,12 @@ define([
modified: '2016-03-25T13:45:21.220732Z',
marketing_slug: 'p_2?param=haha&test=b',
id: 146,
marketing_url: 'http://www.edx.org/xseries/p_2?param=haha&test=b'
marketing_url: 'http://www.edx.org/xseries/p_2?param=haha&test=b',
banner_image_urls: {
w348h116: 'http://www.edx.org/images/test1',
w435h145: 'http://www.edx.org/images/test2',
w726h242: 'http://www.edx.org/images/test3'
}
};
beforeEach(function() {
......@@ -49,10 +55,26 @@ define([
var $cards = view.$el;
expect($cards).toBeDefined();
expect($cards.find('.title').html().trim()).toEqual(program.name);
expect($cards.find('.category span').html().trim()).toEqual(program.category);
expect($cards.find('.organization span').html().trim()).toEqual(program.organizations[0].display_name);
expect($cards.find('.category span').html().trim()).toEqual('XSeries Program');
expect($cards.find('.organization').html().trim()).toEqual(program.organizations[0].display_name);
expect($cards.find('.card-link').attr('href')).toEqual(program.marketing_url);
});
it('should call reEvaluatePicture if reLoadBannerImage is called', function(){
spyOn(view, 'reEvaluatePicture');
view.reLoadBannerImage();
expect(view.reEvaluatePicture).toHaveBeenCalled();
});
it('should handle exceptions from reEvaluatePicture', function(){
spyOn(view, 'reEvaluatePicture').andCallFake(function(){
throw {name:'Picturefill had exceptions'};
});
view.reLoadBannerImage();
expect(view.reEvaluatePicture).toHaveBeenCalled();
expect(view.reLoadBannerImage).not.toThrow('Picturefill had exceptions');
});
});
}
);
......@@ -66,6 +66,7 @@
'_split': 'js/split',
'mathjax_delay_renderer': 'coffee/src/mathjax_delay_renderer',
'MathJaxProcessor': 'coffee/src/customwmd',
'picturefill': 'common/js/vendor/picturefill.min',
// Manually specify LMS files that are not converted to RequireJS
'history': 'js/vendor/history',
......
......@@ -69,6 +69,7 @@ lib_paths:
- xmodule_js/common_static/js/vendor/slick.core.js
- xmodule_js/common_static/js/vendor/slick.grid.js
- xmodule_js/common_static/js/vendor/jquery.event.drag-2.2.js
- xmodule_js/common_static/common/js/vendor/picturefill.min.js
# Paths to source JavaScript files
src_paths:
......
......@@ -94,7 +94,8 @@
"catch": "js/vendor/ova/catch/js/catch",
"handlebars": "js/vendor/ova/catch/js/handlebars-1.1.2",
"tinymce": "js/vendor/tinymce/js/tinymce/tinymce.full.min",
"jquery.tinymce": "js/vendor/tinymce/js/tinymce/jquery.tinymce.min"
"jquery.tinymce": "js/vendor/tinymce/js/tinymce/jquery.tinymce.min",
"picturefill": "common/js/vendor/picturefill.min"
// end of files needed by OVA
},
shim: {
......
......@@ -3,58 +3,90 @@
@import '../base/grid-settings';
@import 'neat/neat'; // lib - Neat
$card-height: 150px;
.program-card{
@include span-columns(12);
height: $card-height;
border:1px solid $border-color-l3;
box-sizing:border-box;
border: 1px solid $border-color-l3;
box-sizing: border-box;
padding: $baseline;
margin-bottom: $baseline;
position:relative;
position: relative;
display: inline;
.card-link{
position:absolute;
top:0;
bottom:0;
right:0;
left:0;
outline:0;
border:0;
z-index:1;
height: $card-height;
position: absolute;
top: 0;
bottom: 0;
right: 0;
left: 0;
border: 0;
z-index: 1;
opacity: 0.5;
&:hover,
&:focus{
opacity: 1;
}
.banner-image-container{
position: relative;
overflow: hidden;
height: 116px;
.banner-image{
position: absolute;
top: 0;
left: 50%;
z-index: 0;
transform: translate(-50%, 0);
min-height: 100%;
}
}
}
.text-section{
margin-top: 106px;
.meta-info{
@include outer-container;
margin-bottom: $baseline;
margin-bottom: $baseline*0.25;
font-size: em(12);
color: $gray;
.organization{
@include span-columns(6);
color: $gray;
white-space: nowrap;
overflow: hidden;
}
.category{
@include span-columns(6);
text-align:right;
span{
text-align: right;
.category-text{
@include float(right);
}
.xseries-icon{
@include float(right);
@include margin-right($baseline*0.2);
@include margin-right($baseline*0.25);
background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat;
background-color: transparent;
width: ($baseline*1);
height: ($baseline*1);
background-size: 100%;
width: ($baseline*0.7);
height: ($baseline*0.7);
}
}
}
.title{
font-size:em(30);
font-size: em(24);
color: $gray-l1;
margin-bottom: 10px;
line-height: 100%;
line-height: 1.2;
}
}
}
@include media($bp-small) {
.program-card{
@include omega(n);
@include span-columns(4);
.card-link{
.banner-image-container{
height: 166px;
}
}
.text-section{
margin-top: 156px;
}
}
}
......@@ -62,7 +94,16 @@ $card-height: 150px;
@include media($bp-medium) {
.program-card{
@include omega(n);
@include span-columns(8);
.card-link{
.banner-image-container{
height: 242px;
}
}
.text-section{
margin-top: 232px;
}
}
}
......@@ -72,7 +113,14 @@ $card-height: 150px;
.program-card{
@include omega(2n);
@include span-columns(6);
display:inline;
.card-link{
.banner-image-container{
height: 116px;
}
}
.text-section{
margin-top: 106px;
}
}
}
......@@ -80,7 +128,14 @@ $card-height: 150px;
.program-card{
@include omega(2n);
@include span-columns(6);
display:inline;
.card-link{
.banner-image-container{
height: 145px;
}
}
.text-section{
margin-top: 135px;
}
}
}
......@@ -3,6 +3,10 @@
@import '../base/grid-settings';
@import 'neat/neat'; // lib - Neat
//Patern Library button colors
$pl-button-border-color: #065683;
$pl-button-color: #0079bc;
.program-list-wrapper{
@include outer-container;
padding: $baseline $baseline;
......@@ -20,20 +24,30 @@
background-color: $body-bg;
box-sizing: border-box;
border: 1px solid $border-color-l3;
clear:both;
clear: both;
.advertise-message{
font-size:em(12);
font-size: em(12);
color: $gray-d4;
margin-bottom: $baseline;
}
.ad-link{
padding:$baseline * 0.5;
border: 1px solid $blue-t1;
font-size: em(16);
text-align:center;
a{
padding: $baseline * 0.5;
border: 1px solid $pl-button-border-color;
color: $pl-button-color;
font-size: em(16);
text-decoration: none;
&:hover, &:focus, &:active{
background-color: $button-bg-hover-color;
display: block;
line-height: 1.2;
&:hover,
&:focus,
&:active{
color: $white;
background-color: $pl-button-color;
}
span{
@include margin-left($baseline*0.25);
}
}
}
......
<div class="banner-image">
<a href="<%- marketingUrl %>" class="card-link">
<img alt="<%- gettext(name)%>" src="" />
</a>
</div>
<a href="<%- marketingUrl %>" class="card-link">
<div class="banner-image-container">
<picture>
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.tiny %>)">
<source srcset="<%- mediumBannerUrl %>" media="(max-width: <%- breakpoints.max.small %>)">
<source srcset="<%- largeBannerUrl %>" media="(max-width: <%- breakpoints.max.medium %>)">
<source srcset="<%- smallBannerUrl %>" media="(max-width: <%- breakpoints.max.large %>)">
<img class="banner-image" srcset="<%- mediumBannerUrl %>" alt="<%- gettext(name)%>">
</picture>
</div>
</a>
<div class="text-section">
<div class="meta-info">
<div class="organization">
<% _.each(organizations, function(org){ %>
<span><%- gettext(org.display_name) %></span>
<%- gettext(org.display_name) %>
<% }); %>
</div>
<div class="category">
<span><%- gettext(category) %></span>
<span class="category-text"><%- gettext(type) %></span>
<i class="xseries-icon" aria-hidden="true"></i>
</div>
</div>
......
......@@ -18,6 +18,7 @@ from openedx.core.djangoapps.programs.utils import (
get_programs_for_dashboard,
get_programs_for_credentials,
get_engaged_programs,
get_display_category
)
from student.tests.factories import UserFactory, CourseEnrollmentFactory
......@@ -109,6 +110,7 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
actual = get_programs_for_dashboard(self.user, self.COURSE_KEYS)
expected = {}
for program in self.PROGRAMS_API_RESPONSE['results']:
program['display_category'] = get_display_category(program)
for course_code in program['course_codes']:
for run in course_code['run_modes']:
course_key = run['course_key']
......@@ -206,6 +208,8 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
actual = get_engaged_programs(self.user, enrollments)
programs = self.PROGRAMS_API_RESPONSE['results']
for program in programs:
program['display_category'] = get_display_category(program)
# 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
......@@ -234,6 +238,8 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
actual = get_engaged_programs(self.user, enrollments)
programs = self.PROGRAMS_API_RESPONSE['results']
for program in programs:
program['display_category'] = get_display_category(program)
expected = [programs[0]]
self.assertEqual(expected, actual)
......@@ -251,6 +257,8 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
actual = get_engaged_programs(self.user, enrollments)
programs = self.PROGRAMS_API_RESPONSE['results']
for program in programs:
program['display_category'] = get_display_category(program)
expected = programs[-2:]
self.assertEqual(expected, actual)
......@@ -277,3 +285,16 @@ class TestProgramRetrieval(ProgramsApiConfigMixin, ProgramsDataMixin,
expected = []
self.assertEqual(expected, actual)
@httpretty.activate
def test_get_display_category_success(self):
self.create_programs_config()
self.mock_programs_api()
actual_programs = get_programs(self.user)
for program in actual_programs:
expected = 'XSeries'
self.assertEqual(expected, get_display_category(program))
def test_get_display_category_none(self):
self.assertEqual('', get_display_category(None))
self.assertEqual('', get_display_category({"id": "test"}))
......@@ -46,6 +46,7 @@ def flatten_programs(programs, course_ids):
for run in course_code['run_modes']:
run_id = run['course_key']
if run_id in course_ids:
program['display_category'] = get_display_category(program)
flattened.setdefault(run_id, []).append(program)
except KeyError:
log.exception('Unable to parse Programs API response: %r', program)
......@@ -113,6 +114,24 @@ def get_programs_for_credentials(user, programs_credentials):
return certificate_programs
def get_display_category(program):
""" Given the program, return the category of the program for display
Arguments:
program (Program): The program to get the display category string from
Returns:
string, the category for display to the user.
Empty string if the program has no category or is null.
"""
display_candidate = ''
if program and program.get('category'):
if program.get('category') == 'xseries':
display_candidate = 'XSeries'
else:
display_candidate = program.get('category', '').capitalize()
return display_candidate
def get_engaged_programs(user, enrollments):
"""Derive a list of programs in which the given user is engaged.
......
......@@ -8,7 +8,8 @@
"requirejs": "~2.1.22",
"uglify-js": "2.4.24",
"underscore": "~1.8.3",
"underscore.string": "~3.3.4"
"underscore.string": "~3.3.4",
"picturefill": "~3.0.2"
},
"devDependencies": {
"jshint": "^2.7.0",
......
......@@ -45,7 +45,8 @@ COMMON_LOOKUP_PATHS = [
# static directory.
NPM_INSTALLED_LIBRARIES = [
'underscore/underscore.js',
'underscore.string/dist/underscore.string.js'
'underscore.string/dist/underscore.string.js',
'picturefill/dist/picturefill.min.js'
]
# Directory to install static vendor files
......
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