Convert course bookmarks into a feature
LEARNER-39
This commit is contained in:
committed by
Diana Huang
parent
652ad7ae73
commit
91d227f76d
@@ -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')
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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',
|
||||
)
|
||||
|
||||
|
||||
1
lms/static/course_bookmarks
Symbolic link
1
lms/static/course_bookmarks
Symbolic link
@@ -0,0 +1 @@
|
||||
../../openedx/features/course_bookmarks/static/course_bookmarks
|
||||
@@ -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);
|
||||
@@ -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
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
<div class="courseware-bookmarks-button" data-bookmarks-api-url="/api/bookmarks/v1/bookmarks/">
|
||||
<button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false">
|
||||
Bookmarks
|
||||
</button>
|
||||
</div>
|
||||
<section class="courseware-results-wrapper">
|
||||
<div id="loading-message" aria-live="assertive" aria-relevant="all"></div>
|
||||
<div id="error-message" aria-live="polite"></div>
|
||||
<div class="courseware-results" data-course-id="a/b/c" data-lang-code="en"></div>
|
||||
</section>
|
||||
@@ -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(' <span class="icon fa fa-caret-right" aria-hidden="true"></span><span class="sr">-</span> ');
|
||||
};
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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'},
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
134
lms/static/sass/elements-v2/_pagination.scss
Normal file
134
lms/static/sass/elements-v2/_pagination.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
64
lms/static/sass/features/_bookmarks-v1.scss
Normal file
64
lms/static/sass/features/_bookmarks-v1.scss
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
87
lms/static/sass/features/_bookmarks.scss
Normal file
87
lms/static/sass/features/_bookmarks.scss
Normal file
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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 _
|
||||
%>
|
||||
|
||||
<div class="bookmark-button-wrapper">
|
||||
<button class="btn btn-link bookmark-button ${"bookmarked" if is_bookmarked else ""}"
|
||||
aria-pressed="${"true" if is_bookmarked else "false"}"
|
||||
data-bookmark-id="${bookmark_id}">
|
||||
data-bookmark-id="${bookmark_id}"
|
||||
data-bookmarks-api-url="${reverse('bookmarks')}">
|
||||
<span class="bookmark-text">${_("Bookmarked") if is_bookmarked else _("Bookmark this page")}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div>
|
||||
<h2 class="bookmarks-results-header"><%= gettext("My Bookmarks") %></h2>
|
||||
|
||||
<% if (bookmarksCollection.length) { %>
|
||||
|
||||
<div class="paging-header"></div>
|
||||
|
||||
<div class='bookmarks-results-list'>
|
||||
<% bookmarksCollection.each(function(bookmark, index) { %>
|
||||
<a class="bookmarks-results-list-item" href="<%= bookmark.blockUrl() %>" aria-labelledby="bookmark-link-<%= index %>" data-bookmark-id="<%= bookmark.get('id') %>" data-component-type="<%= bookmark.get('block_type') %>" data-usage-id="<%= bookmark.get('usage_id') %>" aria-describedby="bookmark-type-<%= index %> bookmark-date-<%= index %>">
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-left-section">
|
||||
<h3 id="bookmark-link-<%= index %>" class="list-item-breadcrumbtrail"> <%= _.map(_.pluck(bookmark.get('path'), 'display_name'), _.escape).concat([_.escape(bookmark.get('display_name'))]).join(' <span class="icon fa fa-caret-right" aria-hidden="true"></span><span class="sr">-</span> ') %> </h3>
|
||||
<p id="bookmark-date-<%= index %>" class="list-item-date"> <%= gettext("Bookmarked on") %> <%= humanFriendlyDate(bookmark.get('created')) %> </p>
|
||||
</div>
|
||||
|
||||
<p id="bookmark-type-<%= index %>" class="list-item-right-section">
|
||||
<span aria-hidden="true"><%= gettext("View") %></span>
|
||||
<span class="icon fa fa-arrow-right" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<div class="paging-footer"></div>
|
||||
|
||||
<% } else {%>
|
||||
|
||||
<div class="bookmarks-empty">
|
||||
<div class="bookmarks-empty-header">
|
||||
<span class="icon fa fa-bookmark-o bookmarks-empty-header-icon" aria-hidden="true"></span>
|
||||
<%= gettext("You have not bookmarked any courseware pages yet.") %>
|
||||
<br>
|
||||
</div>
|
||||
<div class="bookmarks-empty-detail">
|
||||
<span class="bookmarks-empty-detail-title">
|
||||
<%= 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.") %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
@@ -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
|
||||
|
||||
@@ -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())}
|
||||
|
||||
<div class="wrapper-course-modes">
|
||||
|
||||
<div class="courseware-bookmarks-button" data-bookmarks-api-url="${bookmarks_api_url}">
|
||||
<button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false">
|
||||
<div class="courseware-bookmarks-button">
|
||||
<a class="bookmarks-list-button" href="${reverse('openedx.course_bookmarks.home', args=[course.id])}">
|
||||
${_('Bookmarks')}
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
|
||||
|
||||
@@ -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"]:
|
||||
|
||||
@@ -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.
|
||||
|
||||
0
openedx/features/course_bookmarks/__init__.py
Normal file
0
openedx/features/course_bookmarks/__init__.py
Normal file
@@ -1,12 +1,11 @@
|
||||
|
||||
<div class="message-banner" aria-live="polite"></div>
|
||||
|
||||
<div class="xblock xblock-student_view xblock-student_view-vertical xblock-initialized">
|
||||
<div class="bookmark-button-wrapper">
|
||||
<button class="btn bookmark-button"
|
||||
aria-pressed="false"
|
||||
data-bookmark-id="bilbo,usage_1">
|
||||
<span class="bookmark-text">Bookmark this page</span>
|
||||
<button class="btn bookmark-button"
|
||||
aria-pressed="false"
|
||||
data-bookmark-id="bilbo,usage_1">
|
||||
<span class="bookmark-text">Bookmark this page</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,23 @@
|
||||
<div class="course-view container" id="course-container">
|
||||
<header class="page-header has-secondary">
|
||||
<div class="page-header-main">
|
||||
<nav aria-label="Discussions" class="sr-is-focusable" tabindex="-1">
|
||||
<div class="has-breadcrumbs"><div class="breadcrumbs">
|
||||
<span class="nav-item">
|
||||
<a href="/courses/course-v1:test-course/course/">Course</a>
|
||||
</span>
|
||||
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
|
||||
<span class="nav-item">My Bookmarks</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
<div class="course-bookmarks courseware-results-wrapper" id="main">
|
||||
<div id="loading-message" aria-live="polite" aria-relevant="all"></div>
|
||||
<div id="error-message" aria-live="polite"></div>
|
||||
<div class="courseware-results search-results" data-course-id="course-v1:test-course" data-lang-code="en"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -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));
|
||||
|
||||
@@ -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);
|
||||
@@ -16,4 +16,4 @@
|
||||
}
|
||||
});
|
||||
});
|
||||
})(define || RequireJS.define);
|
||||
}(define || RequireJS.define));
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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(' <span class="icon fa fa-caret-right" aria-hidden="true"></span><span class="sr">-</span> ');
|
||||
};
|
||||
|
||||
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
|
||||
};
|
||||
});
|
||||
@@ -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);
|
||||
@@ -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: '<span class="fa fa-fw fa-exclamation-triangle message-error" aria-hidden="true"></span>',
|
||||
loadingIcon: '<span class="fa fa-fw fa-spinner fa-pulse message-in-progress" aria-hidden="true"></span>',
|
||||
loadingIcon: '<span class="fa fa-fw fa-spinner fa-pulse message-in-progress" aria-hidden="true"></span>', // 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() {
|
||||
@@ -0,0 +1,54 @@
|
||||
<div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div>
|
||||
|
||||
<% if (bookmarksCollection.length) { %>
|
||||
|
||||
<div class="paging-header"></div>
|
||||
|
||||
<div class='bookmarks-results-list'>
|
||||
<% bookmarksCollection.each(function(bookmark, index) { %>
|
||||
<a class="bookmarks-results-list-item"
|
||||
href="<%- bookmark.blockUrl() %>"
|
||||
aria-labelledby="bookmark-link-<%- index %>"
|
||||
data-bookmark-id="<%- bookmark.get('id') %>"
|
||||
data-component-type="<%- bookmark.get('block_type') %>"
|
||||
data-usage-id="<%- bookmark.get('usage_id') %>"
|
||||
aria-describedby="bookmark-type-<%- index %> bookmark-date-<%- index %>">
|
||||
<div class="list-item-content">
|
||||
<div class="list-item-left-section">
|
||||
<h3 id="bookmark-link-<%- index %>" class="list-item-breadcrumbtrail">
|
||||
<%=
|
||||
HtmlUtils.HTML(_.map(_.pluck(bookmark.get('path'), 'display_name'), _.escape)
|
||||
.concat([_.escape(bookmark.get('display_name'))])
|
||||
.join(' <span class="icon fa fa-caret-right" aria-hidden="true"></span><span class="sr">-</span> '))
|
||||
%>
|
||||
</h3>
|
||||
<p id="bookmark-date-<%- index %>" class="list-item-date"> <%- gettext("Bookmarked on") %> <%- humanFriendlyDate(bookmark.get('created')) %> </p>
|
||||
</div>
|
||||
|
||||
<p id="bookmark-type-<%- index %>" class="list-item-right-section">
|
||||
<span aria-hidden="true"><%- gettext("View") %></span>
|
||||
<span class="icon fa fa-arrow-right" aria-hidden="true"></span>
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<% }); %>
|
||||
</div>
|
||||
|
||||
<div class="paging-footer"></div>
|
||||
|
||||
<% } else {%>
|
||||
|
||||
<div class="bookmarks-empty">
|
||||
<h3 class="hd-4 bookmarks-empty-header">
|
||||
<span class="icon fa fa-bookmark-o bookmarks-empty-header-icon" aria-hidden="true"></span>
|
||||
<%- gettext("You have not bookmarked any courseware pages yet") %>
|
||||
<br>
|
||||
</h3>
|
||||
<div class="bookmarks-empty-detail">
|
||||
<span class="bookmarks-empty-detail-title">
|
||||
<%- gettext('Use bookmarks to help you easily return to courseware pages. To bookmark a page, click on "Bookmark this page" beneath the unit title.') %>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
@@ -0,0 +1,11 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<div class="course-bookmarks courseware-results-wrapper" id="main" tabindex="-1">
|
||||
<div id="loading-message" aria-live="polite" aria-relevant="all"></div>
|
||||
<div id="error-message" aria-live="polite"></div>
|
||||
<div class="courseware-results search-results" data-course-id="${course.id}" data-lang-code="${language_preference}"></div>
|
||||
</div>
|
||||
@@ -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>
|
||||
<%def name="course_name()">
|
||||
<% return _("{course_number} Courseware").format(course_number=course.display_number_with_default) %>
|
||||
</%def>
|
||||
|
||||
<%!
|
||||
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>
|
||||
|
||||
<%block name="pagetitle">${course_name()}</%block>
|
||||
|
||||
<%include file="../courseware/course_navigation.html" args="active_page='courseware'" />
|
||||
|
||||
<%block name="head_extra">
|
||||
${HTML(outline_fragment.head_html())}
|
||||
</%block>
|
||||
|
||||
<%block name="footer_extra">
|
||||
${HTML(outline_fragment.foot_html())}
|
||||
</%block>
|
||||
|
||||
<%block name="content">
|
||||
<div class="course-view container" id="course-container">
|
||||
<header class="page-header has-secondary">
|
||||
## Breadcrumb navigation
|
||||
<div class="page-header-main">
|
||||
<nav aria-label="Discussions" class="sr-is-focusable" tabindex="-1">
|
||||
<div class="has-breadcrumbs"><div class="breadcrumbs">
|
||||
<span class="nav-item">
|
||||
<a href="${course_url}">Course</a>
|
||||
</span>
|
||||
<span class="icon fa fa-angle-right" aria-hidden="true"></span>
|
||||
<span class="nav-item">My Bookmarks</span>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<div class="page-content">
|
||||
${HTML(outline_fragment.body_html())}
|
||||
</div>
|
||||
</div>
|
||||
</%block>
|
||||
@@ -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);
|
||||
20
openedx/features/course_bookmarks/urls.py
Normal file
20
openedx/features/course_bookmarks/urls.py
Normal file
@@ -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',
|
||||
),
|
||||
]
|
||||
0
openedx/features/course_bookmarks/views/__init__.py
Normal file
0
openedx/features/course_bookmarks/views/__init__.py
Normal file
82
openedx/features/course_bookmarks/views/course_bookmarks.py
Normal file
82
openedx/features/course_bookmarks/views/course_bookmarks.py
Normal file
@@ -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
|
||||
@@ -43,6 +43,9 @@ ${HTML(outline_fragment.foot_html())}
|
||||
<a class="btn" href="${reverse('courseware', kwargs={'course_id': unicode(course.id.to_deprecated_string())})}">
|
||||
${_("Resume Course")}
|
||||
</a>
|
||||
<a class="btn" href="${reverse('openedx.course_bookmarks.home', args=[course.id])}">
|
||||
${_("Bookmarks")}
|
||||
</a>
|
||||
</div>
|
||||
<div class="page-header-search">
|
||||
<form class="search-form" role="search">
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
## mako
|
||||
|
||||
<%page expression_filter="h"/>
|
||||
|
||||
<%namespace name='static' file='../static_content.html'/>
|
||||
|
||||
<%!
|
||||
|
||||
Reference in New Issue
Block a user