diff --git a/common/lib/xmodule/xmodule/assets/vertical/public/js/vertical_student_view.js b/common/lib/xmodule/xmodule/assets/vertical/public/js/vertical_student_view.js
index 123b1c024c..065480b8b6 100644
--- a/common/lib/xmodule/xmodule/assets/vertical/public/js/vertical_student_view.js
+++ b/common/lib/xmodule/xmodule/assets/vertical/public/js/vertical_student_view.js
@@ -1,7 +1,7 @@
/* JavaScript for Vertical Student View. */
window.VerticalStudentView = function(runtime, element) {
'use strict';
- RequireJS.require(['js/bookmarks/views/bookmark_button'], function(BookmarkButton) {
+ RequireJS.require(['course_bookmarks/js/views/bookmark_button'], function(BookmarkButton) {
var $element = $(element);
var $bookmarkButtonElement = $element.find('.bookmark-button');
@@ -10,7 +10,7 @@ window.VerticalStudentView = function(runtime, element) {
bookmarkId: $bookmarkButtonElement.data('bookmarkId'),
usageId: $element.data('usageId'),
bookmarked: $element.parent('#seq_content').data('bookmarked'),
- apiUrl: $('.courseware-bookmarks-button').data('bookmarksApiUrl')
+ apiUrl: $bookmarkButtonElement.data('bookmarksApiUrl')
});
});
};
diff --git a/lms/djangoapps/courseware/tabs.py b/lms/djangoapps/courseware/tabs.py
index 0720dc5ff6..84c655e1c1 100644
--- a/lms/djangoapps/courseware/tabs.py
+++ b/lms/djangoapps/courseware/tabs.py
@@ -37,16 +37,24 @@ class CoursewareTab(EnrolledTab):
is_movable = False
is_default = False
+ @staticmethod
+ def main_course_url_name(request):
+ """
+ Returns the main course URL for the current user.
+ """
+ if waffle.flag_is_active(request, 'unified_course_view'):
+ return 'edx.course_experience.course_home'
+ else:
+ return 'courseware'
+
@property
def link_func(self):
"""
Returns a function that computes the URL for this tab.
"""
request = RequestCache.get_current_request()
- if waffle.flag_is_active(request, 'unified_course_view'):
- return link_reverse_func('edx.course_experience.course_home')
- else:
- return link_reverse_func('courseware')
+ url_name = self.main_course_url_name(request)
+ return link_reverse_func(url_name)
class CourseInfoTab(CourseTab):
diff --git a/lms/djangoapps/courseware/views/index.py b/lms/djangoapps/courseware/views/index.py
index f7808b41e1..2f51b2a545 100644
--- a/lms/djangoapps/courseware/views/index.py
+++ b/lms/djangoapps/courseware/views/index.py
@@ -38,6 +38,7 @@ from util.views import ensure_valid_course_key
from xmodule.modulestore.django import modulestore
from xmodule.x_module import STUDENT_VIEW
from survey.utils import must_answer_survey
+from web_fragments.fragment import Fragment
from ..access import has_access, _adjust_start_date_for_beta_testers
from ..access_utils import in_preview_mode
@@ -401,7 +402,6 @@ class CoursewareIndex(View):
request = RequestCache.get_current_request()
courseware_context = {
'csrf': csrf(self.request)['csrf_token'],
- 'COURSE_TITLE': self.course.display_name_with_default_escaped,
'course': self.course,
'init': '',
'fragment': Fragment(),
@@ -459,7 +459,7 @@ class CoursewareIndex(View):
courseware_context['default_tab'] = self.section.default_tab
# section data
- courseware_context['section_title'] = self.section.display_name_with_default_escaped
+ courseware_context['section_title'] = self.section.display_name_with_default
section_context = self._create_section_context(
table_of_contents['previous_of_active_section'],
table_of_contents['next_of_active_section'],
diff --git a/lms/envs/common.py b/lms/envs/common.py
index d607abfbff..92d5c3c0ff 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -1724,7 +1724,7 @@ REQUIRE_ENVIRONMENT = "node"
# but you don't want to include those dependencies in the JS bundle for the page,
# then you need to add the js urls in this list.
REQUIRE_JS_PATH_OVERRIDES = {
- 'js/bookmarks/views/bookmark_button': 'js/bookmarks/views/bookmark_button.js',
+ 'course_bookmarks/js/views/bookmark_button': 'course_bookmarks/js/views/bookmark_button.js',
'js/views/message_banner': 'js/views/message_banner.js',
'moment': 'common/js/vendor/moment-with-locales.js',
'moment-timezone': 'common/js/vendor/moment-timezone-with-data.js',
@@ -2175,6 +2175,7 @@ INSTALLED_APPS = (
'database_fixups',
# Features
+ 'openedx.features.course_bookmarks',
'openedx.features.course_experience',
)
diff --git a/lms/static/course_bookmarks b/lms/static/course_bookmarks
new file mode 120000
index 0000000000..6384a91bea
--- /dev/null
+++ b/lms/static/course_bookmarks
@@ -0,0 +1 @@
+../../openedx/features/course_bookmarks/static/course_bookmarks
\ No newline at end of file
diff --git a/lms/static/js/bookmarks/views/bookmarks_list_button.js b/lms/static/js/bookmarks/views/bookmarks_list_button.js
deleted file mode 100644
index 6fb74e6543..0000000000
--- a/lms/static/js/bookmarks/views/bookmarks_list_button.js
+++ /dev/null
@@ -1,46 +0,0 @@
-(function(define, undefined) {
- 'use strict';
- define(['gettext', 'jquery', 'underscore', 'backbone', 'js/bookmarks/views/bookmarks_list',
- 'js/bookmarks/collections/bookmarks', 'js/views/message_banner'],
- function(gettext, $, _, Backbone, BookmarksListView, BookmarksCollection, MessageBannerView) {
- return Backbone.View.extend({
-
- el: '.courseware-bookmarks-button',
-
- loadingMessageElement: '#loading-message',
- errorMessageElement: '#error-message',
-
- events: {
- 'click .bookmarks-list-button': 'toggleBookmarksListView'
- },
-
- initialize: function() {
- var bookmarksCollection = new BookmarksCollection([],
- {
- course_id: $('.courseware-results').data('courseId'),
- url: $('.courseware-bookmarks-button').data('bookmarksApiUrl')
- }
- );
- this.bookmarksListView = new BookmarksListView(
- {
- collection: bookmarksCollection,
- loadingMessageView: new MessageBannerView({el: $(this.loadingMessageElement)}),
- errorMessageView: new MessageBannerView({el: $(this.errorMessageElement)})
- }
- );
- },
-
- toggleBookmarksListView: function() {
- if (this.bookmarksListView.areBookmarksVisible()) {
- this.bookmarksListView.hideBookmarks();
- this.$('.bookmarks-list-button').attr('aria-pressed', 'false');
- this.$('.bookmarks-list-button').removeClass('is-active').addClass('is-inactive');
- } else {
- this.bookmarksListView.showBookmarks();
- this.$('.bookmarks-list-button').attr('aria-pressed', 'true');
- this.$('.bookmarks-list-button').removeClass('is-inactive').addClass('is-active');
- }
- }
- });
- });
-}).call(this, define || RequireJS.define);
diff --git a/lms/static/js/courseware/courseware_factory.js b/lms/static/js/courseware/courseware_factory.js
index ff0f6dc297..9c69a3acb9 100644
--- a/lms/static/js/courseware/courseware_factory.js
+++ b/lms/static/js/courseware/courseware_factory.js
@@ -3,10 +3,9 @@
define([
'jquery',
- 'logger',
- 'js/bookmarks/views/bookmarks_list_button'
+ 'logger'
],
- function($, Logger, BookmarksListButton) {
+ function($, Logger) {
return function() {
// This function performs all actions common to all courseware.
// 1. adding an event to all link clicks.
@@ -18,9 +17,6 @@
target_url: event.currentTarget.href
});
});
-
- // 2. instantiating this button attaches events to all buttons in the courseware.
- new BookmarksListButton(); // eslint-disable-line no-new
};
}
);
diff --git a/lms/static/js/fixtures/bookmarks/bookmarks.html b/lms/static/js/fixtures/bookmarks/bookmarks.html
deleted file mode 100644
index 5940981f1f..0000000000
--- a/lms/static/js/fixtures/bookmarks/bookmarks.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
- Bookmarks
-
-
-
diff --git a/lms/static/js/spec/courseware/bookmarks_list_view_spec.js b/lms/static/js/spec/courseware/bookmarks_list_view_spec.js
deleted file mode 100644
index 095c96637a..0000000000
--- a/lms/static/js/spec/courseware/bookmarks_list_view_spec.js
+++ /dev/null
@@ -1,311 +0,0 @@
-define(['backbone',
- 'jquery',
- 'underscore',
- 'logger',
- 'URI',
- 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
- 'common/js/spec_helpers/template_helpers',
- 'js/bookmarks/views/bookmarks_list_button',
- 'js/bookmarks/views/bookmarks_list',
- 'js/bookmarks/collections/bookmarks'],
- function(Backbone, $, _, Logger, URI, AjaxHelpers, TemplateHelpers, BookmarksListButtonView, BookmarksListView,
- BookmarksCollection) {
- 'use strict';
-
- describe('lms.courseware.bookmarks', function() {
- var bookmarksButtonView;
-
- beforeEach(function() {
- loadFixtures('js/fixtures/bookmarks/bookmarks.html');
- TemplateHelpers.installTemplates(
- [
- 'templates/fields/message_banner',
- 'templates/bookmarks/bookmarks-list'
- ]
- );
- spyOn(Logger, 'log').and.returnValue($.Deferred().resolve());
- jasmine.addMatchers({
- toHaveBeenCalledWithUrl: function() {
- return {
- compare: function(actual, expectedUrl) {
- return {
- pass: expectedUrl === actual.calls.mostRecent().args[0].currentTarget.pathname
- };
- }
- };
- }
- });
-
- bookmarksButtonView = new BookmarksListButtonView();
- });
-
- var verifyRequestParams = function(requests, params) {
- var urlParams = (new URI(requests[requests.length - 1].url)).query(true);
- _.each(params, function(value, key) {
- expect(urlParams[key]).toBe(value);
- });
- };
-
- var createBookmarksData = function(options) {
- var data = {
- count: options.count || 0,
- num_pages: options.num_pages || 1,
- current_page: options.current_page || 1,
- start: options.start || 0,
- results: []
- };
-
- for (var i = 0; i < options.numBookmarksToCreate; i++) {
- var bookmarkInfo = {
- id: i,
- display_name: 'UNIT_DISPLAY_NAME_' + i,
- created: new Date().toISOString(),
- course_id: 'COURSE_ID',
- usage_id: 'UNIT_USAGE_ID_' + i,
- block_type: 'vertical',
- path: [
- {display_name: 'SECTION_DISAPLAY_NAME', usage_id: 'SECTION_USAGE_ID'},
- {display_name: 'SUBSECTION_DISAPLAY_NAME', usage_id: 'SUBSECTION_USAGE_ID'}
- ]
- };
-
- data.results.push(bookmarkInfo);
- }
-
- return data;
- };
-
- var createBookmarkUrl = function(courseId, usageId) {
- return '/courses/' + courseId + '/jump_to/' + usageId;
- };
-
- var breadcrumbTrail = function(path, unitDisplayName) {
- return _.pluck(path, 'display_name').
- concat([unitDisplayName]).
- join(' - ');
- };
-
- var verifyBookmarkedData = function(view, expectedData) {
- var courseId, usageId;
- var bookmarks = view.$('.bookmarks-results-list-item');
- var results = expectedData.results;
-
- expect(bookmarks.length, results.length);
-
- for (var bookmark_index = 0; bookmark_index < results.length; bookmark_index++) {
- courseId = results[bookmark_index].course_id;
- usageId = results[bookmark_index].usage_id;
-
- expect(bookmarks[bookmark_index]).toHaveAttr('href', createBookmarkUrl(courseId, usageId));
-
- expect($(bookmarks[bookmark_index]).data('bookmarkId')).toBe(bookmark_index);
- expect($(bookmarks[bookmark_index]).data('componentType')).toBe('vertical');
- expect($(bookmarks[bookmark_index]).data('usageId')).toBe(usageId);
-
- expect($(bookmarks[bookmark_index]).find('.list-item-breadcrumbtrail').html().trim()).
- toBe(breadcrumbTrail(results[bookmark_index].path, results[bookmark_index].display_name));
-
- expect($(bookmarks[bookmark_index]).find('.list-item-date').text().trim()).
- toBe('Bookmarked on ' + view.humanFriendlyDate(results[bookmark_index].created));
- }
- };
-
- var verifyPaginationInfo = function(requests, expectedData, currentPage, headerMessage) {
- AjaxHelpers.respondWithJson(requests, expectedData);
- verifyBookmarkedData(bookmarksButtonView.bookmarksListView, expectedData);
- expect(bookmarksButtonView.bookmarksListView.$('.paging-footer span.current-page').text().trim()).
- toBe(currentPage);
- expect(bookmarksButtonView.bookmarksListView.$('.paging-header span').text().trim()).
- toBe(headerMessage);
- };
-
- it('has correct behavior for bookmarks button', function() {
- var requests = AjaxHelpers.requests(this);
-
- spyOn(bookmarksButtonView, 'toggleBookmarksListView').and.callThrough();
-
- bookmarksButtonView.delegateEvents();
-
- expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveAttr('aria-pressed', 'false');
- expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveClass('is-inactive');
-
- bookmarksButtonView.$('.bookmarks-list-button').click();
- expect(bookmarksButtonView.toggleBookmarksListView).toHaveBeenCalled();
- expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveAttr('aria-pressed', 'true');
- expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveClass('is-active');
- AjaxHelpers.respondWithJson(requests, createBookmarksData({numBookmarksToCreate: 1}));
-
- bookmarksButtonView.$('.bookmarks-list-button').click();
- expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveAttr('aria-pressed', 'false');
- expect(bookmarksButtonView.$('.bookmarks-list-button')).toHaveClass('is-inactive');
- });
-
- it('can correctly render an empty bookmarks list', function() {
- var requests = AjaxHelpers.requests(this);
- var expectedData = createBookmarksData({numBookmarksToCreate: 0});
-
- bookmarksButtonView.$('.bookmarks-list-button').click();
- AjaxHelpers.respondWithJson(requests, expectedData);
-
- expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-empty-header').text().trim()).
- toBe('You have not bookmarked any courseware pages yet.');
-
- var emptyListText = 'Use bookmarks to help you easily return to courseware pages. ' +
- 'To bookmark a page, select Bookmark in the upper right corner of that page. ' +
- 'To see a list of all your bookmarks, select Bookmarks in the upper left ' +
- 'corner of any courseware page.';
-
- expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-empty-detail-title').text().trim()).
- toBe(emptyListText);
-
- expect(bookmarksButtonView.bookmarksListView.$('.paging-header').length).toBe(0);
- expect(bookmarksButtonView.bookmarksListView.$('.paging-footer').length).toBe(0);
- });
-
- it('has rendered bookmarked list correctly', function() {
- var requests = AjaxHelpers.requests(this);
- var expectedData = createBookmarksData({numBookmarksToCreate: 3});
-
- bookmarksButtonView.$('.bookmarks-list-button').click();
-
- verifyRequestParams(
- requests,
- {course_id: 'a/b/c', fields: 'display_name,path', page: '1', page_size: '10'}
- );
- AjaxHelpers.respondWithJson(requests, expectedData);
-
- expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-results-header').text().trim()).
- toBe('My Bookmarks');
-
- verifyBookmarkedData(bookmarksButtonView.bookmarksListView, expectedData);
-
- expect(bookmarksButtonView.bookmarksListView.$('.paging-header').length).toBe(1);
- expect(bookmarksButtonView.bookmarksListView.$('.paging-footer').length).toBe(1);
- });
-
- it('calls bookmarks list render on page_changed event', function() {
- var renderSpy = spyOn(BookmarksListView.prototype, 'render');
- var listView = new BookmarksListView({
- collection: new BookmarksCollection([], {
- course_id: 'abc',
- url: '/test-bookmarks/url/'
- })
- });
- listView.collection.trigger('page_changed');
- expect(renderSpy).toHaveBeenCalled();
- });
-
- it('can go to a page number', function() {
- var requests = AjaxHelpers.requests(this);
- var expectedData = createBookmarksData(
- {
- numBookmarksToCreate: 10,
- count: 12,
- num_pages: 2,
- current_page: 1,
- start: 0
- }
- );
-
- bookmarksButtonView.$('.bookmarks-list-button').click();
- AjaxHelpers.respondWithJson(requests, expectedData);
- verifyBookmarkedData(bookmarksButtonView.bookmarksListView, expectedData);
-
- bookmarksButtonView.bookmarksListView.$('input#page-number-input').val('2');
- bookmarksButtonView.bookmarksListView.$('input#page-number-input').trigger('change');
-
- expectedData = createBookmarksData(
- {
- numBookmarksToCreate: 2,
- count: 12,
- num_pages: 2,
- current_page: 2,
- start: 10
- }
- );
- AjaxHelpers.respondWithJson(requests, expectedData);
- verifyBookmarkedData(bookmarksButtonView.bookmarksListView, expectedData);
-
- expect(bookmarksButtonView.bookmarksListView.$('.paging-footer span.current-page').text().trim()).
- toBe('2');
- expect(bookmarksButtonView.bookmarksListView.$('.paging-header span').text().trim()).
- toBe('Showing 11-12 out of 12 total');
- });
-
- it('can navigate forward and backward', function() {
- var requests = AjaxHelpers.requests(this);
- var expectedData = createBookmarksData(
- {
- numBookmarksToCreate: 10,
- count: 15,
- num_pages: 2,
- current_page: 1,
- start: 0
- }
- );
-
- bookmarksButtonView.$('.bookmarks-list-button').click();
- verifyPaginationInfo(requests, expectedData, '1', 'Showing 1-10 out of 15 total');
- verifyRequestParams(
- requests,
- {course_id: 'a/b/c', fields: 'display_name,path', page: '1', page_size: '10'}
- );
-
- bookmarksButtonView.bookmarksListView.$('.paging-footer .next-page-link').click();
- expectedData = createBookmarksData(
- {
- numBookmarksToCreate: 5,
- count: 15,
- num_pages: 2,
- current_page: 2,
- start: 10
- }
- );
- verifyPaginationInfo(requests, expectedData, '2', 'Showing 11-15 out of 15 total');
- verifyRequestParams(
- requests,
- {course_id: 'a/b/c', fields: 'display_name,path', page: '2', page_size: '10'}
- );
-
- expectedData = createBookmarksData(
- {
- numBookmarksToCreate: 10,
- count: 15,
- num_pages: 2,
- current_page: 1,
- start: 0
- }
- );
- bookmarksButtonView.bookmarksListView.$('.paging-footer .previous-page-link').click();
- verifyPaginationInfo(requests, expectedData, '1', 'Showing 1-10 out of 15 total');
- verifyRequestParams(
- requests,
- {course_id: 'a/b/c', fields: 'display_name,path', page: '1', page_size: '10'}
- );
- });
-
- it('can navigate to correct url', function() {
- var requests = AjaxHelpers.requests(this);
- spyOn(bookmarksButtonView.bookmarksListView, 'visitBookmark');
-
- bookmarksButtonView.$('.bookmarks-list-button').click();
- AjaxHelpers.respondWithJson(requests, createBookmarksData({numBookmarksToCreate: 1}));
-
- bookmarksButtonView.bookmarksListView.$('.bookmarks-results-list-item').click();
- var url = bookmarksButtonView.bookmarksListView.$('.bookmarks-results-list-item').attr('href');
- expect(bookmarksButtonView.bookmarksListView.visitBookmark).toHaveBeenCalledWithUrl(url);
- });
-
- it('shows an error message for HTTP 500', function() {
- var requests = AjaxHelpers.requests(this);
-
- bookmarksButtonView.$('.bookmarks-list-button').click();
-
- AjaxHelpers.respondWithError(requests);
-
- expect(bookmarksButtonView.bookmarksListView.$('.bookmarks-results-header').text().trim()).not
- .toBe('My Bookmarks');
- expect($('#error-message').text().trim()).toBe(bookmarksButtonView.bookmarksListView.errorMessage);
- });
- });
- });
diff --git a/lms/static/karma_lms.conf.js b/lms/static/karma_lms.conf.js
index d520316d71..5039c2a204 100644
--- a/lms/static/karma_lms.conf.js
+++ b/lms/static/karma_lms.conf.js
@@ -27,6 +27,7 @@ var options = {
// Otherwise Istanbul which is used for coverage tracking will cause tests to not run.
sourceFiles: [
{pattern: 'coffee/src/**/!(*spec).js'},
+ {pattern: 'course_bookmarks/**/!(*spec).js'},
{pattern: 'course_experience/js/**/!(*spec).js'},
{pattern: 'discussion/js/**/!(*spec).js'},
{pattern: 'js/**/!(*spec|djangojs).js'},
diff --git a/lms/static/lms/js/build.js b/lms/static/lms/js/build.js
index 1a2a536abf..19e49f319a 100644
--- a/lms/static/lms/js/build.js
+++ b/lms/static/lms/js/build.js
@@ -18,6 +18,7 @@
* done.
*/
modules: getModulesList([
+ 'course_bookmarks/js/course_bookmarks_factory',
'course_experience/js/course_outline_factory',
'discussion/js/discussion_board_factory',
'discussion/js/discussion_profile_page_factory',
diff --git a/lms/static/lms/js/spec/main.js b/lms/static/lms/js/spec/main.js
index 30449257f6..7adbc68f88 100644
--- a/lms/static/lms/js/spec/main.js
+++ b/lms/static/lms/js/spec/main.js
@@ -92,12 +92,6 @@
'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory',
'js/student_profile/views/learner_profile_view': 'js/student_profile/views/learner_profile_view',
'js/ccx/schedule': 'js/ccx/schedule',
-
- 'js/bookmarks/collections/bookmarks': 'js/bookmarks/collections/bookmarks',
- 'js/bookmarks/models/bookmark': 'js/bookmarks/models/bookmark',
- 'js/bookmarks/views/bookmarks_list_button': 'js/bookmarks/views/bookmarks_list_button',
- 'js/bookmarks/views/bookmarks_list': 'js/bookmarks/views/bookmarks_list',
- 'js/bookmarks/views/bookmark_button': 'js/bookmarks/views/bookmark_button',
'js/views/message_banner': 'js/views/message_banner',
// edxnotes
@@ -679,6 +673,9 @@
});
testFiles = [
+ 'course_bookmarks/js/spec/bookmark_button_view_spec.js',
+ 'course_bookmarks/js/spec/bookmarks_list_view_spec.js',
+ 'course_bookmarks/js/spec/course_bookmarks_factory_spec.js',
'course_experience/js/spec/course_outline_factory_spec.js',
'discussion/js/spec/discussion_board_factory_spec.js',
'discussion/js/spec/discussion_profile_page_factory_spec.js',
@@ -686,8 +683,6 @@
'discussion/js/spec/views/discussion_user_profile_view_spec.js',
'lms/js/spec/preview/preview_factory_spec.js',
'js/spec/api_admin/catalog_preview_spec.js',
- 'js/spec/courseware/bookmark_button_view_spec.js',
- 'js/spec/courseware/bookmarks_list_view_spec.js',
'js/spec/ccx/schedule_spec.js',
'js/spec/commerce/receipt_view_spec.js',
'js/spec/components/card/card_spec.js',
diff --git a/lms/static/sass/_build-lms-v1.scss b/lms/static/sass/_build-lms-v1.scss
index 4f3709d4ca..49a180ac3b 100644
--- a/lms/static/sass/_build-lms-v1.scss
+++ b/lms/static/sass/_build-lms-v1.scss
@@ -62,10 +62,12 @@
@import 'views/support';
@import 'views/oauth2';
@import "views/financial-assistance";
-@import 'views/bookmarks';
@import 'course/auto-cert';
@import 'views/api-access';
+// features
+@import 'features/bookmarks-v1';
+
// search
@import 'search/search';
diff --git a/lms/static/sass/_build-lms-v2.scss b/lms/static/sass/_build-lms-v2.scss
index 342ead67f7..8521a6f460 100644
--- a/lms/static/sass/_build-lms-v2.scss
+++ b/lms/static/sass/_build-lms-v2.scss
@@ -19,7 +19,10 @@
@import 'shared-v2/modal';
@import 'shared-v2/help-tab';
+// Elements
@import 'notifications';
+@import 'elements-v2/pagination';
-// course outline
-@import 'shared-v2/course-outline';
+// Features
+@import 'features/bookmarks';
+@import 'features/course-outline';
diff --git a/lms/static/sass/elements-v2/_pagination.scss b/lms/static/sass/elements-v2/_pagination.scss
new file mode 100644
index 0000000000..c85b4d83ca
--- /dev/null
+++ b/lms/static/sass/elements-v2/_pagination.scss
@@ -0,0 +1,134 @@
+// Copied from elements/_pagination.scss
+
+.pagination {
+ @include clearfix();
+ display: inline-block;
+ width: flex-grid(3, 12);
+
+ &.pagination-compact {
+ @include text-align(right);
+ }
+
+ &.pagination-full {
+ display: block;
+ width: flex-grid(4, 12);
+ margin: $baseline auto;
+ }
+
+ .nav-item {
+ position: relative;
+ display: inline-block;
+ vertical-align: middle;
+ }
+
+ .nav-link {
+ @include transition(all $tmg-f2 ease-in-out 0s);
+ display: block;
+ border: 0;
+ background-image: none;
+ background-color: transparent;
+ padding: ($baseline/2) ($baseline*0.75);
+
+ &.previous {
+ margin-right: ($baseline/2);
+ }
+
+ &.next {
+ margin-left: ($baseline/2);
+ }
+
+ &:hover {
+ background-color: $lms-active-color;
+ background-image: none;
+ border-radius: 3px;
+ color: $white;
+ }
+
+ &.is-disabled {
+ background-color: transparent;
+ color: $lms-gray;
+ pointer-events: none;
+ }
+ }
+
+ .nav-label {
+ @extend .sr-only;
+ }
+
+ .pagination-form,
+ .current-page,
+ .page-divider,
+ .total-pages {
+ display: inline-block;
+ }
+
+ .current-page,
+ .page-number-input,
+ .total-pages {
+ width: ($baseline*2.5);
+ vertical-align: middle;
+ margin: 0 ($baseline*0.75);
+ padding: ($baseline/4);
+ text-align: center;
+ color: $lms-gray;
+ }
+
+ .current-page {
+ position: absolute;
+ @include left(-($baseline/4));
+ }
+
+ .page-divider {
+ vertical-align: middle;
+ color: $lms-gray;
+ }
+
+ .pagination-form {
+ position: relative;
+ z-index: 100;
+
+ .page-number-label,
+ .submit-pagination-form {
+ @extend .sr-only;
+ }
+
+ .page-number-input {
+ @include transition(all $tmg-f2 ease-in-out 0s);
+ border: 1px solid transparent;
+ border-bottom: 1px dotted $lms-gray;
+ border-radius: 0;
+ box-shadow: none;
+ background: none;
+
+ &:hover {
+ background-color: $white;
+ opacity: 0.6;
+ }
+
+ &:focus {
+ // borrowing the base input focus styles to match overall app
+ @include linear-gradient($yellow-l4, tint($yellow-l4, 90%));
+ opacity: 1.0;
+ box-shadow: 0 0 3px $black inset;
+ background-color: $white;
+ border: 1px solid transparent;
+ border-radius: 3px;
+ }
+ }
+ }
+}
+
+// styles for search/pagination metadata and sorting
+.listing-tools {
+ color: $lms-gray;
+
+ label { // override
+ color: inherit;
+ font-size: inherit;
+ cursor: auto;
+ }
+
+ .listing-sort-select {
+ border: 0;
+ }
+}
diff --git a/lms/static/sass/features/_bookmarks-v1.scss b/lms/static/sass/features/_bookmarks-v1.scss
new file mode 100644
index 0000000000..b5a3bbd21e
--- /dev/null
+++ b/lms/static/sass/features/_bookmarks-v1.scss
@@ -0,0 +1,64 @@
+$bookmark-icon: "\f097"; // .fa-bookmark-o
+$bookmarked-icon: "\f02e"; // .fa-bookmark
+
+// Rules for placing bookmarks and search button side by side
+.wrapper-course-modes {
+ border-bottom: 1px solid $gray-l3;
+ padding: ($baseline/4);
+
+ > div {
+ @include box-sizing(border-box);
+ display: inline-block;
+ }
+}
+
+// Rules for Bookmarks Button
+.courseware-bookmarks-button {
+ width: flex-grid(5);
+ vertical-align: top;
+
+ .bookmarks-list-button {
+ @extend %ui-clear-button;
+
+ // set styles
+ @extend %btn-pl-default-base;
+ @include font-size(13);
+ width: 100%;
+ padding: ($baseline/4) ($baseline/2);
+
+ &:before {
+ content: $bookmarked-icon;
+ font-family: FontAwesome;
+ }
+ }
+}
+
+// Rules for bookmark icon shown on each sequence nav item
+.course-content {
+
+ .bookmark-icon.bookmarked {
+ @include right($baseline / 4);
+ top: -3px;
+ position: absolute;
+ }
+
+ // Rules for bookmark button's different styles
+ .bookmark-button-wrapper {
+ margin-bottom: ($baseline * 1.5);
+ }
+
+ .bookmark-button {
+
+ &:before {
+ content: $bookmark-icon;
+ font-family: FontAwesome;
+ }
+
+ &.bookmarked {
+ &:before {
+ content: $bookmarked-icon;
+ }
+ }
+
+ }
+}
diff --git a/lms/static/sass/features/_bookmarks.scss b/lms/static/sass/features/_bookmarks.scss
new file mode 100644
index 0000000000..d155afabcd
--- /dev/null
+++ b/lms/static/sass/features/_bookmarks.scss
@@ -0,0 +1,87 @@
+$bookmark-icon: "\f097"; // .fa-bookmark-o
+$bookmarked-icon: "\f02e"; // .fa-bookmark
+
+// Rules for Bookmarks Results Header
+.bookmarks-results-header {
+ letter-spacing: 0;
+ text-transform: none;
+ margin-bottom: ($baseline/2);
+}
+
+// Rules for Bookmarks Results
+.bookmarks-results-list {
+ padding-top: ($baseline/2);
+
+ .bookmarks-results-list-item {
+ @include padding(0, $baseline, ($baseline/4), $baseline);
+ display: block;
+ border: 1px solid $lms-border-color;
+ margin-bottom: $baseline;
+
+ &:hover {
+ border-color: palette(primary, base);
+
+ .list-item-breadcrumbtrail {
+ color: palette(primary, base);
+ }
+ }
+ }
+
+ .results-list-item-view {
+ @include float(right);
+ margin-top: $baseline;
+ }
+
+ .list-item-date {
+ margin-top: ($baseline/4);
+ color: $lms-gray;
+ font-size: font-size(small);
+ }
+
+ .bookmarks-results-list-item:before {
+ content: $bookmarked-icon;
+ position: relative;
+ top: -7px;
+ font-family: FontAwesome;
+ color: palette(primary, base);
+ }
+
+ .list-item-content {
+ overflow: hidden;
+ }
+
+ .list-item-left-section {
+ display: inline-block;
+ vertical-align: middle;
+ width: 90%;
+ }
+
+ .list-item-right-section {
+ display: inline-block;
+ vertical-align: middle;
+
+ .fa-arrow-right {
+
+ @include rtl {
+ @include transform(rotate(180deg));
+ }
+ }
+ }
+}
+
+// Rules for empty bookmarks list
+.bookmarks-empty {
+ margin-top: $baseline;
+ border: 1px solid $lms-border-color;
+ padding: $baseline;
+ background-color: $white;
+}
+
+.bookmarks-empty-header {
+ @extend %t-title5;
+ margin-bottom: ($baseline/2);
+}
+
+.bookmarks-empty-detail {
+ @extend %t-copy-sub1;
+}
diff --git a/lms/static/sass/shared-v2/_course-outline.scss b/lms/static/sass/features/_course-outline.scss
similarity index 100%
rename from lms/static/sass/shared-v2/_course-outline.scss
rename to lms/static/sass/features/_course-outline.scss
diff --git a/lms/static/sass/views/_bookmarks.scss b/lms/static/sass/views/_bookmarks.scss
deleted file mode 100644
index b8ed6e5eb4..0000000000
--- a/lms/static/sass/views/_bookmarks.scss
+++ /dev/null
@@ -1,165 +0,0 @@
-$bookmark-icon: "\f097"; // .fa-bookmark-o
-$bookmarked-icon: "\f02e"; // .fa-bookmark
-
-// Rules for placing bookmarks and search button side by side
-.wrapper-course-modes {
- border-bottom: 1px solid $gray-l3;
- padding: ($baseline/4);
-
- > div {
- @include box-sizing(border-box);
- display: inline-block;
- }
-}
-
-
-// Rules for Bookmarks Button
-.courseware-bookmarks-button {
- width: flex-grid(5);
- vertical-align: top;
-
- .bookmarks-list-button {
- @extend %ui-clear-button;
-
- // set styles
- @extend %btn-pl-default-base;
- @include font-size(13);
- width: 100%;
- padding: ($baseline/4) ($baseline/2);
-
- &:before {
- content: $bookmarked-icon;
- font-family: FontAwesome;
- }
-
- &.is-active {
- background-color: lighten($action-primary-bg,10%);
- color: $white;
- }
- }
-}
-
-
-// Rules for Bookmarks Results Header
-.bookmarks-results-header {
- @extend %t-title4;
- letter-spacing: 0;
- text-transform: none;
- margin-bottom: ($baseline/2);
-}
-
-// Rules for Bookmarks Results
-.bookmarks-results-list {
- padding-top: ($baseline/2);
-
- .bookmarks-results-list-item {
- @include padding(0, $baseline, ($baseline/4), $baseline);
- display: block;
- border: 1px solid $gray-l4;
- margin-bottom: $baseline;
-
- &:hover {
- border-color: $m-blue;
-
- .list-item-breadcrumbtrail {
- color: $blue;
- }
- }
-
- .icon {
- @extend %t-icon6;
- }
- }
-
- .results-list-item-view {
- @include float(right);
- margin-top: $baseline;
- }
-
- .list-item-date {
- @extend %t-copy-sub2;
- margin-top: ($baseline/4);
- color: $gray;
- }
-
- .bookmarks-results-list-item:before {
- content: $bookmarked-icon;
- position: relative;
- top: -7px;
- font-family: FontAwesome;
- color: $m-blue;
- }
-
- .list-item-content {
- overflow: hidden;
- }
-
- .list-item-left-section {
- display: inline-block;
- vertical-align: middle;
- width: 90%;
- }
-
- .list-item-right-section {
- display: inline-block;
- vertical-align: middle;
-
- .fa-arrow-right {
-
- @include rtl {
- @include transform(rotate(180deg));
- }
- }
- }
-}
-
-
-// Rules for empty bookmarks list
-.bookmarks-empty {
- margin-top: $baseline;
- border: 1px solid $gray-l4;
- padding: $baseline;
- background-color: $gray-l6;
-}
-
-.bookmarks-empty-header {
- @extend %t-title5;
- margin-bottom: ($baseline/2);
-}
-
-.bookmarks-empty-detail {
- @extend %t-copy-sub1;
-}
-
-
-// Rules for bookmark icon shown on each sequence nav item
-.course-content {
-
- .bookmark-icon.bookmarked {
- @include right($baseline / 4);
- top: -3px;
- position: absolute;
- }
-
-
- // Rules for bookmark button's different styles
- .bookmark-button-wrapper {
- margin-bottom: ($baseline * 1.5);
- }
-
- .bookmark-button {
-
- &:before {
- content: $bookmark-icon;
- font-family: FontAwesome;
- }
-
- &.bookmarked {
- &:before {
- content: $bookmarked-icon;
- }
- }
-
- }
-
-}
diff --git a/lms/templates/bookmark_button.html b/lms/templates/bookmark_button.html
index 6abaecca9f..21fe05420e 100644
--- a/lms/templates/bookmark_button.html
+++ b/lms/templates/bookmark_button.html
@@ -1,10 +1,15 @@
<%page expression_filter="h" args="bookmark_id, is_bookmarked" />
-<%! from django.utils.translation import ugettext as _ %>
+
+<%!
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
+%>
+ data-bookmark-id="${bookmark_id}"
+ data-bookmarks-api-url="${reverse('bookmarks')}">
${_("Bookmarked") if is_bookmarked else _("Bookmark this page")}
diff --git a/lms/templates/bookmarks/bookmarks-list.underscore b/lms/templates/bookmarks/bookmarks-list.underscore
deleted file mode 100644
index bd3ca9ed2f..0000000000
--- a/lms/templates/bookmarks/bookmarks-list.underscore
+++ /dev/null
@@ -1,43 +0,0 @@
-
-
-
-<% if (bookmarksCollection.length) { %>
-
-
-
-
-
-
-
-<% } else {%>
-
-
-
-
-
- <%= gettext("Use bookmarks to help you easily return to courseware pages. To bookmark a page, select Bookmark in the upper right corner of that page. To see a list of all your bookmarks, select Bookmarks in the upper left corner of any courseware page.") %>
-
-
-
-
-<% } %>
diff --git a/lms/templates/courseware/course_navigation.html b/lms/templates/courseware/course_navigation.html
index 6e3b041412..1a683302c4 100644
--- a/lms/templates/courseware/course_navigation.html
+++ b/lms/templates/courseware/course_navigation.html
@@ -20,7 +20,7 @@ if active_page is None and active_page_context is not UNDEFINED:
def selected(is_selected):
return "selected" if is_selected else ""
-show_preview_menu = not disable_preview_menu and staff_access and active_page in ["courseware", "info", "progress"]
+show_preview_menu = not disable_preview_menu and staff_access and active_page in ["course", "courseware", "info", "progress"]
cohorted_user_partition = get_cohorted_user_partition(course)
masquerade_user_name = masquerade.user_name if masquerade else None
masquerade_group_id = masquerade.group_id if masquerade else None
diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html
index 210d695eed..128f9b6ab1 100644
--- a/lms/templates/courseware/courseware.html
+++ b/lms/templates/courseware/courseware.html
@@ -5,12 +5,13 @@
<%!
import waffle
-from django.utils.translation import ugettext as _
from django.conf import settings
+from django.core.urlresolvers import reverse
+from django.utils.translation import ugettext as _
from edxnotes.helpers import is_feature_enabled as is_edxnotes_enabled
-from openedx.core.djangolib.markup import HTML
from openedx.core.djangolib.js_utils import js_escaped_string
+from openedx.core.djangolib.markup import HTML
%>
<%
include_special_exams = settings.FEATURES.get('ENABLE_SPECIAL_EXAMS', False) and (course.enable_proctored_exams or course.enable_timed_exams)
@@ -117,10 +118,10 @@ ${HTML(fragment.foot_html())}