Merge pull request #11879 from edx/schen/ECOM-3194
ECOM-3194 Create the listing page of Programs (x-series) which learners have enrolled in
This commit is contained in:
23
lms/djangoapps/learner_dashboard/README.rst
Normal file
23
lms/djangoapps/learner_dashboard/README.rst
Normal file
@@ -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.
|
||||
0
lms/djangoapps/learner_dashboard/__init__.py
Normal file
0
lms/djangoapps/learner_dashboard/__init__.py
Normal file
0
lms/djangoapps/learner_dashboard/tests/__init__.py
Normal file
0
lms/djangoapps/learner_dashboard/tests/__init__.py
Normal file
141
lms/djangoapps/learner_dashboard/tests/test_programs.py
Normal file
141
lms/djangoapps/learner_dashboard/tests/test_programs.py
Normal file
@@ -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
|
||||
10
lms/djangoapps/learner_dashboard/urls.py
Normal file
10
lms/djangoapps/learner_dashboard/urls.py
Normal file
@@ -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'),
|
||||
]
|
||||
36
lms/djangoapps/learner_dashboard/views.py
Normal file
36
lms/djangoapps/learner_dashboard/views.py
Normal file
@@ -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
|
||||
})
|
||||
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
24
lms/static/js/learner_dashboard/models/program_model.js
Normal file
24
lms/static/js/learner_dashboard/models/program_model.js
Normal file
@@ -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);
|
||||
24
lms/static/js/learner_dashboard/program_list_factory.js
Normal file
24
lms/static/js/learner_dashboard/program_list_factory.js
Normal file
@@ -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);
|
||||
@@ -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);
|
||||
30
lms/static/js/learner_dashboard/views/program_card_view.js
Normal file
30
lms/static/js/learner_dashboard/views/program_card_view.js
Normal file
@@ -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);
|
||||
32
lms/static/js/learner_dashboard/views/sidebar_view.js
Normal file
32
lms/static/js/learner_dashboard/views/sidebar_view.js
Normal file
@@ -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);
|
||||
@@ -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('<div class="program-cards-container"></div>');
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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('<div class="program-card"></div>');
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
54
lms/static/js/spec/learner_dashboard/sidebar_view_spec.js
Normal file
54
lms/static/js/spec/learner_dashboard/sidebar_view_spec.js
Normal file
@@ -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('<div class="sidebar"></div>');
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
@@ -119,6 +119,7 @@ fixture_paths:
|
||||
- support/templates
|
||||
- js/fixtures/bookmarks
|
||||
- templates/bookmarks
|
||||
- templates/learner_dashboard
|
||||
|
||||
requirejs:
|
||||
paths:
|
||||
|
||||
@@ -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'
|
||||
]),
|
||||
|
||||
/**
|
||||
|
||||
@@ -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";
|
||||
|
||||
86
lms/static/sass/elements/_program-card.scss
Normal file
86
lms/static/sass/elements/_program-card.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
74
lms/static/sass/views/_program-list.scss
Normal file
74
lms/static/sass/views/_program-list.scss
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
24
lms/templates/learner_dashboard/program_card.underscore
Normal file
24
lms/templates/learner_dashboard/program_card.underscore
Normal file
@@ -0,0 +1,24 @@
|
||||
|
||||
<div class="banner-image">
|
||||
<a href="<%- marketingUrl %>" class="card-link">
|
||||
<img alt="<%- gettext(name)%>" src="" />
|
||||
</a>
|
||||
</div>
|
||||
<div class="text-section">
|
||||
<div class="meta-info">
|
||||
<div class="organization">
|
||||
<% _.each(organizations, function(org){ %>
|
||||
<span><%- gettext(org.display_name) %></span>
|
||||
<% }); %>
|
||||
</div>
|
||||
<div class="category">
|
||||
<span><%- gettext(category) %></span>
|
||||
<i class="xseries-icon" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="title" aria-hidden="true">
|
||||
<%- gettext(name) %>
|
||||
</div>
|
||||
</div>
|
||||
<div class="progress">
|
||||
</div>
|
||||
25
lms/templates/learner_dashboard/programs.html
Normal file
25
lms/templates/learner_dashboard/programs.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%inherit file="../main.html" />
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
<%!
|
||||
from django.utils.translation import ugettext as _
|
||||
from openedx.core.djangolib.js_utils import (
|
||||
dump_js_escaped_json, js_escaped_string
|
||||
)
|
||||
%>
|
||||
|
||||
<%block name="js_extra">
|
||||
<%static:require_module module_name="js/learner_dashboard/program_list_factory" class_name="ProgramListFactory">
|
||||
ProgramListFactory({
|
||||
programsData: ${programs | n, dump_js_escaped_json},
|
||||
xseriesUrl: '${xseries_url | n, js_escaped_string}'
|
||||
});
|
||||
</%static:require_module>
|
||||
</%block>
|
||||
|
||||
<%block name="pagetitle">${_("Programs")}</%block>
|
||||
|
||||
<div class="program-list-wrapper">
|
||||
<div class="program-cards-container"></div>
|
||||
<div class="sidebar"></div>
|
||||
</div>
|
||||
13
lms/templates/learner_dashboard/sidebar.underscore
Normal file
13
lms/templates/learner_dashboard/sidebar.underscore
Normal file
@@ -0,0 +1,13 @@
|
||||
<div class="program-advertise">
|
||||
<div class="advertise-message">
|
||||
<%- gettext('Browse recently launched courses and see what\'s new in our favorite subjects') %>
|
||||
</div>
|
||||
<div class="ad-link">
|
||||
<a href="<%- xseriesUrl %>" class="btn">
|
||||
<i class="icon fa fa-search" aria-hidden="true"></i>
|
||||
<span><%- gettext('Explore New XSeries') %></span>
|
||||
</a>
|
||||
</div
|
||||
</div>
|
||||
<div class="certificate-container">
|
||||
</div>
|
||||
@@ -112,6 +112,10 @@ urlpatterns = (
|
||||
url(r'^verify_student/', include('verify_student.urls')),
|
||||
)
|
||||
|
||||
urlpatterns += (
|
||||
url(r'^dashboard/', include('learner_dashboard.urls')),
|
||||
)
|
||||
|
||||
if settings.FEATURES["ENABLE_COMBINED_LOGIN_REGISTRATION"]:
|
||||
# Backwards compatibility with old URL structure, but serve the new views
|
||||
urlpatterns += (
|
||||
|
||||
Reference in New Issue
Block a user