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. */
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')
});
});
};
......@@ -10,29 +10,23 @@ class BookmarksPage(CoursePage, PaginatedUIMixin):
"""
Courseware Bookmarks Page.
"""
url = None
url_path = "courseware/"
url_path = "bookmarks"
BOOKMARKS_BUTTON_SELECTOR = '.bookmarks-list-button'
BOOKMARKS_ELEMENT_SELECTOR = '#my-bookmarks'
BOOKMARKED_ITEMS_SELECTOR = '.bookmarks-results-list .bookmarks-results-list-item'
BOOKMARKED_BREADCRUMBS = BOOKMARKED_ITEMS_SELECTOR + ' .list-item-breadcrumbtrail'
def is_browser_on_page(self):
""" 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):
""" Check if bookmarks button is 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):
""" 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):
""" Returns the bookmarks results header text """
......
......@@ -4,6 +4,7 @@ LMS Course Home page object
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.courseware import CoursewarePage
......@@ -25,6 +26,12 @@ class CourseHomePage(CoursePage):
# TODO: TNL-6546: Remove the following
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):
"""
......
......@@ -7,6 +7,7 @@ from bok_choy.promise import EmptyPromise
import re
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
......@@ -310,6 +311,13 @@ class CoursewarePage(CoursePage):
self.q(css='.bookmark-button').first.click()
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):
"""
......
......@@ -25,6 +25,40 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
USERNAME = "STUDENT"
EMAIL = "student@example.com"
def setUp(self):
super(BookmarksTestMixin, self).setUp()
self.studio_course_outline_page = StudioCourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.bookmarks_page = BookmarksPage(self.browser, self.course_id)
# Get session to be used for bookmarking units
self.session = requests.Session()
params = {'username': self.USERNAME, 'email': self.EMAIL, 'course_id': self.course_id}
response = self.session.get(BASE_URL + "/auto_auth", params=params)
self.assertTrue(response.ok, "Failed to get session")
def setup_test(self, num_chapters=2):
"""
Setup test settings.
Arguments:
num_chapters: number of chapters to create in course
"""
self.create_course_fixture(num_chapters)
# Auto-auth register for the course.
LmsAutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit()
self.courseware_page.visit()
def create_course_fixture(self, num_chapters):
"""
Create course fixture
......@@ -59,50 +93,6 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
actual_events = self.wait_for_events(event_filter={'event_type': event_type}, number_of_matches=1)
self.assert_events_match(event_data, actual_events)
@attr(shard=8)
class BookmarksTest(BookmarksTestMixin):
"""
Tests to verify bookmarks functionality.
"""
def setUp(self):
"""
Initialize test setup.
"""
super(BookmarksTest, self).setUp()
self.studio_course_outline_page = StudioCourseOutlinePage(
self.browser,
self.course_info['org'],
self.course_info['number'],
self.course_info['run']
)
self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_home_page = CourseHomePage(self.browser, self.course_id)
self.bookmarks_page = BookmarksPage(self.browser, self.course_id)
# Get session to be used for bookmarking units
self.session = requests.Session()
params = {'username': self.USERNAME, 'email': self.EMAIL, 'course_id': self.course_id}
response = self.session.get(BASE_URL + "/auto_auth", params=params)
self.assertTrue(response.ok, "Failed to get session")
def _test_setup(self, num_chapters=2):
"""
Setup test settings.
Arguments:
num_chapters: number of chapters to create in course
"""
self.create_course_fixture(num_chapters)
# Auto-auth register for the course.
LmsAutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit()
self.courseware_page.visit()
def _bookmark_unit(self, location):
"""
Bookmark a unit
......@@ -124,7 +114,7 @@ class BookmarksTest(BookmarksTestMixin):
)
self.assertTrue(response.ok, "Failed to bookmark unit")
def _bookmark_units(self, num_units):
def bookmark_units(self, num_units):
"""
Bookmark first `num_units` units
......@@ -135,6 +125,19 @@ class BookmarksTest(BookmarksTestMixin):
for index in range(num_units):
self._bookmark_unit(xblocks[index].locator)
@attr(shard=8)
class BookmarksTest(BookmarksTestMixin):
"""
Tests to verify bookmarks functionality.
"""
def setUp(self):
"""
Initialize test setup.
"""
super(BookmarksTest, self).setUp()
def _breadcrumb(self, num_units, modified_name=None):
"""
Creates breadcrumbs for the first `num_units`
......@@ -187,7 +190,7 @@ class BookmarksTest(BookmarksTestMixin):
self.courseware_page.click_bookmark_unit_button()
self.assertEqual(self.courseware_page.bookmark_icon_visible, bookmark_icon_state)
self.assertEqual(self.courseware_page.bookmark_button_state, bookmark_button_state)
self.bookmarks_page.click_bookmarks_button()
self.bookmarks_page.visit()
self.assertEqual(self.bookmarks_page.count(), bookmarked_count)
def _verify_pagination_info(
......@@ -209,14 +212,6 @@ class BookmarksTest(BookmarksTestMixin):
self.assertEqual(self.bookmarks_page.get_current_page_number(), current_page_number)
self.assertEqual(self.bookmarks_page.get_total_pages, total_pages)
def _navigate_to_bookmarks_list(self):
"""
Navigates and verifies the bookmarks list page.
"""
self.bookmarks_page.click_bookmarks_button()
self.assertTrue(self.bookmarks_page.results_present())
self.assertEqual(self.bookmarks_page.results_header_text(), 'My Bookmarks')
def _verify_breadcrumbs(self, num_units, modified_name=None):
"""
Verifies the breadcrumb trail.
......@@ -265,35 +260,41 @@ class BookmarksTest(BookmarksTestMixin):
Then I click again on the bookmark button
And I should see a unit un-bookmarked
"""
self._test_setup()
self.setup_test()
for index in range(2):
self.course_home_page.visit()
self.course_home_page.outline.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index))
self._toggle_bookmark_and_verify(True, 'bookmarked', 1)
self.bookmarks_page.click_bookmarks_button(False)
self.course_home_page.visit()
self.course_home_page.outline.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index))
self._toggle_bookmark_and_verify(False, '', 0)
# TODO: TNL-6546: Remove this test
def test_courseware_bookmarks_button(self):
"""
Scenario: (Temporarily) test that the courseware's "Bookmarks" button works.
"""
self.setup_test()
self.bookmark_units(2)
self.courseware_page.visit()
self.courseware_page.click_bookmarks_button()
self.assertTrue(self.bookmarks_page.is_browser_on_page())
def test_empty_bookmarks_list(self):
"""
Scenario: An empty bookmarks list is shown if there are no bookmarked units.
Given that I am a registered user
And I visit my courseware page
And I can see the Bookmarks button
When I click on Bookmarks button
And I visit my bookmarks page
Then I should see an empty bookmarks list
And empty bookmarks list content is correct
"""
self._test_setup()
self.assertTrue(self.bookmarks_page.bookmarks_button_visible())
self.bookmarks_page.click_bookmarks_button()
self.assertEqual(self.bookmarks_page.results_header_text(), 'My Bookmarks')
self.assertEqual(self.bookmarks_page.empty_header_text(), 'You have not bookmarked any courseware pages yet.')
empty_list_text = ("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.")
self.setup_test()
self.bookmarks_page.visit()
empty_list_text = (
'Use bookmarks to help you easily return to courseware pages. '
'To bookmark a page, click "Bookmark this page" under the page title.')
self.assertEqual(self.bookmarks_page.empty_list_text(), empty_list_text)
def test_bookmarks_list(self):
......@@ -301,18 +302,16 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: A bookmarks list is shown if there are bookmarked units.
Given that I am a registered user
And I visit my courseware page
And I have bookmarked 2 units
When I click on Bookmarks button
And I visit my bookmarks page
Then I should see a bookmarked list with 2 bookmark links
And breadcrumb trail is correct for a bookmark
When I click on bookmarked link
Then I can navigate to correct bookmarked unit
"""
self._test_setup()
self._bookmark_units(2)
self._navigate_to_bookmarks_list()
self.setup_test()
self.bookmark_units(2)
self.bookmarks_page.visit()
self._verify_breadcrumbs(num_units=2)
self._verify_pagination_info(
......@@ -329,11 +328,10 @@ class BookmarksTest(BookmarksTestMixin):
xblock_usage_ids = [xblock.locator for xblock in xblocks]
# Verify link navigation
for index in range(2):
self.bookmarks_page.visit()
self.bookmarks_page.click_bookmarked_block(index)
self.courseware_page.wait_for_page()
self.assertIn(self.courseware_page.active_usage_id(), xblock_usage_ids)
self.courseware_page.visit().wait_for_page()
self.bookmarks_page.click_bookmarks_button()
def test_bookmark_shows_updated_breadcrumb_after_publish(self):
"""
......@@ -345,16 +343,14 @@ class BookmarksTest(BookmarksTestMixin):
Then I visit unit page in studio
Then I change unit display_name
And I publish the changes
Then I visit my courseware page
And I visit bookmarks list page
Then I visit my bookmarks page
When I see the bookmark
Then I can see the breadcrumb trail
with updated display_name.
Then I can see the breadcrumb trail has the updated display_name.
"""
self._test_setup(num_chapters=1)
self._bookmark_units(num_units=1)
self.setup_test(num_chapters=1)
self.bookmark_units(num_units=1)
self._navigate_to_bookmarks_list()
self.bookmarks_page.visit()
self._verify_breadcrumbs(num_units=1)
LogoutPage(self.browser).visit()
......@@ -371,9 +367,8 @@ class BookmarksTest(BookmarksTestMixin):
LogoutPage(self.browser).visit()
LmsAutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit()
self.courseware_page.visit()
self._navigate_to_bookmarks_list()
self.bookmarks_page.visit()
self._verify_breadcrumbs(num_units=1, modified_name=modified_name)
def test_unreachable_bookmark(self):
......@@ -381,19 +376,18 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: We should get a HTTP 404 for an unreachable bookmark.
Given that I am a registered user
And I visit my courseware page
And I have bookmarked 2 units
Then I delete a bookmarked unit
Then I click on Bookmarks button
And I should see a bookmarked list
When I click on deleted bookmark
And I delete a bookmarked unit
And I visit my bookmarks page
Then I should see a bookmarked list
When I click on the deleted bookmark
Then I should navigated to 404 page
"""
self._test_setup(num_chapters=1)
self._bookmark_units(1)
self.setup_test(num_chapters=1)
self.bookmark_units(1)
self._delete_section(0)
self._navigate_to_bookmarks_list()
self.bookmarks_page.visit()
self._verify_pagination_info(
bookmark_count_on_current_page=1,
......@@ -412,15 +406,14 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: We can't get bookmarks more than default page size.
Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 11 units available
Then I click on Bookmarks button
And I should see a bookmarked list
And bookmark list contains 10 bookmarked items
And I visit my bookmarks page
Then I should see a bookmarked list
And the bookmark list should contain 10 bookmarked items
"""
self._test_setup(11)
self._bookmark_units(11)
self._navigate_to_bookmarks_list()
self.setup_test(11)
self.bookmark_units(11)
self.bookmarks_page.visit()
self._verify_pagination_info(
bookmark_count_on_current_page=10,
......@@ -435,17 +428,15 @@ class BookmarksTest(BookmarksTestMixin):
"""
Scenario: Bookmarks list pagination is working as expected for single page
Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 2 units available
Then I click on Bookmarks button
And I should see a bookmarked list with 2 bookmarked items
And I visit my bookmarks page
Then I should see a bookmarked list with 2 bookmarked items
And I should see paging header and footer with correct data
And previous and next buttons are disabled
"""
self._test_setup(num_chapters=2)
self._bookmark_units(num_units=2)
self.bookmarks_page.click_bookmarks_button()
self.setup_test(num_chapters=2)
self.bookmark_units(num_units=2)
self.bookmarks_page.visit()
self.assertTrue(self.bookmarks_page.results_present())
self._verify_pagination_info(
bookmark_count_on_current_page=2,
......@@ -461,11 +452,10 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Next button is working as expected for bookmarks list pagination
Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 12 units available
And I visit my bookmarks page
Then I click on Bookmarks button
And I should see a bookmarked list of 10 items
Then I should see a bookmarked list of 10 items
And I should see paging header and footer with correct info
Then I click on next page button in footer
......@@ -473,10 +463,10 @@ class BookmarksTest(BookmarksTestMixin):
And I should see a bookmarked list with 2 items
And I should see paging header and footer with correct info
"""
self._test_setup(num_chapters=12)
self._bookmark_units(num_units=12)
self.setup_test(num_chapters=12)
self.bookmark_units(num_units=12)
self.bookmarks_page.click_bookmarks_button()
self.bookmarks_page.visit()
self.assertTrue(self.bookmarks_page.results_present())
self._verify_pagination_info(
......@@ -503,9 +493,8 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Previous button is working as expected for bookmarks list pagination
Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 12 units available
And I click on Bookmarks button
And I visit my bookmarks page
Then I click on next page button in footer
And I should be navigated to second page
......@@ -516,10 +505,10 @@ class BookmarksTest(BookmarksTestMixin):
And I should be navigated to first page
And I should see paging header and footer with correct info
"""
self._test_setup(num_chapters=12)
self._bookmark_units(num_units=12)
self.setup_test(num_chapters=12)
self.bookmark_units(num_units=12)
self.bookmarks_page.click_bookmarks_button()
self.bookmarks_page.visit()
self.assertTrue(self.bookmarks_page.results_present())
self.bookmarks_page.press_next_page_button()
......@@ -547,19 +536,17 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Bookmarks list pagination works as expected for valid page number
Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 12 units available
Then I click on Bookmarks button
And I should see a bookmarked list
And I visit my bookmarks page
Then I should see a bookmarked list
And I should see total page value is 2
Then I enter 2 in the page number input
And I should be navigated to page 2
"""
self._test_setup(num_chapters=11)
self._bookmark_units(num_units=11)
self.setup_test(num_chapters=11)
self.bookmark_units(num_units=11)
self.bookmarks_page.click_bookmarks_button()
self.bookmarks_page.visit()
self.assertTrue(self.bookmarks_page.results_present())
self.assertEqual(self.bookmarks_page.get_total_pages, 2)
......@@ -578,18 +565,17 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Bookmarks list pagination works as expected for invalid page number
Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 11 units available
Then I click on Bookmarks button
And I should see a bookmarked list
And I visit my bookmarks page
Then I should see a bookmarked list
And I should see total page value is 2
Then I enter 3 in the page number input
And I should stay at page 1
"""
self._test_setup(num_chapters=11)
self._bookmark_units(num_units=11)
self.setup_test(num_chapters=11)
self.bookmark_units(num_units=11)
self.bookmarks_page.click_bookmarks_button()
self.bookmarks_page.visit()
self.assertTrue(self.bookmarks_page.results_present())
self.assertEqual(self.bookmarks_page.get_total_pages, 2)
......@@ -613,7 +599,7 @@ class BookmarksTest(BookmarksTestMixin):
When I click on bookmarked unit
Then `edx.course.bookmark.accessed` event is emitted
"""
self._test_setup(num_chapters=1)
self.setup_test(num_chapters=1)
self.reset_event_tracking()
# create expected event data
......@@ -627,8 +613,8 @@ class BookmarksTest(BookmarksTestMixin):
}
}
]
self._bookmark_units(num_units=1)
self.bookmarks_page.click_bookmarks_button()
self.bookmark_units(num_units=1)
self.bookmarks_page.visit()
self._verify_pagination_info(
bookmark_count_on_current_page=1,
......@@ -641,3 +627,18 @@ class BookmarksTest(BookmarksTestMixin):
self.bookmarks_page.click_bookmarked_block(0)
self.verify_event_data('edx.bookmark.accessed', event_data)
@attr('a11y')
class BookmarksA11yTests(BookmarksTestMixin):
"""
Tests for checking the a11y of the bookmarks page.
"""
def test_view_a11y(self):
"""
Verify the basic accessibility of the bookmarks page while paginated.
"""
self.setup_test(num_chapters=11)
self.bookmark_units(num_units=11)
self.bookmarks_page.visit()
self.bookmarks_page.a11y_audit.check_for_accessibility_errors()
......@@ -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.account_settings import AccountSettingsPage
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.course_home import CourseHomePage
from common.test.acceptance.pages.lms.course_info import CourseInfoPage
......@@ -853,6 +854,12 @@ class HighLevelTabTest(UniqueCourseTest):
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'))
# 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')
def test_course_home_a11y(self):
self.course_home_page.visit()
......
......@@ -5,6 +5,7 @@ End-to-end tests for the LMS.
import json
from datetime import datetime, timedelta
from unittest import skip
import ddt
from flaky import flaky
......@@ -434,6 +435,7 @@ class CoursewareMultipleVerticalsTest(UniqueCourseTest, EventsTestMixin):
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL,
course_id=self.course_id, staff=False).visit()
@skip('Disable temporarily to get course bookmarks out')
def test_navigation_buttons(self):
self.courseware_page.visit()
......
......@@ -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):
......
......@@ -44,9 +44,9 @@ from student.roles import GlobalStaff
from survey.utils import must_answer_survey
from util.enterprise_helpers import get_enterprise_consent_url
from util.views import ensure_valid_course_key
from xblock.fragment import Fragment
from xmodule.modulestore.django import modulestore
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_utils import in_preview_mode
......@@ -407,7 +407,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(),
......@@ -462,7 +461,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',
)
......
../../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 @@
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
};
}
);
......
<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>
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';
// 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
$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 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);
// Rules for Bookmarks Button
.courseware-bookmarks-button {
width: flex-grid(5);
vertical-align: top;
.bookmarks-list-button {
@extend %ui-clear-button;
.bookmarks-results-list-item {
@include padding(0, $baseline, ($baseline/4), $baseline);
display: block;
border: 1px solid $lms-border-color;
margin-bottom: $baseline;
// set styles
@extend %btn-pl-default-base;
@include font-size(13);
width: 100%;
padding: ($baseline/4) ($baseline/2);
&:hover {
border-color: palette(primary, base);
&:before {
content: $bookmarked-icon;
font-family: FontAwesome;
.list-item-breadcrumbtrail {
color: palette(primary, base);
}
}
}
&.is-active {
background-color: lighten($action-primary-bg,10%);
color: $white;
.results-list-item-view {
@include float(right);
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 {
@include padding(0, $baseline, ($baseline/4), $baseline);
display: block;
border: 1px solid $gray-l4;
margin-bottom: $baseline;
.list-item-date {
margin-top: ($baseline/4);
color: $lms-gray;
font-size: font-size(small);
}
&:hover {
border-color: $m-blue;
.bookmarks-results-list-item:before {
content: $bookmarked-icon;
position: relative;
top: -7px;
font-family: FontAwesome;
color: palette(primary, base);
}
.list-item-breadcrumbtrail {
color: $blue;
}
.list-item-content {
overflow: hidden;
}
.icon {
@extend %t-icon6;
.list-item-left-section {
display: inline-block;
vertical-align: middle;
width: 90%;
}
}
.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));
}
.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;
margin-top: $baseline;
border: 1px solid $lms-border-color;
padding: $baseline;
background-color: $white;
}
.bookmarks-empty-header {
@extend %t-title5;
margin-bottom: ($baseline/2);
@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;
}
}
}
@extend %t-copy-sub1;
}
<%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>
......@@ -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.
......
<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>
<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 @@
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));
(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);
define(['backbone', 'jquery', 'underscore', 'edx-ui-toolkit/js/utils/spec-helpers/ajax-helpers',
'common/js/spec_helpers/template_helpers', 'js/bookmarks/views/bookmark_button'
],
define([
'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) {
'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 +26,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 +36,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 +47,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);
......
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 "Bookmark this page" under the page 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);
});
});
});
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';
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);
......
(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'
],
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!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 +16,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 +28,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 +66,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() {
......
<div id="my-bookmarks" class="sr-is-focusable" tabindex="-1"></div>
<h2 class="bookmarks-results-header"><%= gettext("My Bookmarks") %></h2>
<% if (bookmarksCollection.length) { %>
......@@ -7,15 +6,27 @@
<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 %>">
<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>
<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>
<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>
......@@ -28,14 +39,14 @@
<% } else {%>
<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>
<%= gettext("You have not bookmarked any courseware pages yet.") %>
<%- gettext("You have not bookmarked any courseware pages yet") %>
<br>
</div>
</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, 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>
</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())}
<header class="page-header has-secondary">
<div class="page-header-secondary">
<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")}
</a>
<a class="btn action-show-bookmarks" href="${reverse('openedx.course_bookmarks.home', args=[course.id])}">
${_("Bookmarks")}
</a>
</div>
<div class="page-header-search">
<form class="search-form" role="search">
......
## mako
<%page expression_filter="h"/>
<%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