diff --git a/lms/djangoapps/learner_dashboard/README.rst b/lms/djangoapps/learner_dashboard/README.rst new file mode 100644 index 0000000000..7fd834058c --- /dev/null +++ b/lms/djangoapps/learner_dashboard/README.rst @@ -0,0 +1,23 @@ +Learner Dashboard +================= + +This Django app hosts dashboard pages used by edX learners. The intent is for this Django app to include the following three important dashboard tabs: + - Courses + - Programs + - Profile + +Courses +--------------- +The learner-facing dashboard listing active and archived enrollments. The current implementation of the dashboard resides in ``common/djangoapps/student/``. The goal is to replace the existing dashboard with a Backbone app served by this Django app. + +Programs +--------------- +A page listing programs in which the learner is engaged. The page also shows learners' progress towards completing the programs. Programs are structured collections of course runs which culminate into a certificate. + +Implementation +^^^^^^^^^^^^^^^^^^^^^ +The ``views`` module contains the Django views used to serve the Program listing page. The corresponding Backbone app is in the ``edx-platform/static/js/learner_dashboard``. + +Profile +--------------- +A page allowing learners to see what they have accomplished and view credits or certificates they have earned on the edX platform. diff --git a/lms/djangoapps/learner_dashboard/__init__.py b/lms/djangoapps/learner_dashboard/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/learner_dashboard/tests/__init__.py b/lms/djangoapps/learner_dashboard/tests/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/djangoapps/learner_dashboard/tests/test_programs.py b/lms/djangoapps/learner_dashboard/tests/test_programs.py new file mode 100644 index 0000000000..5f22787557 --- /dev/null +++ b/lms/djangoapps/learner_dashboard/tests/test_programs.py @@ -0,0 +1,141 @@ +""" +Tests for viewing the programs enrolled by a learner. +""" +import datetime +import httpretty +import unittest +from urlparse import urljoin + +from django.conf import settings +from django.core.urlresolvers import reverse +from django.http import HttpResponseRedirect +from django.test import override_settings +from oauth2_provider.tests.factories import ClientFactory +from opaque_keys.edx import locator +from provider.constants import CONFIDENTIAL + +from openedx.core.djangoapps.programs.tests.mixins import ( + ProgramsApiConfigMixin, + ProgramsDataMixin) +from openedx.core.djangoapps.programs.models import ProgramsApiConfig +from student.models import CourseEnrollment +from student.tests.factories import UserFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + + +@unittest.skipUnless(settings.ROOT_URLCONF == 'lms.urls', 'Test only valid in lms') +@override_settings(MKTG_URLS={'ROOT': 'http://edx.org'}) +class TestProgramListing( + ModuleStoreTestCase, + ProgramsApiConfigMixin, + ProgramsDataMixin): + + """ + Unit tests for getting the list of programs enrolled by a logged in user + """ + PASSWORD = 'test' + + def setUp(self): + """ + Add a student + """ + super(TestProgramListing, self).setUp() + ClientFactory(name=ProgramsApiConfig.OAUTH2_CLIENT_NAME, client_type=CONFIDENTIAL) + self.student = UserFactory() + self.create_programs_config(xseries_ad_enabled=True) + + def _create_course_and_enroll(self, student, org, course, run): + """ + Creates a course and associated enrollment. + """ + course_location = locator.CourseLocator(org, course, run) + course = CourseFactory.create( + org=course_location.org, + number=course_location.course, + run=course_location.run + ) + enrollment = CourseEnrollment.enroll(student, course.id) + enrollment.created = datetime.datetime(2000, 12, 31, 0, 0, 0, 0) + enrollment.save() + + def _get_program_url(self, marketing_slug): + """ + Helper function to get the program card url + """ + return urljoin( + settings.MKTG_URLS.get('ROOT'), + 'xseries' + '/{}' + ).format(marketing_slug) + + def _setup_and_get_program(self): + """ + The core function to setup the mock program api, + then call the django test client to get the actual program listing page + make sure the request suceeds and make sure x_series_url is on the page + """ + self.mock_programs_api() + self.client.login(username=self.student.username, password=self.PASSWORD) + response = self.client.get(reverse("program_listing_view")) + self.assertEqual(response.status_code, 200) + x_series_url = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries') + self.assertIn(x_series_url, response.content) + return response + + def _get_program_checklist(self, program_id): + """ + The convenience function to get all the program related page element we would like to check against + """ + return [ + self.PROGRAM_NAMES[program_id], + self._get_program_url(self.PROGRAMS_API_RESPONSE['results'][program_id]['marketing_slug']), + self.PROGRAMS_API_RESPONSE['results'][program_id]['organizations'][0]['display_name'], + ] + + @httpretty.activate + def test_get_program_with_no_enrollment(self): + response = self._setup_and_get_program() + for program_element in self._get_program_checklist(0): + self.assertNotIn(program_element, response.content) + for program_element in self._get_program_checklist(1): + self.assertNotIn(program_element, response.content) + + @httpretty.activate + def test_get_one_program(self): + self._create_course_and_enroll(self.student, *self.COURSE_KEYS[0].split('/')) + response = self._setup_and_get_program() + for program_element in self._get_program_checklist(0): + self.assertIn(program_element, response.content) + for program_element in self._get_program_checklist(1): + self.assertNotIn(program_element, response.content) + + @httpretty.activate + def test_get_both_program(self): + self._create_course_and_enroll(self.student, *self.COURSE_KEYS[0].split('/')) + self._create_course_and_enroll(self.student, *self.COURSE_KEYS[5].split('/')) + response = self._setup_and_get_program() + for program_element in self._get_program_checklist(0): + self.assertIn(program_element, response.content) + for program_element in self._get_program_checklist(1): + self.assertIn(program_element, response.content) + + def test_get_programs_dashboard_not_enabled(self): + self.create_programs_config(enable_student_dashboard=False) + self.client.login(username=self.student.username, password=self.PASSWORD) + response = self.client.get(reverse("program_listing_view")) + self.assertEqual(response.status_code, 404) + + def test_xseries_advertise_disabled(self): + self.create_programs_config(xseries_ad_enabled=False) + self.client.login(username=self.student.username, password=self.PASSWORD) + response = self.client.get(reverse("program_listing_view")) + self.assertEqual(response.status_code, 200) + x_series_url = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries') + self.assertNotIn(x_series_url, response.content) + + def test_get_programs_not_logged_in(self): + self.create_programs_config() + response = self.client.get(reverse("program_listing_view")) + self.assertEqual(response.status_code, 302) + self.assertIsInstance(response, HttpResponseRedirect) + self.assertIn('login', response.url) # pylint: disable=no-member diff --git a/lms/djangoapps/learner_dashboard/urls.py b/lms/djangoapps/learner_dashboard/urls.py new file mode 100644 index 0000000000..548724c14a --- /dev/null +++ b/lms/djangoapps/learner_dashboard/urls.py @@ -0,0 +1,10 @@ +""" +Learner's Dashboard urls +""" + +from django.conf.urls import url +from . import views + +urlpatterns = [ + url(r'^programs/$', views.view_programs, name='program_listing_view'), +] diff --git a/lms/djangoapps/learner_dashboard/views.py b/lms/djangoapps/learner_dashboard/views.py new file mode 100644 index 0000000000..85026a445c --- /dev/null +++ b/lms/djangoapps/learner_dashboard/views.py @@ -0,0 +1,36 @@ +"""New learner dashboard views.""" +from urlparse import urljoin + +from django.conf import settings +from django.contrib.auth.decorators import login_required +from django.views.decorators.http import require_GET +from django.http import Http404 + +from edxmako.shortcuts import render_to_response +from openedx.core.djangoapps.programs.utils import get_engaged_programs +from openedx.core.djangoapps.programs.models import ProgramsApiConfig +from student.views import get_course_enrollments + + +@login_required +@require_GET +def view_programs(request): + """View programs in which the user is engaged.""" + if not ProgramsApiConfig.current().is_student_dashboard_enabled: + raise Http404 + + enrollments = list(get_course_enrollments(request.user, None, [])) + programs = get_engaged_programs(request.user, enrollments) + + # TODO: Pull 'xseries' string from configuration model. + marketing_root = urljoin(settings.MKTG_URLS.get('ROOT'), 'xseries').strip('/') + for program in programs: + program['marketing_url'] = '{root}/{slug}'.format( + root=marketing_root, + slug=program['marketing_slug'] + ) + + return render_to_response('learner_dashboard/programs.html', { + 'programs': programs, + 'xseries_url': marketing_root if ProgramsApiConfig.current().show_xseries_ad else None + }) diff --git a/lms/envs/common.py b/lms/envs/common.py index 288f78411c..57a7b02a2a 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2019,6 +2019,9 @@ INSTALLED_APPS = ( # Verified Track Content Cohorting 'verified_track_content', + + # Learner's dashboard + 'learner_dashboard', ) # Migrations which are not in the standard module "migrations" diff --git a/lms/static/js/learner_dashboard/collections/program_collection.js b/lms/static/js/learner_dashboard/collections/program_collection.js new file mode 100644 index 0000000000..47ca233e44 --- /dev/null +++ b/lms/static/js/learner_dashboard/collections/program_collection.js @@ -0,0 +1,12 @@ +(function (define) { + 'use strict'; + define([ + 'backbone', + 'js/learner_dashboard/models/program_model' + ], + function (Backbone, Program) { + return Backbone.Collection.extend({ + model: Program + }); + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/models/program_model.js b/lms/static/js/learner_dashboard/models/program_model.js new file mode 100644 index 0000000000..7a5ece3cf3 --- /dev/null +++ b/lms/static/js/learner_dashboard/models/program_model.js @@ -0,0 +1,24 @@ +/** + * Model for Course Programs. + */ +(function (define) { + 'use strict'; + define([ + 'backbone' + ], + function (Backbone) { + return Backbone.Model.extend({ + initialize: function(data) { + if (data){ + this.set({ + name: data.name, + category: data.category, + subtitle: data.subtitle, + organizations: data.organizations, + marketingUrl: data.marketing_url + }); + } + } + }); + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/program_list_factory.js b/lms/static/js/learner_dashboard/program_list_factory.js new file mode 100644 index 0000000000..8384ed9a94 --- /dev/null +++ b/lms/static/js/learner_dashboard/program_list_factory.js @@ -0,0 +1,24 @@ +;(function (define) { + 'use strict'; + + define([ + 'js/learner_dashboard/views/collection_list_view', + 'js/learner_dashboard/views/sidebar_view', + 'js/learner_dashboard/views/program_card_view', + 'js/learner_dashboard/collections/program_collection' + ], + function (CollectionListView, SidebarView, ProgramCardView, ProgramCollection) { + return function (options) { + new CollectionListView({ + el: '.program-cards-container', + childView: ProgramCardView, + collection: new ProgramCollection(options.programsData) + }).render(); + + new SidebarView({ + el: '.sidebar', + context: options + }).render(); + }; + }); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/views/collection_list_view.js b/lms/static/js/learner_dashboard/views/collection_list_view.js new file mode 100644 index 0000000000..ab9fd18a5c --- /dev/null +++ b/lms/static/js/learner_dashboard/views/collection_list_view.js @@ -0,0 +1,24 @@ +;(function (define) { + 'use strict'; + + define(['backbone'], + function( + Backbone + ) { + return Backbone.View.extend({ + initialize: function(data) { + this.childView = data.childView; + }, + + render: function() { + var childList = []; + this.collection.each(function(program){ + var child = new this.childView({model:program}); + childList.push(child.el); + }, this); + this.$el.html(childList); + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/views/program_card_view.js b/lms/static/js/learner_dashboard/views/program_card_view.js new file mode 100644 index 0000000000..ce3a8650f6 --- /dev/null +++ b/lms/static/js/learner_dashboard/views/program_card_view.js @@ -0,0 +1,30 @@ +;(function (define) { + 'use strict'; + + define(['backbone', + 'jquery', + 'underscore', + 'gettext', + 'text!../../../templates/learner_dashboard/program_card.underscore' + ], + function( + Backbone, + $, + _, + gettext, + programCardTpl + ) { + return Backbone.View.extend({ + className: 'program-card', + tpl: _.template(programCardTpl), + initialize: function() { + this.render(); + }, + render: function() { + var templated = this.tpl(this.model.toJSON()); + this.$el.html(templated); + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/learner_dashboard/views/sidebar_view.js b/lms/static/js/learner_dashboard/views/sidebar_view.js new file mode 100644 index 0000000000..d373463331 --- /dev/null +++ b/lms/static/js/learner_dashboard/views/sidebar_view.js @@ -0,0 +1,32 @@ +;(function (define) { + 'use strict'; + + define(['backbone', + 'jquery', + 'underscore', + 'gettext', + 'text!../../../templates/learner_dashboard/sidebar.underscore' + ], + function( + Backbone, + $, + _, + gettext, + sidebarTpl + ) { + return Backbone.View.extend({ + el: '.sidebar', + tpl: _.template(sidebarTpl), + initialize: function(data) { + this.context = data.context; + }, + render: function() { + if (this.context.xseriesUrl){ + //Only show the xseries advertising panel if the link is passed in + this.$el.html(this.tpl(this.context)); + } + } + }); + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/spec/learner_dashboard/collection_list_view_spec.js b/lms/static/js/spec/learner_dashboard/collection_list_view_spec.js new file mode 100644 index 0000000000..cda26f6a4e --- /dev/null +++ b/lms/static/js/spec/learner_dashboard/collection_list_view_spec.js @@ -0,0 +1,96 @@ +define([ + 'backbone', + 'jquery', + 'js/learner_dashboard/views/program_card_view', + 'js/learner_dashboard/collections/program_collection', + 'js/learner_dashboard/views/collection_list_view' + ], function (Backbone, $, ProgramCardView, ProgramCollection, CollectionListView) { + + 'use strict'; + /*jslint maxlen: 500 */ + + describe('Collection List View', function () { + var view = null, + programCollection, + context = { + programsData:[ + { + category: 'xseries', + status: 'active', + subtitle: 'program 1', + name: 'test program 1', + organizations: [ + { + display_name: 'edX', + key: 'edx' + } + ], + created: '2016-03-03T19:18:50.061136Z', + 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' + }, + { + category: 'xseries', + status: 'active', + subtitle: 'fda', + name: 'fda', + organizations: [ + { + display_name: 'edX', + key: 'edx' + } + ], + created: '2016-03-09T14:30:41.484848Z', + modified: '2016-03-09T14:30:52.840898Z', + marketing_slug: 'gdaf', + id: 147, + marketing_url: 'http://www.edx.org/xseries/gdaf' + } + ] + }; + + beforeEach(function() { + setFixtures('
'); + programCollection = new ProgramCollection(context.programsData); + view = new CollectionListView({ + el: '.program-cards-container', + childView: ProgramCardView, + collection: programCollection + }); + view.render(); + }); + + afterEach(function() { + view.remove(); + }); + + it('should exist', function() { + expect(view).toBeDefined(); + }); + + it('should load the collection items based on passed in collection', function() { + var $cards = view.$el.find('.program-card'); + expect($cards.length).toBe(2); + $cards.each(function(index, el){ + expect($(el).find('.title').html().trim()).toEqual(context.programsData[index].name); + }); + }); + + it('should display no item if collection is empty', function(){ + var $cards; + view.remove(); + programCollection = new ProgramCollection([]); + view = new CollectionListView({ + el: '.program-cards-container', + childView: ProgramCardView, + collection: programCollection + }); + view.render(); + $cards = view.$el.find('.program-card'); + expect($cards.length).toBe(0); + }); + }); + } +); diff --git a/lms/static/js/spec/learner_dashboard/program_card_view_spec.js b/lms/static/js/spec/learner_dashboard/program_card_view_spec.js new file mode 100644 index 0000000000..f4942fc00c --- /dev/null +++ b/lms/static/js/spec/learner_dashboard/program_card_view_spec.js @@ -0,0 +1,58 @@ +define([ + 'backbone', + 'jquery', + 'js/learner_dashboard/views/program_card_view', + 'js/learner_dashboard/models/program_model' + ], function (Backbone, $, ProgramCardView, ProgramModel) { + + 'use strict'; + /*jslint maxlen: 500 */ + + describe('Program card View', function () { + var view = null, + programModel, + program = { + category: 'xseries', + status: 'active', + subtitle: 'program 1', + name: 'test program 1', + organizations: [ + { + display_name: 'edX', + key: 'edx' + } + ], + created: '2016-03-03T19:18:50.061136Z', + 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' + }; + + beforeEach(function() { + setFixtures(''); + programModel = new ProgramModel(program); + view = new ProgramCardView({ + model: programModel + }); + }); + + afterEach(function() { + view.remove(); + }); + + it('should exist', function() { + expect(view).toBeDefined(); + }); + + it('should load the program-cards based on passed in context', function() { + 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('.card-link').attr('href')).toEqual(program.marketing_url); + }); + }); + } +); diff --git a/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js b/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js new file mode 100644 index 0000000000..4b9104e810 --- /dev/null +++ b/lms/static/js/spec/learner_dashboard/sidebar_view_spec.js @@ -0,0 +1,54 @@ +define([ + 'backbone', + 'jquery', + 'js/learner_dashboard/views/sidebar_view' + ], function (Backbone, $, SidebarView) { + + 'use strict'; + /*jslint maxlen: 500 */ + + describe('Sidebar View', function () { + var view = null, + context = { + xseriesUrl: 'http://www.edx.org/xseries' + }; + + beforeEach(function() { + setFixtures(''); + + view = new SidebarView({ + el: '.sidebar', + context: context + }); + view.render(); + }); + + afterEach(function() { + view.remove(); + }); + + it('should exist', function() { + expect(view).toBeDefined(); + }); + + it('should load the xseries advertising based on passed in xseries URL', function() { + var $sidebar = view.$el; + expect($sidebar.find('.program-advertise .advertise-message').html().trim()) + .toEqual('Browse recently launched courses and see what\'s new in our favorite subjects'); + expect($sidebar.find('.program-advertise .ad-link a').attr('href')).toEqual(context.xseriesUrl); + }); + + it('should not load the xseries advertising if no xseriesUrl passed in', function(){ + var $ad; + view.remove(); + view = new SidebarView({ + el: '.sidebar', + context: {} + }); + view.render(); + $ad = view.$el.find('.program-advertise'); + expect($ad.length).toBe(0); + }); + }); + } +); diff --git a/lms/static/js/spec/main.js b/lms/static/js/spec/main.js index 97f57f521f..cc72307351 100644 --- a/lms/static/js/spec/main.js +++ b/lms/static/js/spec/main.js @@ -738,7 +738,10 @@ 'lms/include/js/spec/bookmarks/bookmarks_list_view_spec.js', 'lms/include/js/spec/bookmarks/bookmark_button_view_spec.js', 'lms/include/js/spec/views/message_banner_spec.js', - 'lms/include/js/spec/markdown_editor_spec.js' + 'lms/include/js/spec/markdown_editor_spec.js', + 'lms/include/js/spec/learner_dashboard/collection_list_view_spec.js', + 'lms/include/js/spec/learner_dashboard/sidebar_view_spec.js', + 'lms/include/js/spec/learner_dashboard/program_card_view_spec.js' ]); }).call(this, requirejs, define); diff --git a/lms/static/js_test.yml b/lms/static/js_test.yml index 767fe9311f..c82b77311f 100644 --- a/lms/static/js_test.yml +++ b/lms/static/js_test.yml @@ -119,6 +119,7 @@ fixture_paths: - support/templates - js/fixtures/bookmarks - templates/bookmarks + - templates/learner_dashboard requirejs: paths: diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js index 747097574d..45afb1dfa3 100644 --- a/lms/static/lms/js/build.js +++ b/lms/static/lms/js/build.js @@ -33,7 +33,8 @@ 'teams/js/teams_tab_factory', 'support/js/certificates_factory', 'support/js/enrollment_factory', - 'js/bookmarks/bookmarks_factory' + 'js/bookmarks/bookmarks_factory', + 'js/learner_dashboard/program_list_factory' ]), /** diff --git a/lms/static/sass/_build-lms.scss b/lms/static/sass/_build-lms.scss index 88ee8ce3c0..b749e0f28f 100644 --- a/lms/static/sass/_build-lms.scss +++ b/lms/static/sass/_build-lms.scss @@ -17,6 +17,7 @@ @import 'elements/controls'; @import 'elements/pagination'; @import 'elements/creative-commons'; +@import 'elements/program-card'; // shared - course @import 'shared/fields'; @@ -57,6 +58,7 @@ @import "views/financial-assistance"; @import 'views/bookmarks'; @import 'course/auto-cert'; +@import 'views/program-list'; // app - discussion @import "discussion/utilities/variables"; diff --git a/lms/static/sass/elements/_program-card.scss b/lms/static/sass/elements/_program-card.scss new file mode 100644 index 0000000000..77f8d8df9c --- /dev/null +++ b/lms/static/sass/elements/_program-card.scss @@ -0,0 +1,86 @@ +// +Imports +// ==================== +@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; + padding: $baseline; + margin-bottom: $baseline; + position:relative; + .card-link{ + position:absolute; + top:0; + bottom:0; + right:0; + left:0; + outline:0; + border:0; + z-index:1; + height: $card-height; + } + .text-section{ + .meta-info{ + @include outer-container; + margin-bottom: $baseline; + font-size: em(12); + .organization{ + @include span-columns(6); + color: $gray; + } + .category{ + @include span-columns(6); + text-align:right; + span{ + @include float(right); + } + .xseries-icon{ + @include float(right); + @include margin-right($baseline*0.2); + background: url('#{$static-path}/images/icon-sm-xseries-black.png') no-repeat; + background-color: transparent; + + width: ($baseline*1); + height: ($baseline*1); + } + } + } + .title{ + font-size:em(30); + color: $gray-l1; + margin-bottom: 10px; + line-height: 100%; + } + } +} + + +@include media($bp-medium) { + .program-card{ + @include span-columns(8); + } +} + + + +@include media($bp-large) { + .program-card{ + @include omega(2n); + @include span-columns(6); + display:inline; + } +} + +@include media($bp-huge) { + .program-card{ + @include omega(2n); + @include span-columns(6); + display:inline; + } +} + diff --git a/lms/static/sass/views/_program-list.scss b/lms/static/sass/views/_program-list.scss new file mode 100644 index 0000000000..75a287962d --- /dev/null +++ b/lms/static/sass/views/_program-list.scss @@ -0,0 +1,74 @@ +// +Imports +// ==================== +@import '../base/grid-settings'; +@import 'neat/neat'; // lib - Neat + +.program-list-wrapper{ + @include outer-container; + padding: $baseline $baseline; +} + +.program-cards-container{ + @include outer-container; + @include span-columns(12); +} +.sidebar{ + @include outer-container; + @include span-columns(12); + .program-advertise{ + padding: $baseline; + background-color: $body-bg; + box-sizing: border-box; + border: 1px solid $border-color-l3; + clear:both; + .advertise-message{ + 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); + a{ + text-decoration: none; + &:hover, &:focus, &:active{ + background-color: $button-bg-hover-color; + } + } + } + } +} + + + +@include media($bp-medium) { + .program-cards-container{ + @include span-columns(8); + } + .sidebar{ + @include span-columns(8); + } +} + + + +@include media($bp-large) { + .program-cards-container{ + @include span-columns(9); + } + .sidebar{ + @include omega(n); + @include span-columns(3); + } +} + +@include media($bp-huge) { + .program-cards-container{ + @include span-columns(9); + } + .sidebar{ + @include omega(n); + @include span-columns(3); + } +} diff --git a/lms/templates/learner_dashboard/program_card.underscore b/lms/templates/learner_dashboard/program_card.underscore new file mode 100644 index 0000000000..644a704251 --- /dev/null +++ b/lms/templates/learner_dashboard/program_card.underscore @@ -0,0 +1,24 @@ + + +