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,40 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest): ...@@ -25,6 +25,40 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
USERNAME = "STUDENT" USERNAME = "STUDENT"
EMAIL = "student@example.com" 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): def create_course_fixture(self, num_chapters):
""" """
Create course fixture Create course fixture
...@@ -59,50 +93,6 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest): ...@@ -59,50 +93,6 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
actual_events = self.wait_for_events(event_filter={'event_type': event_type}, number_of_matches=1) actual_events = self.wait_for_events(event_filter={'event_type': event_type}, number_of_matches=1)
self.assert_events_match(event_data, actual_events) 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): def _bookmark_unit(self, location):
""" """
Bookmark a unit Bookmark a unit
...@@ -124,7 +114,7 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -124,7 +114,7 @@ class BookmarksTest(BookmarksTestMixin):
) )
self.assertTrue(response.ok, "Failed to bookmark unit") 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 Bookmark first `num_units` units
...@@ -135,6 +125,19 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -135,6 +125,19 @@ class BookmarksTest(BookmarksTestMixin):
for index in range(num_units): for index in range(num_units):
self._bookmark_unit(xblocks[index].locator) 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): def _breadcrumb(self, num_units, modified_name=None):
""" """
Creates breadcrumbs for the first `num_units` Creates breadcrumbs for the first `num_units`
...@@ -187,7 +190,7 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -187,7 +190,7 @@ class BookmarksTest(BookmarksTestMixin):
self.courseware_page.click_bookmark_unit_button() self.courseware_page.click_bookmark_unit_button()
self.assertEqual(self.courseware_page.bookmark_icon_visible, bookmark_icon_state) self.assertEqual(self.courseware_page.bookmark_icon_visible, bookmark_icon_state)
self.assertEqual(self.courseware_page.bookmark_button_state, bookmark_button_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) self.assertEqual(self.bookmarks_page.count(), bookmarked_count)
def _verify_pagination_info( def _verify_pagination_info(
...@@ -209,14 +212,6 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -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_current_page_number(), current_page_number)
self.assertEqual(self.bookmarks_page.get_total_pages, total_pages) 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): def _verify_breadcrumbs(self, num_units, modified_name=None):
""" """
Verifies the breadcrumb trail. Verifies the breadcrumb trail.
...@@ -265,35 +260,41 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -265,35 +260,41 @@ class BookmarksTest(BookmarksTestMixin):
Then I click again on the bookmark button Then I click again on the bookmark button
And I should see a unit un-bookmarked And I should see a unit un-bookmarked
""" """
self._test_setup() self.setup_test()
for index in range(2): for index in range(2):
self.course_home_page.visit() self.course_home_page.visit()
self.course_home_page.outline.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index)) self.course_home_page.outline.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index))
self._toggle_bookmark_and_verify(True, 'bookmarked', 1) 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) 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): def test_empty_bookmarks_list(self):
""" """
Scenario: An empty bookmarks list is shown if there are no bookmarked units. Scenario: An empty bookmarks list is shown if there are no bookmarked units.
Given that I am a registered user Given that I am a registered user
And I visit my courseware page And I visit my bookmarks page
And I can see the Bookmarks button
When I click on Bookmarks button
Then I should see an empty bookmarks list Then I should see an empty bookmarks list
And empty bookmarks list content is correct And empty bookmarks list content is correct
""" """
self._test_setup() self.setup_test()
self.assertTrue(self.bookmarks_page.bookmarks_button_visible()) self.bookmarks_page.visit()
self.bookmarks_page.click_bookmarks_button() empty_list_text = (
self.assertEqual(self.bookmarks_page.results_header_text(), 'My Bookmarks') 'Use bookmarks to help you easily return to courseware pages. '
self.assertEqual(self.bookmarks_page.empty_header_text(), 'You have not bookmarked any courseware pages yet.') 'To bookmark a page, click "Bookmark this page" under the page title.')
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.assertEqual(self.bookmarks_page.empty_list_text(), empty_list_text) self.assertEqual(self.bookmarks_page.empty_list_text(), empty_list_text)
def test_bookmarks_list(self): def test_bookmarks_list(self):
...@@ -301,18 +302,16 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -301,18 +302,16 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: A bookmarks list is shown if there are bookmarked units. Scenario: A bookmarks list is shown if there are bookmarked units.
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked 2 units 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 Then I should see a bookmarked list with 2 bookmark links
And breadcrumb trail is correct for a bookmark And breadcrumb trail is correct for a bookmark
When I click on bookmarked link When I click on bookmarked link
Then I can navigate to correct bookmarked unit Then I can navigate to correct bookmarked unit
""" """
self._test_setup() self.setup_test()
self._bookmark_units(2) self.bookmark_units(2)
self.bookmarks_page.visit()
self._navigate_to_bookmarks_list()
self._verify_breadcrumbs(num_units=2) self._verify_breadcrumbs(num_units=2)
self._verify_pagination_info( self._verify_pagination_info(
...@@ -329,11 +328,10 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -329,11 +328,10 @@ class BookmarksTest(BookmarksTestMixin):
xblock_usage_ids = [xblock.locator for xblock in xblocks] xblock_usage_ids = [xblock.locator for xblock in xblocks]
# Verify link navigation # Verify link navigation
for index in range(2): for index in range(2):
self.bookmarks_page.visit()
self.bookmarks_page.click_bookmarked_block(index) self.bookmarks_page.click_bookmarked_block(index)
self.courseware_page.wait_for_page() self.courseware_page.wait_for_page()
self.assertIn(self.courseware_page.active_usage_id(), xblock_usage_ids) 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): def test_bookmark_shows_updated_breadcrumb_after_publish(self):
""" """
...@@ -345,16 +343,14 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -345,16 +343,14 @@ class BookmarksTest(BookmarksTestMixin):
Then I visit unit page in studio Then I visit unit page in studio
Then I change unit display_name Then I change unit display_name
And I publish the changes And I publish the changes
Then I visit my courseware page Then I visit my bookmarks page
And I visit bookmarks list page
When I see the bookmark When I see the bookmark
Then I can see the breadcrumb trail Then I can see the breadcrumb trail has the updated display_name.
with updated display_name.
""" """
self._test_setup(num_chapters=1) self.setup_test(num_chapters=1)
self._bookmark_units(num_units=1) self.bookmark_units(num_units=1)
self._navigate_to_bookmarks_list() self.bookmarks_page.visit()
self._verify_breadcrumbs(num_units=1) self._verify_breadcrumbs(num_units=1)
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
...@@ -371,9 +367,8 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -371,9 +367,8 @@ class BookmarksTest(BookmarksTestMixin):
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
LmsAutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).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) self._verify_breadcrumbs(num_units=1, modified_name=modified_name)
def test_unreachable_bookmark(self): def test_unreachable_bookmark(self):
...@@ -381,19 +376,18 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -381,19 +376,18 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: We should get a HTTP 404 for an unreachable bookmark. Scenario: We should get a HTTP 404 for an unreachable bookmark.
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked 2 units And I have bookmarked 2 units
Then I delete a bookmarked unit And I delete a bookmarked unit
Then I click on Bookmarks button And I visit my bookmarks page
And I should see a bookmarked list Then I should see a bookmarked list
When I click on deleted bookmark When I click on the deleted bookmark
Then I should navigated to 404 page Then I should navigated to 404 page
""" """
self._test_setup(num_chapters=1) self.setup_test(num_chapters=1)
self._bookmark_units(1) self.bookmark_units(1)
self._delete_section(0) self._delete_section(0)
self._navigate_to_bookmarks_list() self.bookmarks_page.visit()
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=1, bookmark_count_on_current_page=1,
...@@ -412,15 +406,14 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -412,15 +406,14 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: We can't get bookmarks more than default page size. Scenario: We can't get bookmarks more than default page size.
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 11 units available And I have bookmarked all the 11 units available
Then I click on Bookmarks button And I visit my bookmarks page
And I should see a bookmarked list Then I should see a bookmarked list
And bookmark list contains 10 bookmarked items And the bookmark list should contain 10 bookmarked items
""" """
self._test_setup(11) self.setup_test(11)
self._bookmark_units(11) self.bookmark_units(11)
self._navigate_to_bookmarks_list() self.bookmarks_page.visit()
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=10, bookmark_count_on_current_page=10,
...@@ -435,17 +428,15 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -435,17 +428,15 @@ class BookmarksTest(BookmarksTestMixin):
""" """
Scenario: Bookmarks list pagination is working as expected for single page Scenario: Bookmarks list pagination is working as expected for single page
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 2 units available And I have bookmarked all the 2 units available
Then I click on Bookmarks button And I visit my bookmarks page
And I should see a bookmarked list with 2 bookmarked items Then I should see a bookmarked list with 2 bookmarked items
And I should see paging header and footer with correct data And I should see paging header and footer with correct data
And previous and next buttons are disabled And previous and next buttons are disabled
""" """
self._test_setup(num_chapters=2) self.setup_test(num_chapters=2)
self._bookmark_units(num_units=2) self.bookmark_units(num_units=2)
self.bookmarks_page.visit()
self.bookmarks_page.click_bookmarks_button()
self.assertTrue(self.bookmarks_page.results_present()) self.assertTrue(self.bookmarks_page.results_present())
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=2, bookmark_count_on_current_page=2,
...@@ -461,11 +452,10 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -461,11 +452,10 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Next button is working as expected for bookmarks list pagination Scenario: Next button is working as expected for bookmarks list pagination
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 12 units available And I have bookmarked all the 12 units available
And I visit my bookmarks page
Then I click on Bookmarks button Then I should see a bookmarked list of 10 items
And I should see a bookmarked list of 10 items
And I should see paging header and footer with correct info And I should see paging header and footer with correct info
Then I click on next page button in footer Then I click on next page button in footer
...@@ -473,10 +463,10 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -473,10 +463,10 @@ class BookmarksTest(BookmarksTestMixin):
And I should see a bookmarked list with 2 items And I should see a bookmarked list with 2 items
And I should see paging header and footer with correct info And I should see paging header and footer with correct info
""" """
self._test_setup(num_chapters=12) self.setup_test(num_chapters=12)
self._bookmark_units(num_units=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.assertTrue(self.bookmarks_page.results_present())
self._verify_pagination_info( self._verify_pagination_info(
...@@ -503,9 +493,8 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -503,9 +493,8 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Previous button is working as expected for bookmarks list pagination Scenario: Previous button is working as expected for bookmarks list pagination
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 12 units available 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 Then I click on next page button in footer
And I should be navigated to second page And I should be navigated to second page
...@@ -516,10 +505,10 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -516,10 +505,10 @@ class BookmarksTest(BookmarksTestMixin):
And I should be navigated to first page And I should be navigated to first page
And I should see paging header and footer with correct info And I should see paging header and footer with correct info
""" """
self._test_setup(num_chapters=12) self.setup_test(num_chapters=12)
self._bookmark_units(num_units=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.assertTrue(self.bookmarks_page.results_present())
self.bookmarks_page.press_next_page_button() self.bookmarks_page.press_next_page_button()
...@@ -547,19 +536,17 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -547,19 +536,17 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Bookmarks list pagination works as expected for valid page number Scenario: Bookmarks list pagination works as expected for valid page number
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 12 units available And I have bookmarked all the 12 units available
And I visit my bookmarks page
Then I click on Bookmarks button Then I should see a bookmarked list
And I should see a bookmarked list
And I should see total page value is 2 And I should see total page value is 2
Then I enter 2 in the page number input Then I enter 2 in the page number input
And I should be navigated to page 2 And I should be navigated to page 2
""" """
self._test_setup(num_chapters=11) self.setup_test(num_chapters=11)
self._bookmark_units(num_units=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.assertTrue(self.bookmarks_page.results_present())
self.assertEqual(self.bookmarks_page.get_total_pages, 2) self.assertEqual(self.bookmarks_page.get_total_pages, 2)
...@@ -578,18 +565,17 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -578,18 +565,17 @@ class BookmarksTest(BookmarksTestMixin):
Scenario: Bookmarks list pagination works as expected for invalid page number Scenario: Bookmarks list pagination works as expected for invalid page number
Given that I am a registered user Given that I am a registered user
And I visit my courseware page
And I have bookmarked all the 11 units available And I have bookmarked all the 11 units available
Then I click on Bookmarks button And I visit my bookmarks page
And I should see a bookmarked list Then I should see a bookmarked list
And I should see total page value is 2 And I should see total page value is 2
Then I enter 3 in the page number input Then I enter 3 in the page number input
And I should stay at page 1 And I should stay at page 1
""" """
self._test_setup(num_chapters=11) self.setup_test(num_chapters=11)
self._bookmark_units(num_units=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.assertTrue(self.bookmarks_page.results_present())
self.assertEqual(self.bookmarks_page.get_total_pages, 2) self.assertEqual(self.bookmarks_page.get_total_pages, 2)
...@@ -613,7 +599,7 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -613,7 +599,7 @@ class BookmarksTest(BookmarksTestMixin):
When I click on bookmarked unit When I click on bookmarked unit
Then `edx.course.bookmark.accessed` event is emitted Then `edx.course.bookmark.accessed` event is emitted
""" """
self._test_setup(num_chapters=1) self.setup_test(num_chapters=1)
self.reset_event_tracking() self.reset_event_tracking()
# create expected event data # create expected event data
...@@ -627,8 +613,8 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -627,8 +613,8 @@ class BookmarksTest(BookmarksTestMixin):
} }
} }
] ]
self._bookmark_units(num_units=1) self.bookmark_units(num_units=1)
self.bookmarks_page.click_bookmarks_button() self.bookmarks_page.visit()
self._verify_pagination_info( self._verify_pagination_info(
bookmark_count_on_current_page=1, bookmark_count_on_current_page=1,
...@@ -641,3 +627,18 @@ class BookmarksTest(BookmarksTestMixin): ...@@ -641,3 +627,18 @@ class BookmarksTest(BookmarksTestMixin):
self.bookmarks_page.click_bookmarked_block(0) self.bookmarks_page.click_bookmarked_block(0)
self.verify_event_data('edx.bookmark.accessed', event_data) 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 ...@@ -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>
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 = { ...@@ -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([
'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'; '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