From 91d227f76d5075abe802cdc6823de86628c904c6 Mon Sep 17 00:00:00 2001 From: Andy Armstrong Date: Thu, 9 Mar 2017 18:20:34 -0500 Subject: [PATCH] Convert course bookmarks into a feature LEARNER-39 --- .../public/js/vertical_student_view.js | 4 +- lms/djangoapps/courseware/tabs.py | 16 +- lms/djangoapps/courseware/views/index.py | 4 +- lms/envs/common.py | 3 +- lms/static/course_bookmarks | 1 + .../bookmarks/views/bookmarks_list_button.js | 46 --- .../js/courseware/courseware_factory.js | 8 +- .../js/fixtures/bookmarks/bookmarks.html | 10 - .../courseware/bookmarks_list_view_spec.js | 311 ------------------ lms/static/karma_lms.conf.js | 1 + lms/static/lms/js/build.js | 1 + lms/static/lms/js/spec/main.js | 11 +- lms/static/sass/_build-lms-v1.scss | 4 +- lms/static/sass/_build-lms-v2.scss | 7 +- lms/static/sass/elements-v2/_pagination.scss | 134 ++++++++ lms/static/sass/features/_bookmarks-v1.scss | 64 ++++ lms/static/sass/features/_bookmarks.scss | 87 +++++ .../_course-outline.scss | 0 lms/static/sass/views/_bookmarks.scss | 165 ---------- lms/templates/bookmark_button.html | 9 +- .../bookmarks/bookmarks-list.underscore | 43 --- .../courseware/course_navigation.html | 2 +- lms/templates/courseware/courseware.html | 11 +- lms/urls.py | 8 + openedx/core/djangoapps/plugin_api/views.py | 3 - openedx/features/course_bookmarks/__init__.py | 0 .../fixtures}/bookmark_button.html | 9 +- .../course_bookmarks/fixtures/bookmarks.html | 23 ++ .../js}/collections/bookmarks.js | 4 +- .../js/course_bookmarks_factory.js | 35 ++ .../course_bookmarks/js}/models/bookmark.js | 2 +- .../js/spec}/bookmark_button_view_spec.js | 16 +- .../js/spec/bookmarks_list_view_spec.js | 260 +++++++++++++++ .../js/spec/course_bookmarks_factory_spec.js | 36 ++ .../js/spec_helpers/bookmark_helpers.js | 93 ++++++ .../js}/views/bookmark_button.js | 14 +- .../js}/views/bookmarks_list.js | 56 ++-- .../templates/bookmarks-list.underscore | 54 +++ .../course-bookmarks-fragment.html | 11 + .../course_bookmarks/course-bookmarks.html | 58 ++++ .../course_bookmarks_js.template | 15 + openedx/features/course_bookmarks/urls.py | 20 ++ .../course_bookmarks/views/__init__.py | 0 .../views/course_bookmarks.py | 82 +++++ .../course_experience/course-home.html | 3 + .../course-outline-fragment.html | 2 + 46 files changed, 1079 insertions(+), 667 deletions(-) create mode 120000 lms/static/course_bookmarks delete mode 100644 lms/static/js/bookmarks/views/bookmarks_list_button.js delete mode 100644 lms/static/js/fixtures/bookmarks/bookmarks.html delete mode 100644 lms/static/js/spec/courseware/bookmarks_list_view_spec.js create mode 100644 lms/static/sass/elements-v2/_pagination.scss create mode 100644 lms/static/sass/features/_bookmarks-v1.scss create mode 100644 lms/static/sass/features/_bookmarks.scss rename lms/static/sass/{shared-v2 => features}/_course-outline.scss (100%) delete mode 100644 lms/static/sass/views/_bookmarks.scss delete mode 100644 lms/templates/bookmarks/bookmarks-list.underscore create mode 100644 openedx/features/course_bookmarks/__init__.py rename {lms/static/js/fixtures/bookmarks => openedx/features/course_bookmarks/static/course_bookmarks/fixtures}/bookmark_button.html (52%) create mode 100644 openedx/features/course_bookmarks/static/course_bookmarks/fixtures/bookmarks.html rename {lms/static/js/bookmarks => openedx/features/course_bookmarks/static/course_bookmarks/js}/collections/bookmarks.js (90%) create mode 100644 openedx/features/course_bookmarks/static/course_bookmarks/js/course_bookmarks_factory.js rename {lms/static/js/bookmarks => openedx/features/course_bookmarks/static/course_bookmarks/js}/models/bookmark.js (94%) rename {lms/static/js/spec/courseware => openedx/features/course_bookmarks/static/course_bookmarks/js/spec}/bookmark_button_view_spec.js (92%) create mode 100644 openedx/features/course_bookmarks/static/course_bookmarks/js/spec/bookmarks_list_view_spec.js create mode 100644 openedx/features/course_bookmarks/static/course_bookmarks/js/spec/course_bookmarks_factory_spec.js create mode 100644 openedx/features/course_bookmarks/static/course_bookmarks/js/spec_helpers/bookmark_helpers.js rename {lms/static/js/bookmarks => openedx/features/course_bookmarks/static/course_bookmarks/js}/views/bookmark_button.js (91%) rename {lms/static/js/bookmarks => openedx/features/course_bookmarks/static/course_bookmarks/js}/views/bookmarks_list.js (70%) create mode 100644 openedx/features/course_bookmarks/static/course_bookmarks/templates/bookmarks-list.underscore create mode 100644 openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks-fragment.html create mode 100644 openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks.html create mode 100644 openedx/features/course_bookmarks/templates/course_bookmarks/course_bookmarks_js.template create mode 100644 openedx/features/course_bookmarks/urls.py create mode 100644 openedx/features/course_bookmarks/views/__init__.py create mode 100644 openedx/features/course_bookmarks/views/course_bookmarks.py 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 @@ -
- -
-
-
-
-
-
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 _ +%>
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 @@ -
-

<%= gettext("My Bookmarks") %>

- -<% if (bookmarksCollection.length) { %> - -
- -
- <% bookmarksCollection.each(function(bookmark, index) { %> - -
-
- -

<%= gettext("Bookmarked on") %> <%= humanFriendlyDate(bookmark.get('created')) %>

-
- -

- - -

-
-
- <% }); %> -
- - - -<% } else {%> - -
-
- - <%= gettext("You have not bookmarked any courseware pages yet.") %> -
-
-
- - <%= 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())}
- % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): diff --git a/lms/urls.py b/lms/urls.py index cdcb0b8b34..0dedb36db9 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -614,6 +614,14 @@ urlpatterns += ( ), include('openedx.features.course_experience.urls'), ), + + # Course bookmarks + url( + r'^courses/{}/bookmarks/'.format( + settings.COURSE_ID_PATTERN, + ), + include('openedx.features.course_bookmarks.urls'), + ), ) if settings.FEATURES["ENABLE_TEAMS"]: diff --git a/openedx/core/djangoapps/plugin_api/views.py b/openedx/core/djangoapps/plugin_api/views.py index 668eea6caa..ba0b34796c 100644 --- a/openedx/core/djangoapps/plugin_api/views.py +++ b/openedx/core/djangoapps/plugin_api/views.py @@ -45,21 +45,18 @@ class EdxFragmentView(FragmentView): else: return settings.PIPELINE_JS[group]['source_filenames'] - @abstractmethod def vendor_js_dependencies(self): """ Returns list of the vendor JS files that this view depends on. """ return [] - @abstractmethod def js_dependencies(self): """ Returns list of the JavaScript files that this view depends on. """ return [] - @abstractmethod def css_dependencies(self): """ Returns list of the CSS files that this view depends on. diff --git a/openedx/features/course_bookmarks/__init__.py b/openedx/features/course_bookmarks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/lms/static/js/fixtures/bookmarks/bookmark_button.html b/openedx/features/course_bookmarks/static/course_bookmarks/fixtures/bookmark_button.html similarity index 52% rename from lms/static/js/fixtures/bookmarks/bookmark_button.html rename to openedx/features/course_bookmarks/static/course_bookmarks/fixtures/bookmark_button.html index 1e536fdd14..760f661c30 100644 --- a/lms/static/js/fixtures/bookmarks/bookmark_button.html +++ b/openedx/features/course_bookmarks/static/course_bookmarks/fixtures/bookmark_button.html @@ -1,12 +1,11 @@ -
-
diff --git a/openedx/features/course_bookmarks/static/course_bookmarks/fixtures/bookmarks.html b/openedx/features/course_bookmarks/static/course_bookmarks/fixtures/bookmarks.html new file mode 100644 index 0000000000..fed78bc0d2 --- /dev/null +++ b/openedx/features/course_bookmarks/static/course_bookmarks/fixtures/bookmarks.html @@ -0,0 +1,23 @@ +
+ +
+
+
+
+
+
+
+
diff --git a/lms/static/js/bookmarks/collections/bookmarks.js b/openedx/features/course_bookmarks/static/course_bookmarks/js/collections/bookmarks.js similarity index 90% rename from lms/static/js/bookmarks/collections/bookmarks.js rename to openedx/features/course_bookmarks/static/course_bookmarks/js/collections/bookmarks.js index ffedc9ef2a..65a40988ee 100644 --- a/lms/static/js/bookmarks/collections/bookmarks.js +++ b/openedx/features/course_bookmarks/static/course_bookmarks/js/collections/bookmarks.js @@ -3,7 +3,7 @@ define([ 'backbone', 'edx-ui-toolkit/js/pagination/paging-collection', - 'js/bookmarks/models/bookmark' + 'course_bookmarks/js/models/bookmark' ], function(Backbone, PagingCollection, BookmarkModel) { return PagingCollection.extend({ model: BookmarkModel, @@ -24,5 +24,5 @@ } }); }); -})(define || RequireJS.define); +}(define || RequireJS.define)); diff --git a/openedx/features/course_bookmarks/static/course_bookmarks/js/course_bookmarks_factory.js b/openedx/features/course_bookmarks/static/course_bookmarks/js/course_bookmarks_factory.js new file mode 100644 index 0000000000..f9aab1d77d --- /dev/null +++ b/openedx/features/course_bookmarks/static/course_bookmarks/js/course_bookmarks_factory.js @@ -0,0 +1,35 @@ +(function(define) { + 'use strict'; + + define( + [ + 'jquery', + 'js/views/message_banner', + 'course_bookmarks/js/collections/bookmarks', + 'course_bookmarks/js/views/bookmarks_list' + ], + function($, MessageBannerView, BookmarksCollection, BookmarksListView) { + return function(options) { + var courseId = options.courseId, + bookmarksApiUrl = options.bookmarksApiUrl, + bookmarksCollection = new BookmarksCollection([], + { + course_id: courseId, + url: bookmarksApiUrl + } + ); + var bookmarksView = new BookmarksListView( + { + $el: options.$el, + collection: bookmarksCollection, + loadingMessageView: new MessageBannerView({el: $('#loading-message')}), + errorMessageView: new MessageBannerView({el: $('#error-message')}) + } + ); + bookmarksView.render(); + bookmarksView.showBookmarks(); + return bookmarksView; + }; + } + ); +}).call(this, define || RequireJS.define); diff --git a/lms/static/js/bookmarks/models/bookmark.js b/openedx/features/course_bookmarks/static/course_bookmarks/js/models/bookmark.js similarity index 94% rename from lms/static/js/bookmarks/models/bookmark.js rename to openedx/features/course_bookmarks/static/course_bookmarks/js/models/bookmark.js index 3f0e7c6e17..25a8eeeeb1 100644 --- a/lms/static/js/bookmarks/models/bookmark.js +++ b/openedx/features/course_bookmarks/static/course_bookmarks/js/models/bookmark.js @@ -16,4 +16,4 @@ } }); }); -})(define || RequireJS.define); +}(define || RequireJS.define)); diff --git a/lms/static/js/spec/courseware/bookmark_button_view_spec.js b/openedx/features/course_bookmarks/static/course_bookmarks/js/spec/bookmark_button_view_spec.js similarity index 92% rename from lms/static/js/spec/courseware/bookmark_button_view_spec.js rename to openedx/features/course_bookmarks/static/course_bookmarks/js/spec/bookmark_button_view_spec.js index 179b157010..0a6335321f 100644 --- a/lms/static/js/spec/courseware/bookmark_button_view_spec.js +++ b/openedx/features/course_bookmarks/static/course_bookmarks/js/spec/bookmark_button_view_spec.js @@ -1,23 +1,23 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', - 'common/js/spec_helpers/template_helpers', 'js/bookmarks/views/bookmark_button' + 'common/js/spec_helpers/template_helpers', 'course_bookmarks/js/views/bookmark_button' ], function(Backbone, $, _, AjaxHelpers, TemplateHelpers, BookmarkButtonView) { 'use strict'; - describe('bookmarks.button', function() { - var timerCallback; + describe('BookmarkButtonView', function() { + var createBookmarkButtonView, verifyBookmarkButtonState; var API_URL = 'bookmarks/api/v1/bookmarks/'; beforeEach(function() { - loadFixtures('js/fixtures/bookmarks/bookmark_button.html'); + loadFixtures('course_bookmarks/fixtures/bookmark_button.html'); TemplateHelpers.installTemplates( [ 'templates/fields/message_banner' ] ); - timerCallback = jasmine.createSpy('timerCallback'); + jasmine.createSpy('timerCallback'); jasmine.clock().install(); }); @@ -25,7 +25,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper jasmine.clock().uninstall(); }); - var createBookmarkButtonView = function(isBookmarked) { + createBookmarkButtonView = function(isBookmarked) { return new BookmarkButtonView({ el: '.bookmark-button', bookmarked: isBookmarked, @@ -35,7 +35,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper }); }; - var verifyBookmarkButtonState = function(view, bookmarked) { + verifyBookmarkButtonState = function(view, bookmarked) { if (bookmarked) { expect(view.$el).toHaveAttr('aria-pressed', 'true'); expect(view.$el).toHaveClass('bookmarked'); @@ -46,7 +46,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper expect(view.$el.data('bookmarkId')).toBe('bilbo,usage_1'); }; - it('rendered correctly ', function() { + it('rendered correctly', function() { var view = createBookmarkButtonView(false); verifyBookmarkButtonState(view, false); diff --git a/openedx/features/course_bookmarks/static/course_bookmarks/js/spec/bookmarks_list_view_spec.js b/openedx/features/course_bookmarks/static/course_bookmarks/js/spec/bookmarks_list_view_spec.js new file mode 100644 index 0000000000..bf5f0b4b7f --- /dev/null +++ b/openedx/features/course_bookmarks/static/course_bookmarks/js/spec/bookmarks_list_view_spec.js @@ -0,0 +1,260 @@ +define(['backbone', + 'jquery', + 'underscore', + 'logger', + 'URI', + 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', + 'common/js/spec_helpers/template_helpers', + 'js/views/message_banner', + 'course_bookmarks/js/spec_helpers/bookmark_helpers', + 'course_bookmarks/js/views/bookmarks_list', + 'course_bookmarks/js/collections/bookmarks'], + function(Backbone, $, _, Logger, URI, AjaxHelpers, TemplateHelpers, MessageBannerView, + BookmarkHelpers, BookmarksListView, BookmarksCollection) { + 'use strict'; + + describe('BookmarksListView', function() { + var createBookmarksView, verifyRequestParams; + + beforeEach(function() { + loadFixtures('course_bookmarks/fixtures/bookmarks.html'); + TemplateHelpers.installTemplates([ + 'templates/fields/message_banner' + ]); + 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 + }; + } + }; + } + }); + }); + + createBookmarksView = function() { + var bookmarksCollection = new BookmarksCollection( + [], + { + course_id: BookmarkHelpers.TEST_COURSE_ID, + url: BookmarkHelpers.TEST_API_URL + } + ); + var bookmarksView = new BookmarksListView({ + $el: $('.course-bookmarks'), + collection: bookmarksCollection, + loadingMessageView: new MessageBannerView({el: $('#loading-message')}), + errorMessageView: new MessageBannerView({el: $('#error-message')}) + }); + return bookmarksView; + }; + + 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); + }); + }; + + it('can correctly render an empty bookmarks list', function() { + var requests = AjaxHelpers.requests(this); + var bookmarksView = createBookmarksView(); + var expectedData = BookmarkHelpers.createBookmarksData({numBookmarksToCreate: 0}); + + bookmarksView.showBookmarks(); + AjaxHelpers.respondWithJson(requests, expectedData); + + expect(bookmarksView.$('.bookmarks-empty-header').text().trim()).toBe( + 'You have not bookmarked any courseware pages yet' + ); + + expect(bookmarksView.$('.bookmarks-empty-detail-title').text().trim()).toBe( + 'Use bookmarks to help you easily return to courseware pages. ' + + 'To bookmark a page, click on "Bookmark this page" beneath the unit title.' + ); + + expect(bookmarksView.$('.paging-header').length).toBe(0); + expect(bookmarksView.$('.paging-footer').length).toBe(0); + }); + + it('has rendered bookmarked list correctly', function() { + var requests = AjaxHelpers.requests(this); + var bookmarksView = createBookmarksView(); + var expectedData = BookmarkHelpers.createBookmarksData({numBookmarksToCreate: 3}); + + bookmarksView.showBookmarks(); + verifyRequestParams( + requests, + { + course_id: BookmarkHelpers.TEST_COURSE_ID, + fields: 'display_name,path', + page: '1', + page_size: '10' + } + ); + AjaxHelpers.respondWithJson(requests, expectedData); + + BookmarkHelpers.verifyBookmarkedData(bookmarksView, expectedData); + + expect(bookmarksView.$('.paging-header').length).toBe(1); + expect(bookmarksView.$('.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 = BookmarkHelpers.createBookmarksData( + { + numBookmarksToCreate: 10, + count: 12, + num_pages: 2, + current_page: 1, + start: 0 + } + ); + var bookmarksView = createBookmarksView(); + bookmarksView.showBookmarks(); + AjaxHelpers.respondWithJson(requests, expectedData); + BookmarkHelpers.verifyBookmarkedData(bookmarksView, expectedData); + + bookmarksView.$('input#page-number-input').val('2'); + bookmarksView.$('input#page-number-input').trigger('change'); + + expectedData = BookmarkHelpers.createBookmarksData( + { + numBookmarksToCreate: 2, + count: 12, + num_pages: 2, + current_page: 2, + start: 10 + } + ); + AjaxHelpers.respondWithJson(requests, expectedData); + BookmarkHelpers.verifyBookmarkedData(bookmarksView, expectedData); + + expect(bookmarksView.$('.paging-footer span.current-page').text().trim()).toBe('2'); + expect(bookmarksView.$('.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 bookmarksView = createBookmarksView(); + var expectedData = BookmarkHelpers.createBookmarksData( + { + numBookmarksToCreate: 10, + count: 15, + num_pages: 2, + current_page: 1, + start: 0 + } + ); + bookmarksView.showBookmarks(); + BookmarkHelpers.verifyPaginationInfo( + requests, + bookmarksView, + expectedData, + '1', + 'Showing 1-10 out of 15 total' + ); + verifyRequestParams( + requests, + { + course_id: BookmarkHelpers.TEST_COURSE_ID, + fields: 'display_name,path', + page: '1', + page_size: '10' + } + ); + + bookmarksView.$('.paging-footer .next-page-link').click(); + expectedData = BookmarkHelpers.createBookmarksData( + { + numBookmarksToCreate: 5, + count: 15, + num_pages: 2, + current_page: 2, + start: 10 + } + ); + BookmarkHelpers.verifyPaginationInfo( + requests, + bookmarksView, + expectedData, + '2', + 'Showing 11-15 out of 15 total' + ); + verifyRequestParams( + requests, + { + course_id: BookmarkHelpers.TEST_COURSE_ID, + fields: 'display_name,path', + page: '2', + page_size: '10' + } + ); + + expectedData = BookmarkHelpers.createBookmarksData( + { + numBookmarksToCreate: 10, + count: 15, + num_pages: 2, + current_page: 1, + start: 0 + } + ); + bookmarksView.$('.paging-footer .previous-page-link').click(); + BookmarkHelpers.verifyPaginationInfo( + requests, + bookmarksView, + expectedData, + '1', + 'Showing 1-10 out of 15 total' + ); + verifyRequestParams( + requests, + { + course_id: BookmarkHelpers.TEST_COURSE_ID, + fields: 'display_name,path', + page: '1', + page_size: '10' + } + ); + }); + + it('can navigate to correct url', function() { + var requests = AjaxHelpers.requests(this); + var bookmarksView = createBookmarksView(); + var url; + spyOn(bookmarksView, 'visitBookmark'); + bookmarksView.showBookmarks(); + AjaxHelpers.respondWithJson(requests, BookmarkHelpers.createBookmarksData({numBookmarksToCreate: 1})); + + bookmarksView.$('.bookmarks-results-list-item').click(); + url = bookmarksView.$('.bookmarks-results-list-item').attr('href'); + expect(bookmarksView.visitBookmark).toHaveBeenCalledWithUrl(url); + }); + + it('shows an error message for HTTP 500', function() { + var requests = AjaxHelpers.requests(this); + var bookmarksView = createBookmarksView(); + bookmarksView.showBookmarks(); + AjaxHelpers.respondWithError(requests); + + expect($('#error-message').text().trim()).toBe(bookmarksView.errorMessage); + }); + }); + }); diff --git a/openedx/features/course_bookmarks/static/course_bookmarks/js/spec/course_bookmarks_factory_spec.js b/openedx/features/course_bookmarks/static/course_bookmarks/js/spec/course_bookmarks_factory_spec.js new file mode 100644 index 0000000000..51e6407910 --- /dev/null +++ b/openedx/features/course_bookmarks/static/course_bookmarks/js/spec/course_bookmarks_factory_spec.js @@ -0,0 +1,36 @@ +define(['jquery', + 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', + 'course_bookmarks/js/spec_helpers/bookmark_helpers', + 'course_bookmarks/js/course_bookmarks_factory' + ], + function($, AjaxHelpers, BookmarkHelpers, CourseBookmarksFactory) { + 'use strict'; + + describe('CourseBookmarksFactory', function() { + beforeEach(function() { + loadFixtures('course_bookmarks/fixtures/bookmarks.html'); + }); + + it('can render the initial bookmarks', function() { + var requests = AjaxHelpers.requests(this), + expectedData = BookmarkHelpers.createBookmarksData( + { + numBookmarksToCreate: 10, + count: 15, + num_pages: 2, + current_page: 1, + start: 0 + } + ), + bookmarksView; + bookmarksView = CourseBookmarksFactory({ + $el: $('.course-bookmarks'), + courseId: BookmarkHelpers.TEST_COURSE_ID, + bookmarksApiUrl: BookmarkHelpers.TEST_API_URL + }); + BookmarkHelpers.verifyPaginationInfo( + requests, bookmarksView, expectedData, '1', 'Showing 1-10 out of 15 total' + ); + }); + }); + }); diff --git a/openedx/features/course_bookmarks/static/course_bookmarks/js/spec_helpers/bookmark_helpers.js b/openedx/features/course_bookmarks/static/course_bookmarks/js/spec_helpers/bookmark_helpers.js new file mode 100644 index 0000000000..ff3a301dd9 --- /dev/null +++ b/openedx/features/course_bookmarks/static/course_bookmarks/js/spec_helpers/bookmark_helpers.js @@ -0,0 +1,93 @@ +define( + [ + 'underscore', + 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers' + ], + function(_, AjaxHelpers) { + 'use strict'; + + var TEST_COURSE_ID = 'course-v1:test-course'; + + 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: [] + }, + i, bookmarkInfo; + + for (i = 0; i < options.numBookmarksToCreate; i++) { + 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_DISPLAY_NAME', usage_id: 'SECTION_USAGE_ID'}, + {display_name: 'SUBSECTION_DISPLAY_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; + var i, $bookmark; + + expect(bookmarks.length, results.length); + + for (i = 0; i < results.length; i++) { + $bookmark = $(bookmarks[i]); + courseId = results[i].course_id; + usageId = results[i].usage_id; + + expect(bookmarks[i]).toHaveAttr('href', createBookmarkUrl(courseId, usageId)); + + expect($bookmark.data('bookmarkId')).toBe(i); + expect($bookmark.data('componentType')).toBe('vertical'); + expect($bookmark.data('usageId')).toBe(usageId); + + expect($bookmark.find('.list-item-breadcrumbtrail').html().trim()) + .toBe(breadcrumbTrail(results[i].path, results[i].display_name)); + + expect($bookmark.find('.list-item-date').text().trim()) + .toBe('Bookmarked on ' + view.humanFriendlyDate(results[i].created)); + } + }; + + var verifyPaginationInfo = function(requests, view, expectedData, currentPage, headerMessage) { + AjaxHelpers.respondWithJson(requests, expectedData); + verifyBookmarkedData(view, expectedData); + expect(view.$('.paging-footer span.current-page').text().trim()).toBe(currentPage); + expect(view.$('.paging-header span').text().trim()).toBe(headerMessage); + }; + + return { + TEST_COURSE_ID: TEST_COURSE_ID, + TEST_API_URL: '/bookmarks/api', + createBookmarksData: createBookmarksData, + createBookmarkUrl: createBookmarkUrl, + verifyBookmarkedData: verifyBookmarkedData, + verifyPaginationInfo: verifyPaginationInfo + }; + }); diff --git a/lms/static/js/bookmarks/views/bookmark_button.js b/openedx/features/course_bookmarks/static/course_bookmarks/js/views/bookmark_button.js similarity index 91% rename from lms/static/js/bookmarks/views/bookmark_button.js rename to openedx/features/course_bookmarks/static/course_bookmarks/js/views/bookmark_button.js index 5c85ffdb12..d64ac41af5 100644 --- a/lms/static/js/bookmarks/views/bookmark_button.js +++ b/openedx/features/course_bookmarks/static/course_bookmarks/js/views/bookmark_button.js @@ -1,4 +1,4 @@ -(function(define, undefined) { +(function(define) { 'use strict'; define(['gettext', 'jquery', 'underscore', 'backbone', 'js/views/message_banner'], function(gettext, $, _, Backbone, MessageBannerView) { @@ -9,7 +9,7 @@ bookmarkedText: gettext('Bookmarked'), events: { - 'click': 'toggleBookmark' + click: 'toggleBookmark' }, showBannerInterval: 5000, // time in ms @@ -46,14 +46,14 @@ view.setBookmarkState(true); }, error: function(jqXHR) { + var response, userMessage; try { - var response = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : ''; - var userMessage = response ? response.user_message : ''; + response = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : ''; + userMessage = response ? response.user_message : ''; view.showError(userMessage); + } catch (err) { + view.showError(); } - catch (err) { - view.showError(); - } }, complete: function() { view.$el.prop('disabled', false); diff --git a/lms/static/js/bookmarks/views/bookmarks_list.js b/openedx/features/course_bookmarks/static/course_bookmarks/js/views/bookmarks_list.js similarity index 70% rename from lms/static/js/bookmarks/views/bookmarks_list.js rename to openedx/features/course_bookmarks/static/course_bookmarks/js/views/bookmarks_list.js index 18db4360a3..c076ebbf1e 100644 --- a/lms/static/js/bookmarks/views/bookmarks_list.js +++ b/openedx/features/course_bookmarks/static/course_bookmarks/js/views/bookmarks_list.js @@ -1,11 +1,11 @@ -(function(define, undefined) { +(function(define) { 'use strict'; define(['gettext', 'jquery', 'underscore', 'backbone', 'logger', 'moment', 'edx-ui-toolkit/js/utils/html-utils', 'common/js/components/views/paging_header', 'common/js/components/views/paging_footer', - 'text!templates/bookmarks/bookmarks-list.underscore' + 'text!course_bookmarks/templates/bookmarks-list.underscore' ], function(gettext, $, _, Backbone, Logger, _moment, HtmlUtils, - PagingHeaderView, PagingFooterView, BookmarksListTemplate) { + PagingHeaderView, PagingFooterView, bookmarksListTemplate) { var moment = _moment || window.moment; return Backbone.View.extend({ @@ -15,7 +15,7 @@ coursewareResultsWrapperEl: '.courseware-results-wrapper', errorIcon: '', - loadingIcon: '', + loadingIcon: '', // eslint-disable-line max-len errorMessage: gettext('An error has occurred. Please try again.'), loadingMessage: gettext('Loading'), @@ -27,7 +27,7 @@ }, initialize: function(options) { - this.template = HtmlUtils.template(BookmarksListTemplate); + this.template = HtmlUtils.template(bookmarksListTemplate); this.loadingMessageView = options.loadingMessageView; this.errorMessageView = options.errorMessageView; this.langCode = $(this.el).data('langCode'); @@ -65,47 +65,39 @@ }, visitBookmark: function(event) { - var bookmarkedComponent = $(event.currentTarget); - var bookmark_id = bookmarkedComponent.data('bookmarkId'); - var component_usage_id = bookmarkedComponent.data('usageId'); - var component_type = bookmarkedComponent.data('componentType'); + var $bookmarkedComponent = $(event.currentTarget), + bookmarkId = $bookmarkedComponent.data('bookmarkId'), + componentUsageId = $bookmarkedComponent.data('usageId'), + componentType = $bookmarkedComponent.data('componentType'); Logger.log( - 'edx.bookmark.accessed', + 'edx.bookmark.accessed', { - bookmark_id: bookmark_id, - component_type: component_type, - component_usage_id: component_usage_id + bookmark_id: bookmarkId, + component_type: componentType, + component_usage_id: componentUsageId } - ).always(function() { - window.location.href = event.currentTarget.pathname; - }); + ).always(function() { + window.location.href = event.currentTarget.pathname; + }); }, - /** - * Convert ISO 8601 formatted date into human friendly format. e.g, `2014-05-23T14:00:00Z` to `May 23, 2014` - * @param {String} isoDate - ISO 8601 formatted date string. - */ + /** + * Convert ISO 8601 formatted date into human friendly format. + * + * e.g, `2014-05-23T14:00:00Z` to `May 23, 2014` + * + * @param {String} isoDate - ISO 8601 formatted date string. + */ humanFriendlyDate: function(isoDate) { moment.locale(this.langCode); return moment(isoDate).format('LL'); }, - areBookmarksVisible: function() { - return this.$('#my-bookmarks').is(':visible'); - }, - - hideBookmarks: function() { - this.$el.hide(); - $(this.coursewareResultsWrapperEl).hide(); - $(this.coursewareContentEl).css('display', 'table-cell'); - }, - showBookmarksContainer: function() { $(this.coursewareContentEl).hide(); - // Empty el if it's not empty to get the clean state. + // Empty el if it's not empty to get the clean state. this.$el.html(''); this.$el.show(); - $(this.coursewareResultsWrapperEl).css('display', 'table-cell'); }, showLoadingMessage: function() { diff --git a/openedx/features/course_bookmarks/static/course_bookmarks/templates/bookmarks-list.underscore b/openedx/features/course_bookmarks/static/course_bookmarks/templates/bookmarks-list.underscore new file mode 100644 index 0000000000..0169eb713e --- /dev/null +++ b/openedx/features/course_bookmarks/static/course_bookmarks/templates/bookmarks-list.underscore @@ -0,0 +1,54 @@ +
+ +<% if (bookmarksCollection.length) { %> + +
+ + + + + +<% } else {%> + +
+

+ + <%- gettext("You have not bookmarked any courseware pages yet") %> +
+

+
+ + <%- gettext('Use bookmarks to help you easily return to courseware pages. To bookmark a page, click on "Bookmark this page" beneath the unit title.') %> + +
+
+ +<% } %> diff --git a/openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks-fragment.html b/openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks-fragment.html new file mode 100644 index 0000000000..3dcf8fbe33 --- /dev/null +++ b/openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks-fragment.html @@ -0,0 +1,11 @@ +## mako + +<%page expression_filter="h"/> + +<%namespace name='static' file='../static_content.html'/> + +
+
+
+
+
diff --git a/openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks.html b/openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks.html new file mode 100644 index 0000000000..a2620e2a39 --- /dev/null +++ b/openedx/features/course_bookmarks/templates/course_bookmarks/course-bookmarks.html @@ -0,0 +1,58 @@ +## mako + +<%! main_css = "style-main-v2" %> + +<%page expression_filter="h"/> +<%inherit file="../main.html" /> +<%namespace name='static' file='../static_content.html'/> +<%def name="online_help_token()"><% return "courseware" %> +<%def name="course_name()"> +<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %> + + +<%! +import json +from django.utils.translation import ugettext as _ +from django.template.defaultfilters import escapejs + +from django_comment_client.permissions import has_permission +from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string +from openedx.core.djangolib.markup import HTML +%> + +<%block name="bodyclass">course + +<%block name="pagetitle">${course_name()} + +<%include file="../courseware/course_navigation.html" args="active_page='courseware'" /> + +<%block name="head_extra"> +${HTML(outline_fragment.head_html())} + + +<%block name="footer_extra"> +${HTML(outline_fragment.foot_html())} + + +<%block name="content"> +
+ +
+ ${HTML(outline_fragment.body_html())} +
+
+ diff --git a/openedx/features/course_bookmarks/templates/course_bookmarks/course_bookmarks_js.template b/openedx/features/course_bookmarks/templates/course_bookmarks/course_bookmarks_js.template new file mode 100644 index 0000000000..348670d3d0 --- /dev/null +++ b/openedx/features/course_bookmarks/templates/course_bookmarks/course_bookmarks_js.template @@ -0,0 +1,15 @@ +## mako + +<%! +from openedx.core.djangolib.js_utils import dump_js_escaped_json, js_escaped_string +%> + +(function (require, define) { + require(['course_bookmarks/js/course_bookmarks_factory'], function (CourseBookmarksFactory) { + CourseBookmarksFactory({ + $el: $(".course-bookmarks"), + courseId: '${unicode(course.id) | n, js_escaped_string}', + bookmarksApiUrl: '${bookmarks_api_url | n, js_escaped_string}', + }); + }); +}).call(this, require || RequireJS.require, define || RequireJS.define); diff --git a/openedx/features/course_bookmarks/urls.py b/openedx/features/course_bookmarks/urls.py new file mode 100644 index 0000000000..789579e27d --- /dev/null +++ b/openedx/features/course_bookmarks/urls.py @@ -0,0 +1,20 @@ +""" +Defines URLs for the course experience. +""" + +from django.conf.urls import url + +from views.course_bookmarks import CourseBookmarksView, CourseBookmarksFragmentView + +urlpatterns = [ + url( + r'^$', + CourseBookmarksView.as_view(), + name='openedx.course_bookmarks.home', + ), + url( + r'^bookmarks_fragment$', + CourseBookmarksFragmentView.as_view(), + name='openedx.course_bookmarks.course_bookmarks_fragment_view', + ), +] diff --git a/openedx/features/course_bookmarks/views/__init__.py b/openedx/features/course_bookmarks/views/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openedx/features/course_bookmarks/views/course_bookmarks.py b/openedx/features/course_bookmarks/views/course_bookmarks.py new file mode 100644 index 0000000000..116ce6cc26 --- /dev/null +++ b/openedx/features/course_bookmarks/views/course_bookmarks.py @@ -0,0 +1,82 @@ +""" +Views to show a course's bookmarks. +""" + +from django.contrib.auth.decorators import login_required +from django.core.context_processors import csrf +from django.core.urlresolvers import reverse +from django.shortcuts import render_to_response +from django.template.loader import render_to_string +from django.utils.decorators import method_decorator +from django.views.decorators.cache import cache_control +from django.views.decorators.csrf import ensure_csrf_cookie +from django.views.generic import View + +from courseware.courses import get_course_with_access +from lms.djangoapps.courseware.tabs import CoursewareTab +from opaque_keys.edx.keys import CourseKey +from openedx.core.djangoapps.plugin_api.views import EdxFragmentView +from util.views import ensure_valid_course_key +from web_fragments.fragment import Fragment +from xmodule.modulestore.django import modulestore + + +class CourseBookmarksView(View): + """ + The home page for a course. + """ + @method_decorator(login_required) + @method_decorator(ensure_csrf_cookie) + @method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True)) + @method_decorator(ensure_valid_course_key) + def get(self, request, course_id): + """ + Displays the home page for the specified course. + + Arguments: + request: HTTP request + course_id (unicode): course id + """ + course_key = CourseKey.from_string(course_id) + course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) + course_url_name = CoursewareTab.main_course_url_name(request) + course_url = reverse(course_url_name, kwargs={'course_id': unicode(course.id)}) + + # Render the bookmarks list as a fragment + outline_fragment = CourseBookmarksFragmentView().render_to_fragment(request, course_id=course_id) + + # Render the entire unified course view + context = { + 'csrf': csrf(request)['csrf_token'], + 'course': course, + 'course_url': course_url, + 'outline_fragment': outline_fragment, + 'disable_courseware_js': True, + 'uses_pattern_library': True, + } + return render_to_response('course_bookmarks/course-bookmarks.html', context) + + +class CourseBookmarksFragmentView(EdxFragmentView): + """ + Fragment view that shows a user's bookmarks for a course. + """ + def render_to_fragment(self, request, course_id=None, **kwargs): + """ + Renders the course outline as a fragment. + """ + course_key = CourseKey.from_string(course_id) + course = get_course_with_access(request.user, 'load', course_key, check_if_enrolled=True) + + context = { + 'csrf': csrf(request)['csrf_token'], + 'course': course, + 'bookmarks_api_url': reverse('bookmarks'), + 'language_preference': 'en', # TODO: + } + html = render_to_string('course_bookmarks/course-bookmarks-fragment.html', context) + inline_js = render_to_string('course_bookmarks/course_bookmarks_js.template', context) + fragment = Fragment(html) + self.add_fragment_resource_urls(fragment) + fragment.add_javascript(inline_js) + return fragment diff --git a/openedx/features/course_experience/templates/course_experience/course-home.html b/openedx/features/course_experience/templates/course_experience/course-home.html index 8744adea38..60f0425d06 100644 --- a/openedx/features/course_experience/templates/course_experience/course-home.html +++ b/openedx/features/course_experience/templates/course_experience/course-home.html @@ -43,6 +43,9 @@ ${HTML(outline_fragment.foot_html())} ${_("Resume Course")} + + ${_("Bookmarks")} +