Commit d16059a6 by Andy Armstrong Committed by GitHub

Merge pull request #14669 from edx/andya/new-bookmarks-page

Convert course bookmarks into a separate page
parents 596634eb 9df3779c
/* JavaScript for Vertical Student View. */ /* JavaScript for Vertical Student View. */
window.VerticalStudentView = function(runtime, element) { window.VerticalStudentView = function(runtime, element) {
'use strict'; '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 $element = $(element);
var $bookmarkButtonElement = $element.find('.bookmark-button'); var $bookmarkButtonElement = $element.find('.bookmark-button');
...@@ -10,7 +10,7 @@ window.VerticalStudentView = function(runtime, element) { ...@@ -10,7 +10,7 @@ window.VerticalStudentView = function(runtime, element) {
bookmarkId: $bookmarkButtonElement.data('bookmarkId'), bookmarkId: $bookmarkButtonElement.data('bookmarkId'),
usageId: $element.data('usageId'), usageId: $element.data('usageId'),
bookmarked: $element.parent('#seq_content').data('bookmarked'), bookmarked: $element.parent('#seq_content').data('bookmarked'),
apiUrl: $('.courseware-bookmarks-button').data('bookmarksApiUrl') apiUrl: $bookmarkButtonElement.data('bookmarksApiUrl')
}); });
}); });
}; };
...@@ -10,29 +10,23 @@ class BookmarksPage(CoursePage, PaginatedUIMixin): ...@@ -10,29 +10,23 @@ class BookmarksPage(CoursePage, PaginatedUIMixin):
""" """
Courseware Bookmarks Page. Courseware Bookmarks Page.
""" """
url = None url_path = "bookmarks"
url_path = "courseware/"
BOOKMARKS_BUTTON_SELECTOR = '.bookmarks-list-button' BOOKMARKS_BUTTON_SELECTOR = '.bookmarks-list-button'
BOOKMARKS_ELEMENT_SELECTOR = '#my-bookmarks'
BOOKMARKED_ITEMS_SELECTOR = '.bookmarks-results-list .bookmarks-results-list-item' BOOKMARKED_ITEMS_SELECTOR = '.bookmarks-results-list .bookmarks-results-list-item'
BOOKMARKED_BREADCRUMBS = BOOKMARKED_ITEMS_SELECTOR + ' .list-item-breadcrumbtrail' BOOKMARKED_BREADCRUMBS = BOOKMARKED_ITEMS_SELECTOR + ' .list-item-breadcrumbtrail'
def is_browser_on_page(self): def is_browser_on_page(self):
""" Verify if we are on correct page """ """ Verify if we are on correct page """
return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible return self.q(css=self.BOOKMARKS_ELEMENT_SELECTOR).present
def bookmarks_button_visible(self): def bookmarks_button_visible(self):
""" Check if bookmarks button is visible """ """ Check if bookmarks button is visible """
return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible
def click_bookmarks_button(self, wait_for_results=True):
""" Click on Bookmarks button """
self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).first.click()
if wait_for_results:
EmptyPromise(self.results_present, "Bookmarks results present").fulfill()
def results_present(self): def results_present(self):
""" Check if bookmarks results are present """ """ Check if bookmarks results are present """
return self.q(css='#my-bookmarks').present return self.q(css=self.BOOKMARKS_ELEMENT_SELECTOR).present
def results_header_text(self): def results_header_text(self):
""" Returns the bookmarks results header text """ """ Returns the bookmarks results header text """
......
...@@ -4,6 +4,7 @@ LMS Course Home page object ...@@ -4,6 +4,7 @@ LMS Course Home page object
from bok_choy.page_object import PageObject from bok_choy.page_object import PageObject
from common.test.acceptance.pages.lms.bookmarks import BookmarksPage
from common.test.acceptance.pages.lms.course_page import CoursePage from common.test.acceptance.pages.lms.course_page import CoursePage
from common.test.acceptance.pages.lms.courseware import CoursewarePage from common.test.acceptance.pages.lms.courseware import CoursewarePage
...@@ -25,6 +26,12 @@ class CourseHomePage(CoursePage): ...@@ -25,6 +26,12 @@ class CourseHomePage(CoursePage):
# TODO: TNL-6546: Remove the following # TODO: TNL-6546: Remove the following
self.unified_course_view = False self.unified_course_view = False
def click_bookmarks_button(self):
""" Click on Bookmarks button """
self.q(css='.bookmarks-list-button').first.click()
bookmarks_page = BookmarksPage(self.browser, self.course_id)
bookmarks_page.visit()
class CourseOutlinePage(PageObject): class CourseOutlinePage(PageObject):
""" """
......
...@@ -7,6 +7,7 @@ from bok_choy.promise import EmptyPromise ...@@ -7,6 +7,7 @@ from bok_choy.promise import EmptyPromise
import re import re
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
from common.test.acceptance.pages.lms.bookmarks import BookmarksPage
from common.test.acceptance.pages.lms.course_page import CoursePage from common.test.acceptance.pages.lms.course_page import CoursePage
...@@ -310,6 +311,13 @@ class CoursewarePage(CoursePage): ...@@ -310,6 +311,13 @@ class CoursewarePage(CoursePage):
self.q(css='.bookmark-button').first.click() self.q(css='.bookmark-button').first.click()
EmptyPromise(lambda: self.bookmark_button_state != previous_state, "Bookmark button toggled").fulfill() EmptyPromise(lambda: self.bookmark_button_state != previous_state, "Bookmark button toggled").fulfill()
# TODO: TNL-6546: Remove this helper function
def click_bookmarks_button(self):
""" Click on Bookmarks button """
self.q(css='.bookmarks-list-button').first.click()
bookmarks_page = BookmarksPage(self.browser, self.course_id)
bookmarks_page.visit()
class CoursewareSequentialTabPage(CoursePage): class CoursewareSequentialTabPage(CoursePage):
""" """
......
...@@ -25,6 +25,7 @@ from common.test.acceptance.pages.common.logout import LogoutPage ...@@ -25,6 +25,7 @@ from common.test.acceptance.pages.common.logout import LogoutPage
from common.test.acceptance.pages.lms import BASE_URL from common.test.acceptance.pages.lms import BASE_URL
from common.test.acceptance.pages.lms.account_settings import AccountSettingsPage from common.test.acceptance.pages.lms.account_settings import AccountSettingsPage
from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage from common.test.acceptance.pages.lms.auto_auth import AutoAuthPage
from common.test.acceptance.pages.lms.bookmarks import BookmarksPage
from common.test.acceptance.pages.lms.create_mode import ModeCreationPage from common.test.acceptance.pages.lms.create_mode import ModeCreationPage
from common.test.acceptance.pages.lms.course_home import CourseHomePage from common.test.acceptance.pages.lms.course_home import CourseHomePage
from common.test.acceptance.pages.lms.course_info import CourseInfoPage from common.test.acceptance.pages.lms.course_info import CourseInfoPage
...@@ -853,6 +854,12 @@ class HighLevelTabTest(UniqueCourseTest): ...@@ -853,6 +854,12 @@ class HighLevelTabTest(UniqueCourseTest):
self.course_home_page.outline.go_to_section('Test Section 2', 'Test Subsection 3') self.course_home_page.outline.go_to_section('Test Section 2', 'Test Subsection 3')
self.assertTrue(self.courseware_page.nav.is_on_section('Test Section 2', 'Test Subsection 3')) self.assertTrue(self.courseware_page.nav.is_on_section('Test Section 2', 'Test Subsection 3'))
# Verify that we can navigate to the bookmarks page
self.course_home_page.visit()
self.course_home_page.click_bookmarks_button()
bookmarks_page = BookmarksPage(self.browser, self.course_id)
self.assertTrue(bookmarks_page.is_browser_on_page())
@attr('a11y') @attr('a11y')
def test_course_home_a11y(self): def test_course_home_a11y(self):
self.course_home_page.visit() self.course_home_page.visit()
......
...@@ -5,6 +5,7 @@ End-to-end tests for the LMS. ...@@ -5,6 +5,7 @@ End-to-end tests for the LMS.
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from unittest import skip
import ddt import ddt
from flaky import flaky from flaky import flaky
...@@ -434,6 +435,7 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin): ...@@ -434,6 +435,7 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
course_id=self.course_id, staff=False).visit() course_id=self.course_id, staff=False).visit()
@skip('Disable temporarily to get course bookmarks out')
def test_navigation_buttons(self): def test_navigation_buttons(self):
self.courseware_page.visit() self.courseware_page.visit()
......
...@@ -37,16 +37,24 @@ class CoursewareTab(EnrolledTab): ...@@ -37,16 +37,24 @@ class CoursewareTab(EnrolledTab):
is_movable = False is_movable = False
is_default = 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 @property
def link_func(self): def link_func(self):
""" """
Returns a function that computes the URL for this tab. Returns a function that computes the URL for this tab.
""" """
request = RequestCache.get_current_request() request = RequestCache.get_current_request()
if waffle.flag_is_active(request, 'unified_course_view'): url_name = self.main_course_url_name(request)
return link_reverse_func('edx.course_experience.course_home') return link_reverse_func(url_name)
else:
return link_reverse_func('courseware')
class CourseInfoTab(CourseTab): class CourseInfoTab(CourseTab):
......
...@@ -44,9 +44,9 @@ from student.roles import GlobalStaff ...@@ -44,9 +44,9 @@ from student.roles import GlobalStaff
from survey.utils import must_answer_survey from survey.utils import must_answer_survey
from util.enterprise_helpers import get_enterprise_consent_url from util.enterprise_helpers import get_enterprise_consent_url
from util.views import ensure_valid_course_key from util.views import ensure_valid_course_key
from xblock.fragment import Fragment
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.x_module import STUDENT_VIEW from xmodule.x_module import STUDENT_VIEW
from web_fragments.fragment import Fragment
from ..access import has_access, _adjust_start_date_for_beta_testers from ..access import has_access, _adjust_start_date_for_beta_testers
from ..access_utils import in_preview_mode from ..access_utils import in_preview_mode
...@@ -407,7 +407,6 @@ class CoursewareIndex(View): ...@@ -407,7 +407,6 @@ class CoursewareIndex(View):
request = RequestCache.get_current_request() request = RequestCache.get_current_request()
courseware_context = { courseware_context = {
'csrf': csrf(self.request)['csrf_token'], 'csrf': csrf(self.request)['csrf_token'],
'COURSE_TITLE': self.course.display_name_with_default_escaped,
'course': self.course, 'course': self.course,
'init': '', 'init': '',
'fragment': Fragment(), 'fragment': Fragment(),
...@@ -462,7 +461,7 @@ class CoursewareIndex(View): ...@@ -462,7 +461,7 @@ class CoursewareIndex(View):
courseware_context['default_tab'] = self.section.default_tab courseware_context['default_tab'] = self.section.default_tab
# section data # 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( section_context = self._create_section_context(
table_of_contents['previous_of_active_section'], table_of_contents['previous_of_active_section'],
table_of_contents['next_of_active_section'], table_of_contents['next_of_active_section'],
......
...@@ -1724,7 +1724,7 @@ REQUIRE_ENVIRONMENT = "node" ...@@ -1724,7 +1724,7 @@ REQUIRE_ENVIRONMENT = "node"
# but you don't want to include those dependencies in the JS bundle for the page, # 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. # then you need to add the js urls in this list.
REQUIRE_JS_PATH_OVERRIDES = { 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', 'js/views/message_banner': 'js/views/message_banner.js',
'moment': 'common/js/vendor/moment-with-locales.js', 'moment': 'common/js/vendor/moment-with-locales.js',
'moment-timezone': 'common/js/vendor/moment-timezone-with-data.js', 'moment-timezone': 'common/js/vendor/moment-timezone-with-data.js',
...@@ -2175,6 +2175,7 @@ INSTALLED_APPS = ( ...@@ -2175,6 +2175,7 @@ INSTALLED_APPS = (
'database_fixups', 'database_fixups',
# Features # Features
'openedx.features.course_bookmarks',
'openedx.features.course_experience', 'openedx.features.course_experience',
) )
......
../../openedx/features/course_bookmarks/static/course_bookmarks
\ No newline at end of file
(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 @@ ...@@ -3,10 +3,9 @@
define([ define([
'jquery', 'jquery',
'logger', 'logger'
'js/bookmarks/views/bookmarks_list_button'
], ],
function($, Logger, BookmarksListButton) { function($, Logger) {
return function() { return function() {
// This function performs all actions common to all courseware. // This function performs all actions common to all courseware.
// 1. adding an event to all link clicks. // 1. adding an event to all link clicks.
...@@ -18,9 +17,6 @@ ...@@ -18,9 +17,6 @@
target_url: event.currentTarget.href target_url: event.currentTarget.href
}); });
}); });
// 2. instantiating this button attaches events to all buttons in the courseware.
new BookmarksListButton(); // eslint-disable-line no-new
}; };
} }
); );
......
<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>
...@@ -27,6 +27,7 @@ var options = { ...@@ -27,6 +27,7 @@ var options = {
// Otherwise Istanbul which is used for coverage tracking will cause tests to not run. // Otherwise Istanbul which is used for coverage tracking will cause tests to not run.
sourceFiles: [ sourceFiles: [
{pattern: 'coffee/src/**/!(*spec).js'}, {pattern: 'coffee/src/**/!(*spec).js'},
{pattern: 'course_bookmarks/**/!(*spec).js'},
{pattern: 'course_experience/js/**/!(*spec).js'}, {pattern: 'course_experience/js/**/!(*spec).js'},
{pattern: 'discussion/js/**/!(*spec).js'}, {pattern: 'discussion/js/**/!(*spec).js'},
{pattern: 'js/**/!(*spec|djangojs).js'}, {pattern: 'js/**/!(*spec|djangojs).js'},
......
...@@ -18,6 +18,7 @@ ...@@ -18,6 +18,7 @@
* done. * done.
*/ */
modules: getModulesList([ modules: getModulesList([
'course_bookmarks/js/course_bookmarks_factory',
'course_experience/js/course_outline_factory', 'course_experience/js/course_outline_factory',
'discussion/js/discussion_board_factory', 'discussion/js/discussion_board_factory',
'discussion/js/discussion_profile_page_factory', 'discussion/js/discussion_profile_page_factory',
......
...@@ -92,12 +92,6 @@ ...@@ -92,12 +92,6 @@
'js/student_profile/views/learner_profile_factory': 'js/student_profile/views/learner_profile_factory', '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/student_profile/views/learner_profile_view': 'js/student_profile/views/learner_profile_view',
'js/ccx/schedule': 'js/ccx/schedule', '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', 'js/views/message_banner': 'js/views/message_banner',
// edxnotes // edxnotes
...@@ -679,6 +673,9 @@ ...@@ -679,6 +673,9 @@
}); });
testFiles = [ 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', 'course_experience/js/spec/course_outline_factory_spec.js',
'discussion/js/spec/discussion_board_factory_spec.js', 'discussion/js/spec/discussion_board_factory_spec.js',
'discussion/js/spec/discussion_profile_page_factory_spec.js', 'discussion/js/spec/discussion_profile_page_factory_spec.js',
...@@ -686,8 +683,6 @@ ...@@ -686,8 +683,6 @@
'discussion/js/spec/views/discussion_user_profile_view_spec.js', 'discussion/js/spec/views/discussion_user_profile_view_spec.js',
'lms/js/spec/preview/preview_factory_spec.js', 'lms/js/spec/preview/preview_factory_spec.js',
'js/spec/api_admin/catalog_preview_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/ccx/schedule_spec.js',
'js/spec/commerce/receipt_view_spec.js', 'js/spec/commerce/receipt_view_spec.js',
'js/spec/components/card/card_spec.js', 'js/spec/components/card/card_spec.js',
......
...@@ -62,10 +62,12 @@ ...@@ -62,10 +62,12 @@
@import 'views/support'; @import 'views/support';
@import 'views/oauth2'; @import 'views/oauth2';
@import "views/financial-assistance"; @import "views/financial-assistance";
@import 'views/bookmarks';
@import 'course/auto-cert'; @import 'course/auto-cert';
@import 'views/api-access'; @import 'views/api-access';
// features
@import 'features/bookmarks-v1';
// search // search
@import 'search/search'; @import 'search/search';
......
...@@ -19,7 +19,10 @@ ...@@ -19,7 +19,10 @@
@import 'shared-v2/modal'; @import 'shared-v2/modal';
@import 'shared-v2/help-tab'; @import 'shared-v2/help-tab';
// Elements
@import 'notifications'; @import 'notifications';
@import 'elements-v2/pagination';
// course outline // Features
@import 'shared-v2/course-outline'; @import 'features/bookmarks';
@import 'features/course-outline';
// 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;
}
}
$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;
}
}
}
}
$bookmark-icon: "\f097"; // .fa-bookmark-o $bookmark-icon: "\f097"; // .fa-bookmark-o
$bookmarked-icon: "\f02e"; // .fa-bookmark $bookmarked-icon: "\f02e"; // .fa-bookmark
// Rules for placing bookmarks and search button side by side // Rules for Bookmarks Results Header
.wrapper-course-modes { .bookmarks-results-header {
border-bottom: 1px solid $gray-l3; letter-spacing: 0;
padding: ($baseline/4); text-transform: none;
margin-bottom: ($baseline/2);
> div {
@include box-sizing(border-box);
display: inline-block;
}
} }
// Rules for Bookmarks Results
.bookmarks-results-list {
padding-top: ($baseline/2);
// Rules for Bookmarks Button .bookmarks-results-list-item {
.courseware-bookmarks-button { @include padding(0, $baseline, ($baseline/4), $baseline);
width: flex-grid(5); display: block;
vertical-align: top; border: 1px solid $lms-border-color;
margin-bottom: $baseline;
.bookmarks-list-button {
@extend %ui-clear-button;
// set styles &:hover {
@extend %btn-pl-default-base; border-color: palette(primary, base);
@include font-size(13);
width: 100%;
padding: ($baseline/4) ($baseline/2);
&:before { .list-item-breadcrumbtrail {
content: $bookmarked-icon; color: palette(primary, base);
font-family: FontAwesome; }
}
} }
&.is-active { .results-list-item-view {
background-color: lighten($action-primary-bg,10%); @include float(right);
color: $white; margin-top: $baseline;
} }
}
}
// 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 { .list-item-date {
@include padding(0, $baseline, ($baseline/4), $baseline); margin-top: ($baseline/4);
display: block; color: $lms-gray;
border: 1px solid $gray-l4; font-size: font-size(small);
margin-bottom: $baseline; }
&:hover { .bookmarks-results-list-item:before {
border-color: $m-blue; content: $bookmarked-icon;
position: relative;
top: -7px;
font-family: FontAwesome;
color: palette(primary, base);
}
.list-item-breadcrumbtrail { .list-item-content {
color: $blue; overflow: hidden;
}
} }
.icon { .list-item-left-section {
@extend %t-icon6; display: inline-block;
vertical-align: middle;
width: 90%;
} }
}
.results-list-item-view { .list-item-right-section {
@include float(right); display: inline-block;
margin-top: $baseline; vertical-align: middle;
}
.fa-arrow-right {
.list-item-date {
@extend %t-copy-sub2; @include rtl {
margin-top: ($baseline/4); @include transform(rotate(180deg));
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 // Rules for empty bookmarks list
.bookmarks-empty { .bookmarks-empty {
margin-top: $baseline; margin-top: $baseline;
border: 1px solid $gray-l4; border: 1px solid $lms-border-color;
padding: $baseline; padding: $baseline;
background-color: $gray-l6; background-color: $white;
} }
.bookmarks-empty-header { .bookmarks-empty-header {
@extend %t-title5; @extend %t-title5;
margin-bottom: ($baseline/2); margin-bottom: ($baseline/2);
} }
.bookmarks-empty-detail { .bookmarks-empty-detail {
@extend %t-copy-sub1; @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;
}
}
}
} }
<%page expression_filter="h" args="bookmark_id, is_bookmarked" /> <%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"> <div class="bookmark-button-wrapper">
<button class="btn btn-link bookmark-button ${"bookmarked" if is_bookmarked else ""}" <button class="btn btn-link bookmark-button ${"bookmarked" if is_bookmarked else ""}"
aria-pressed="${"true" if is_bookmarked else "false"}" 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> <span class="bookmark-text">${_("Bookmarked") if is_bookmarked else _("Bookmark this page")}</span>
</button> </button>
</div> </div>
...@@ -5,12 +5,13 @@ ...@@ -5,12 +5,13 @@
<%! <%!
import waffle import waffle
from django.utils.translation import ugettext as _
from django.conf import settings 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 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.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) 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())} ...@@ -117,10 +118,10 @@ ${HTML(fragment.foot_html())}
<div class="wrapper-course-modes"> <div class="wrapper-course-modes">
<div class="courseware-bookmarks-button" data-bookmarks-api-url="${bookmarks_api_url}"> <div class="courseware-bookmarks-button">
<button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false"> <a class="bookmarks-list-button" href="${reverse('openedx.course_bookmarks.home', args=[course.id])}">
${_('Bookmarks')} ${_('Bookmarks')}
</button> </a>
</div> </div>
% if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'): % if settings.FEATURES.get('ENABLE_COURSEWARE_SEARCH'):
......
...@@ -614,6 +614,14 @@ urlpatterns += ( ...@@ -614,6 +614,14 @@ urlpatterns += (
), ),
include('openedx.features.course_experience.urls'), 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"]: if settings.FEATURES["ENABLE_TEAMS"]:
......
...@@ -45,21 +45,18 @@ class EdxFragmentView(FragmentView): ...@@ -45,21 +45,18 @@ class EdxFragmentView(FragmentView):
else: else:
return settings.PIPELINE_JS[group]['source_filenames'] return settings.PIPELINE_JS[group]['source_filenames']
@abstractmethod
def vendor_js_dependencies(self): def vendor_js_dependencies(self):
""" """
Returns list of the vendor JS files that this view depends on. Returns list of the vendor JS files that this view depends on.
""" """
return [] return []
@abstractmethod
def js_dependencies(self): def js_dependencies(self):
""" """
Returns list of the JavaScript files that this view depends on. Returns list of the JavaScript files that this view depends on.
""" """
return [] return []
@abstractmethod
def css_dependencies(self): def css_dependencies(self):
""" """
Returns list of the CSS files that this view depends on. Returns list of the CSS files that this view depends on.
......
<div class="message-banner" aria-live="polite"></div> <div class="message-banner" aria-live="polite"></div>
<div class="xblock xblock-student_view xblock-student_view-vertical xblock-initialized"> <div class="xblock xblock-student_view xblock-student_view-vertical xblock-initialized">
<div class="bookmark-button-wrapper"> <div class="bookmark-button-wrapper">
<button class="btn bookmark-button" <button class="btn bookmark-button"
aria-pressed="false" aria-pressed="false"
data-bookmark-id="bilbo,usage_1"> data-bookmark-id="bilbo,usage_1">
<span class="bookmark-text">Bookmark this page</span> <span class="bookmark-text">Bookmark this page</span>
</button> </button>
</div> </div>
</div> </div>
<div class="course-view container" id="course-container">
<header class="page-header has-secondary">
<div class="page-header-main">
<nav aria-label="My Bookmarks" 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 @@ ...@@ -3,7 +3,7 @@
define([ define([
'backbone', 'backbone',
'edx-ui-toolkit/js/pagination/paging-collection', 'edx-ui-toolkit/js/pagination/paging-collection',
'js/bookmarks/models/bookmark' 'course_bookmarks/js/models/bookmark'
], function(Backbone, PagingCollection, BookmarkModel) { ], function(Backbone, PagingCollection, BookmarkModel) {
return PagingCollection.extend({ return PagingCollection.extend({
model: BookmarkModel, model: BookmarkModel,
...@@ -24,5 +24,5 @@ ...@@ -24,5 +24,5 @@
} }
}); });
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
(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 @@ ...@@ -16,4 +16,4 @@
} }
}); });
}); });
})(define || RequireJS.define); }(define || RequireJS.define));
define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers', define([
'common/js/spec_helpers/template_helpers', 'js/bookmarks/views/bookmark_button' 'backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
], 'common/js/spec_helpers/template_helpers', 'course_bookmarks/js/views/bookmark_button'
],
function(Backbone, $, _, AjaxHelpers, TemplateHelpers, BookmarkButtonView) { function(Backbone, $, _, AjaxHelpers, TemplateHelpers, BookmarkButtonView) {
'use strict'; 'use strict';
describe('bookmarks.button', function() { describe('BookmarkButtonView', function() {
var timerCallback; var createBookmarkButtonView, verifyBookmarkButtonState;
var API_URL = 'bookmarks/api/v1/bookmarks/'; var API_URL = 'bookmarks/api/v1/bookmarks/';
beforeEach(function() { beforeEach(function() {
loadFixtures('js/fixtures/bookmarks/bookmark_button.html'); loadFixtures('course_bookmarks/fixtures/bookmark_button.html');
TemplateHelpers.installTemplates( TemplateHelpers.installTemplates(
[ [
'templates/fields/message_banner' 'templates/fields/message_banner'
] ]
); );
timerCallback = jasmine.createSpy('timerCallback'); jasmine.createSpy('timerCallback');
jasmine.clock().install(); jasmine.clock().install();
}); });
...@@ -25,7 +26,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper ...@@ -25,7 +26,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
jasmine.clock().uninstall(); jasmine.clock().uninstall();
}); });
var createBookmarkButtonView = function(isBookmarked) { createBookmarkButtonView = function(isBookmarked) {
return new BookmarkButtonView({ return new BookmarkButtonView({
el: '.bookmark-button', el: '.bookmark-button',
bookmarked: isBookmarked, bookmarked: isBookmarked,
...@@ -35,7 +36,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper ...@@ -35,7 +36,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
}); });
}; };
var verifyBookmarkButtonState = function(view, bookmarked) { verifyBookmarkButtonState = function(view, bookmarked) {
if (bookmarked) { if (bookmarked) {
expect(view.$el).toHaveAttr('aria-pressed', 'true'); expect(view.$el).toHaveAttr('aria-pressed', 'true');
expect(view.$el).toHaveClass('bookmarked'); expect(view.$el).toHaveClass('bookmarked');
...@@ -46,7 +47,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper ...@@ -46,7 +47,7 @@ define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helper
expect(view.$el.data('bookmarkId')).toBe('bilbo,usage_1'); expect(view.$el.data('bookmarkId')).toBe('bilbo,usage_1');
}; };
it('rendered correctly ', function() { it('rendered correctly', function() {
var view = createBookmarkButtonView(false); var view = createBookmarkButtonView(false);
verifyBookmarkButtonState(view, false); verifyBookmarkButtonState(view, false);
......
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'
);
});
});
});
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
};
});
(function(define, undefined) { (function(define) {
'use strict'; 'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'js/views/message_banner'], define(['gettext', 'jquery', 'underscore', 'backbone', 'js/views/message_banner'],
function(gettext, $, _, Backbone, MessageBannerView) { function(gettext, $, _, Backbone, MessageBannerView) {
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
bookmarkedText: gettext('Bookmarked'), bookmarkedText: gettext('Bookmarked'),
events: { events: {
'click': 'toggleBookmark' click: 'toggleBookmark'
}, },
showBannerInterval: 5000, // time in ms showBannerInterval: 5000, // time in ms
...@@ -46,14 +46,14 @@ ...@@ -46,14 +46,14 @@
view.setBookmarkState(true); view.setBookmarkState(true);
}, },
error: function(jqXHR) { error: function(jqXHR) {
var response, userMessage;
try { try {
var response = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : ''; response = jqXHR.responseText ? JSON.parse(jqXHR.responseText) : '';
var userMessage = response ? response.user_message : ''; userMessage = response ? response.user_message : '';
view.showError(userMessage); view.showError(userMessage);
} catch (err) {
view.showError();
} }
catch (err) {
view.showError();
}
}, },
complete: function() { complete: function() {
view.$el.prop('disabled', false); view.$el.prop('disabled', false);
......
(function(define, undefined) { (function(define) {
'use strict'; 'use strict';
define(['gettext', 'jquery', 'underscore', 'backbone', 'logger', 'moment', 'edx-ui-toolkit/js/utils/html-utils', define([
'common/js/components/views/paging_header', 'common/js/components/views/paging_footer', 'gettext', 'jquery', 'underscore', 'backbone', 'logger', 'moment', 'edx-ui-toolkit/js/utils/html-utils',
'text!templates/bookmarks/bookmarks-list.underscore' 'common/js/components/views/paging_header', 'common/js/components/views/paging_footer',
], 'text!course_bookmarks/templates/bookmarks-list.underscore'
],
function(gettext, $, _, Backbone, Logger, _moment, HtmlUtils, function(gettext, $, _, Backbone, Logger, _moment, HtmlUtils,
PagingHeaderView, PagingFooterView, BookmarksListTemplate) { PagingHeaderView, PagingFooterView, bookmarksListTemplate) {
var moment = _moment || window.moment; var moment = _moment || window.moment;
return Backbone.View.extend({ return Backbone.View.extend({
...@@ -15,7 +16,7 @@ ...@@ -15,7 +16,7 @@
coursewareResultsWrapperEl: '.courseware-results-wrapper', coursewareResultsWrapperEl: '.courseware-results-wrapper',
errorIcon: '<span class="fa fa-fw fa-exclamation-triangle message-error" aria-hidden="true"></span>', 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.'), errorMessage: gettext('An error has occurred. Please try again.'),
loadingMessage: gettext('Loading'), loadingMessage: gettext('Loading'),
...@@ -27,7 +28,7 @@ ...@@ -27,7 +28,7 @@
}, },
initialize: function(options) { initialize: function(options) {
this.template = HtmlUtils.template(BookmarksListTemplate); this.template = HtmlUtils.template(bookmarksListTemplate);
this.loadingMessageView = options.loadingMessageView; this.loadingMessageView = options.loadingMessageView;
this.errorMessageView = options.errorMessageView; this.errorMessageView = options.errorMessageView;
this.langCode = $(this.el).data('langCode'); this.langCode = $(this.el).data('langCode');
...@@ -65,47 +66,39 @@ ...@@ -65,47 +66,39 @@
}, },
visitBookmark: function(event) { visitBookmark: function(event) {
var bookmarkedComponent = $(event.currentTarget); var $bookmarkedComponent = $(event.currentTarget),
var bookmark_id = bookmarkedComponent.data('bookmarkId'); bookmarkId = $bookmarkedComponent.data('bookmarkId'),
var component_usage_id = bookmarkedComponent.data('usageId'); componentUsageId = $bookmarkedComponent.data('usageId'),
var component_type = bookmarkedComponent.data('componentType'); componentType = $bookmarkedComponent.data('componentType');
Logger.log( Logger.log(
'edx.bookmark.accessed', 'edx.bookmark.accessed',
{ {
bookmark_id: bookmark_id, bookmark_id: bookmarkId,
component_type: component_type, component_type: componentType,
component_usage_id: component_usage_id component_usage_id: componentUsageId
} }
).always(function() { ).always(function() {
window.location.href = event.currentTarget.pathname; 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` * Convert ISO 8601 formatted date into human friendly format.
* @param {String} isoDate - ISO 8601 formatted date string. *
*/ * e.g, `2014-05-23T14:00:00Z` to `May 23, 2014`
*
* @param {String} isoDate - ISO 8601 formatted date string.
*/
humanFriendlyDate: function(isoDate) { humanFriendlyDate: function(isoDate) {
moment.locale(this.langCode); moment.locale(this.langCode);
return moment(isoDate).format('LL'); 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() { showBookmarksContainer: function() {
$(this.coursewareContentEl).hide(); $(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.html('');
this.$el.show(); this.$el.show();
$(this.coursewareResultsWrapperEl).css('display', 'table-cell');
}, },
showLoadingMessage: function() { showLoadingMessage: function() {
......
<div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div> <div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div>
<h2 class="bookmarks-results-header"><%= gettext("My Bookmarks") %></h2>
<% if (bookmarksCollection.length) { %> <% if (bookmarksCollection.length) { %>
...@@ -7,15 +6,27 @@ ...@@ -7,15 +6,27 @@
<div class='bookmarks-results-list'> <div class='bookmarks-results-list'>
<% bookmarksCollection.each(function(bookmark, index) { %> <% 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 %>"> <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-content">
<div class="list-item-left-section"> <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> <h3 id="bookmark-link-<%- index %>" class="list-item-breadcrumbtrail">
<p id="bookmark-date-<%= index %>" class="list-item-date"> <%= gettext("Bookmarked on") %> <%= humanFriendlyDate(bookmark.get('created')) %> </p> <%=
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> </div>
<p id="bookmark-type-<%= index %>" class="list-item-right-section"> <p id="bookmark-type-<%- index %>" class="list-item-right-section">
<span aria-hidden="true"><%= gettext("View") %></span> <span aria-hidden="true"><%- gettext("View") %></span>
<span class="icon fa fa-arrow-right" aria-hidden="true"></span> <span class="icon fa fa-arrow-right" aria-hidden="true"></span>
</p> </p>
</div> </div>
...@@ -28,14 +39,14 @@ ...@@ -28,14 +39,14 @@
<% } else {%> <% } else {%>
<div class="bookmarks-empty"> <div class="bookmarks-empty">
<div class="bookmarks-empty-header"> <h3 class="hd-4 bookmarks-empty-header">
<span class="icon fa fa-bookmark-o bookmarks-empty-header-icon" aria-hidden="true"></span> <span class="icon fa fa-bookmark-o bookmarks-empty-header-icon" aria-hidden="true"></span>
<%= gettext("You have not bookmarked any courseware pages yet.") %> <%- gettext("You have not bookmarked any courseware pages yet") %>
<br> <br>
</div> </h3>
<div class="bookmarks-empty-detail"> <div class="bookmarks-empty-detail">
<span class="bookmarks-empty-detail-title"> <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.") %> <%- gettext('Use bookmarks to help you easily return to courseware pages. To bookmark a page, click "Bookmark this page" under the page title.') %>
</span> </span>
</div> </div>
</div> </div>
......
## 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>
## 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(bookmarks_fragment.head_html())}
</%block>
<%block name="footer_extra">
${HTML(bookmarks_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="${_('My Bookmarks')}" 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(bookmarks_fragment.body_html())}
</div>
</div>
</%block>
## 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);
"""
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',
),
]
"""
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):
"""
View showing the user's bookmarks 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 user's bookmarks 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
bookmarks_fragment = CourseBookmarksFragmentView().render_to_fragment(request, course_id=course_id)
# Render the course bookmarks page
context = {
'csrf': csrf(request)['csrf_token'],
'course': course,
'course_url': course_url,
'bookmarks_fragment': bookmarks_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 user's course bookmarks 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
...@@ -40,9 +40,12 @@ ${HTML(outline_fragment.foot_html())} ...@@ -40,9 +40,12 @@ ${HTML(outline_fragment.foot_html())}
<header class="page-header has-secondary"> <header class="page-header has-secondary">
<div class="page-header-secondary"> <div class="page-header-secondary">
<div class="form-actions"> <div class="form-actions">
<a class="btn" href="${reverse('courseware', kwargs={'course_id': unicode(course.id.to_deprecated_string())})}"> <a class="btn action-resume-course" href="${reverse('courseware', kwargs={'course_id': unicode(course.id.to_deprecated_string())})}">
${_("Resume Course")} ${_("Resume Course")}
</a> </a>
<a class="btn action-show-bookmarks" href="${reverse('openedx.course_bookmarks.home', args=[course.id])}">
${_("Bookmarks")}
</a>
</div> </div>
<div class="page-header-search"> <div class="page-header-search">
<form class="search-form" role="search"> <form class="search-form" role="search">
......
## mako ## mako
<%page expression_filter="h"/>
<%namespace name='static' file='../static_content.html'/> <%namespace name='static' file='../static_content.html'/>
<%! <%!
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment