Add/Remove Bookmark button to each unit in LMS courseware.
TNL-1957
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
$sequence--border-color: #C8C8C8;
|
||||
|
||||
$link-color: rgb(26, 161, 222);
|
||||
// repeated extends - needed since LMS styling was referenced
|
||||
.block-link {
|
||||
border-left: 1px solid lighten($sequence--border-color, 10%);
|
||||
@@ -36,7 +36,7 @@ $sequence--border-color: #C8C8C8;
|
||||
// TODO (cpennington): This doesn't work anymore. XModules aren't able to
|
||||
// import from external sources.
|
||||
@extend .topbar;
|
||||
margin: -4px 0 ($baseline*1.5);
|
||||
margin: -4px 0 $baseline;
|
||||
position: relative;
|
||||
border-bottom: none;
|
||||
z-index: 0;
|
||||
@@ -119,6 +119,10 @@ $sequence--border-color: #C8C8C8;
|
||||
-webkit-font-smoothing: antialiased; // Clear up the lines on the icons
|
||||
}
|
||||
|
||||
i.fa-bookmark {
|
||||
color: $link-color;
|
||||
}
|
||||
|
||||
&.inactive {
|
||||
|
||||
.icon {
|
||||
@@ -142,6 +146,10 @@ $sequence--border-color: #C8C8C8;
|
||||
.icon {
|
||||
color: rgb(10, 10, 10);
|
||||
}
|
||||
|
||||
i.fa-bookmark {
|
||||
color: $link-color;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -295,3 +303,4 @@ nav.sequence-bottom {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ class @Sequence
|
||||
|
||||
bind: ->
|
||||
@$('#sequence-list a').click @goto
|
||||
@el.on 'bookmark:add', @addBookmarkIconToActiveNavItem
|
||||
@el.on 'bookmark:remove', @removeBookmarkIconFromActiveNavItem
|
||||
|
||||
initProgress: ->
|
||||
@progressTable = {} # "#problem_#{id}" -> progress
|
||||
@@ -102,8 +104,9 @@ class @Sequence
|
||||
@mark_active new_position
|
||||
|
||||
current_tab = @contents.eq(new_position - 1)
|
||||
@content_container.html(current_tab.text()).attr("aria-labelledby", current_tab.attr("aria-labelledby"))
|
||||
|
||||
bookmarked = if @el.find('.active .bookmark-icon').hasClass('bookmarked') then true else false
|
||||
@content_container.html(current_tab.text()).attr("aria-labelledby", current_tab.attr("aria-labelledby")).data('bookmarked', bookmarked)
|
||||
XBlock.initializeBlocks(@content_container, @requestToken)
|
||||
|
||||
window.update_schematics() # For embedded circuit simulator exercises in 6.002x
|
||||
@@ -116,6 +119,8 @@ class @Sequence
|
||||
sequence_links = @content_container.find('a.seqnav')
|
||||
sequence_links.click @goto
|
||||
|
||||
@el.find('.path').html(@el.find('.nav-item.active').data('path'))
|
||||
|
||||
@sr_container.focus();
|
||||
# @$("a.active").blur()
|
||||
|
||||
@@ -180,3 +185,13 @@ class @Sequence
|
||||
element.removeClass("inactive")
|
||||
.removeClass("visited")
|
||||
.addClass("active")
|
||||
|
||||
addBookmarkIconToActiveNavItem: (event) =>
|
||||
event.preventDefault()
|
||||
@el.find('.nav-item.active .bookmark-icon').removeClass('is-hidden').addClass('bookmarked')
|
||||
@el.find('.nav-item.active .bookmark-icon-sr').text(gettext('Bookmarked'))
|
||||
|
||||
removeBookmarkIconFromActiveNavItem: (event) =>
|
||||
event.preventDefault()
|
||||
@el.find('.nav-item.active .bookmark-icon').removeClass('bookmarked').addClass('is-hidden')
|
||||
@el.find('.nav-item.active .bookmark-icon-sr').text('')
|
||||
|
||||
@@ -316,6 +316,7 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
|
||||
fragment.add_content(self.system.render_template('vert_module.html', {
|
||||
'items': contents,
|
||||
'xblock_context': context,
|
||||
'show_bookmark_button': False,
|
||||
}))
|
||||
return fragment
|
||||
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
/* JavaScript for Vertical Student View. */
|
||||
window.VerticalStudentView = function (runtime, element) {
|
||||
|
||||
RequireJS.require(['js/bookmarks/views/bookmark_button'], function (BookmarkButton) {
|
||||
var $element = $(element);
|
||||
var $bookmarkButtonElement = $element.find('.bookmark-button');
|
||||
|
||||
return new BookmarkButton({
|
||||
el: $bookmarkButtonElement,
|
||||
bookmarkId: $bookmarkButtonElement.data('bookmarkId'),
|
||||
usageId: $element.data('usageId'),
|
||||
bookmarked: $element.parent('#seq_content').data('bookmarked'),
|
||||
apiUrl: $(".courseware-bookmarks-button").data('bookmarksApiUrl')
|
||||
});
|
||||
});
|
||||
};
|
||||
@@ -55,7 +55,6 @@ class SequenceFields(object):
|
||||
scope=Scope.settings,
|
||||
)
|
||||
|
||||
|
||||
class ProctoringFields(object):
|
||||
"""
|
||||
Fields that are specific to Proctored or Timed Exams
|
||||
@@ -119,9 +118,12 @@ class ProctoringFields(object):
|
||||
|
||||
@XBlock.wants('proctoring')
|
||||
@XBlock.wants('credit')
|
||||
class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
''' Layout module which lays out content in a temporal sequence
|
||||
'''
|
||||
@XBlock.needs("user")
|
||||
@XBlock.needs("bookmarks")
|
||||
class SequenceModule(SequenceFields, XModule):
|
||||
"""
|
||||
Layout module which lays out content in a temporal sequence
|
||||
"""
|
||||
js = {
|
||||
'coffee': [resource_string(__name__, 'js/src/sequence/display.coffee')],
|
||||
'js': [resource_string(__name__, 'js/src/sequence/display/jquery.sequence.js')],
|
||||
@@ -182,7 +184,12 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
contents = []
|
||||
|
||||
fragment = Fragment()
|
||||
context = context or {}
|
||||
|
||||
bookmarks_service = self.runtime.service(self, "bookmarks")
|
||||
context["username"] = self.runtime.service(self, "user").get_current_user().opt_attrs['edx-platform.username']
|
||||
|
||||
display_names = [self.get_parent().display_name or '', self.display_name or '']
|
||||
# Is this sequential part of a timed or proctored exam?
|
||||
if self.is_time_limited:
|
||||
view_html = self._time_limited_student_view(context)
|
||||
@@ -194,6 +201,9 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
return fragment
|
||||
|
||||
for child in self.get_display_items():
|
||||
is_bookmarked = bookmarks_service.is_bookmarked(usage_key=child.scope_ids.usage_id)
|
||||
context["bookmarked"] = is_bookmarked
|
||||
|
||||
progress = child.get_progress()
|
||||
rendered_child = child.render(STUDENT_VIEW, context)
|
||||
fragment.add_frag_resources(rendered_child)
|
||||
@@ -209,6 +219,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
|
||||
'progress_detail': Progress.to_js_detail_str(progress),
|
||||
'type': child.get_icon_class(),
|
||||
'id': child.scope_ids.usage_id.to_deprecated_string(),
|
||||
'bookmarked': is_bookmarked,
|
||||
'path': " > ".join(display_names + [child.display_name or '']),
|
||||
}
|
||||
if childinfo['title'] == '':
|
||||
childinfo['title'] = child.display_name_with_default
|
||||
|
||||
@@ -37,18 +37,31 @@ class BaseVerticalBlockTest(XModuleXmlImportTest):
|
||||
self.vertical = course_seq.get_children()[0]
|
||||
self.vertical.xmodule_runtime = self.module_system
|
||||
|
||||
self.username = "bilbo"
|
||||
self.default_context = {"bookmarked": False, "username": self.username}
|
||||
|
||||
|
||||
class VerticalBlockTestCase(BaseVerticalBlockTest):
|
||||
"""
|
||||
Tests for the VerticalBlock.
|
||||
"""
|
||||
def assert_bookmark_info_in(self, content):
|
||||
"""
|
||||
Assert content has all the bookmark info.
|
||||
"""
|
||||
self.assertIn('bookmark_id', content)
|
||||
self.assertIn('{},{}'.format(self.username, unicode(self.vertical.location)), content)
|
||||
self.assertIn('bookmarked', content)
|
||||
self.assertIn('show_bookmark_button', content)
|
||||
|
||||
def test_render_student_view(self):
|
||||
"""
|
||||
Test the rendering of the student view.
|
||||
"""
|
||||
html = self.module_system.render(self.vertical, STUDENT_VIEW, {}).content
|
||||
html = self.module_system.render(self.vertical, STUDENT_VIEW, self.default_context).content
|
||||
self.assertIn(self.test_html_1, html)
|
||||
self.assertIn(self.test_html_2, html)
|
||||
self.assert_bookmark_info_in(html)
|
||||
|
||||
def test_render_studio_view(self):
|
||||
"""
|
||||
|
||||
@@ -54,7 +54,14 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
|
||||
fragment.add_content(self.system.render_template('vert_module.html', {
|
||||
'items': contents,
|
||||
'xblock_context': context,
|
||||
'show_bookmark_button': True,
|
||||
'bookmarked': child_context['bookmarked'],
|
||||
'bookmark_id': "{},{}".format(child_context['username'], unicode(self.location))
|
||||
}))
|
||||
|
||||
fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/vertical_student_view.js'))
|
||||
fragment.initialize_js('VerticalStudentView')
|
||||
|
||||
return fragment
|
||||
|
||||
def author_view(self, context):
|
||||
|
||||
@@ -7,7 +7,7 @@ from .course_page import CoursePage
|
||||
|
||||
class BookmarksPage(CoursePage):
|
||||
"""
|
||||
Coursware Bookmarks Page.
|
||||
Courseware Bookmarks Page.
|
||||
"""
|
||||
url = None
|
||||
url_path = "courseware/"
|
||||
@@ -23,10 +23,11 @@ class BookmarksPage(CoursePage):
|
||||
""" Check if bookmarks button is visible """
|
||||
return self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).visible
|
||||
|
||||
def click_bookmarks_button(self):
|
||||
def click_bookmarks_button(self, wait_for_results=True):
|
||||
""" Click on Bookmarks button """
|
||||
self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).first.click()
|
||||
EmptyPromise(self.results_present, "Bookmarks results present").fulfill()
|
||||
if wait_for_results:
|
||||
EmptyPromise(self.results_present, "Bookmarks results present").fulfill()
|
||||
|
||||
def results_present(self):
|
||||
""" Check if bookmarks results are present """
|
||||
@@ -53,9 +54,9 @@ class BookmarksPage(CoursePage):
|
||||
breadcrumbs = self.q(css=self.BOOKMARKED_BREADCRUMBS).text
|
||||
return [breadcrumb.replace('\n', '').split('-') for breadcrumb in breadcrumbs]
|
||||
|
||||
def click_bookmark(self, index):
|
||||
def click_bookmarked_block(self, index):
|
||||
"""
|
||||
Click on bookmark at index `index`
|
||||
Click on bookmarked block at index `index`
|
||||
|
||||
Arguments:
|
||||
index (int): bookmark index in the list
|
||||
|
||||
@@ -193,13 +193,13 @@ class CourseNavPage(PageObject):
|
||||
)
|
||||
|
||||
# Regular expression to remove HTML span tags from a string
|
||||
REMOVE_SPAN_TAG_RE = re.compile(r'<span.+/span>')
|
||||
REMOVE_SPAN_TAG_RE = re.compile(r'</span>(.+)<span')
|
||||
|
||||
def _clean_seq_titles(self, element):
|
||||
"""
|
||||
Clean HTML of sequence titles, stripping out span tags and returning the first line.
|
||||
"""
|
||||
return self.REMOVE_SPAN_TAG_RE.sub('', element.get_attribute('innerHTML')).strip().split('\n')[0]
|
||||
return self.REMOVE_SPAN_TAG_RE.search(element.get_attribute('innerHTML')).groups()[0].strip()
|
||||
|
||||
def go_to_sequential_position(self, sequential_position):
|
||||
"""
|
||||
|
||||
@@ -3,6 +3,7 @@ Courseware page.
|
||||
"""
|
||||
|
||||
from .course_page import CoursePage
|
||||
from bok_choy.promise import EmptyPromise
|
||||
from selenium.webdriver.common.action_chains import ActionChains
|
||||
|
||||
|
||||
@@ -177,6 +178,32 @@ class CoursewarePage(CoursePage):
|
||||
attribute_value = lambda el: el.get_attribute('data-id')
|
||||
return self.q(css='#sequence-list a').filter(get_active).map(attribute_value).results[0]
|
||||
|
||||
@property
|
||||
def breadcrumb(self):
|
||||
""" Return the course tree breadcrumb shown above the sequential bar """
|
||||
return [part.strip() for part in self.q(css='.path').text[0].split('>')]
|
||||
|
||||
def bookmark_button_visible(self):
|
||||
""" Check if bookmark button is visible """
|
||||
EmptyPromise(lambda: self.q(css='.bookmark-button').visible, "Bookmark button visible").fulfill()
|
||||
return True
|
||||
|
||||
@property
|
||||
def bookmark_button_state(self):
|
||||
""" Return `bookmarked` if button is in bookmarked state else '' """
|
||||
return 'bookmarked' if self.q(css='.bookmark-button.bookmarked').present else ''
|
||||
|
||||
@property
|
||||
def bookmark_icon_visible(self):
|
||||
""" Check if bookmark icon is visible on active sequence nav item """
|
||||
return self.q(css='.active .bookmark-icon').visible
|
||||
|
||||
def click_bookmark_unit_button(self):
|
||||
""" Bookmark a unit by clicking on Bookmark button """
|
||||
previous_state = self.bookmark_button_state
|
||||
self.q(css='.bookmark-button').first.click()
|
||||
EmptyPromise(lambda: self.bookmark_button_state != previous_state, "Bookmark button toggled").fulfill()
|
||||
|
||||
|
||||
class CoursewareSequentialTabPage(CoursePage):
|
||||
"""
|
||||
|
||||
@@ -2,17 +2,15 @@
|
||||
"""
|
||||
End-to-end tests for the courseware unit bookmarks.
|
||||
"""
|
||||
import json
|
||||
import requests
|
||||
|
||||
from ...pages.studio.auto_auth import AutoAuthPage
|
||||
from ...pages.lms.bookmarks import BookmarksPage
|
||||
from ...pages.lms.courseware import CoursewarePage
|
||||
from ...pages.lms.course_nav import CourseNavPage
|
||||
from ...pages.studio.overview import CourseOutlinePage
|
||||
from ...pages.common.logout import LogoutPage
|
||||
|
||||
from ...fixtures.course import CourseFixture, XBlockFixtureDesc
|
||||
from ...fixtures import LMS_BASE_URL
|
||||
from ..helpers import EventsTestMixin, UniqueCourseTest, is_404_page
|
||||
|
||||
|
||||
@@ -22,30 +20,29 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest):
|
||||
"""
|
||||
USERNAME = "STUDENT"
|
||||
EMAIL = "student@example.com"
|
||||
COURSE_TREE_INFO = [
|
||||
['TestSection1', 'TestSubsection1', 'TestProblem1'],
|
||||
['TestSection2', 'TestSubsection2', 'TestProblem2']
|
||||
]
|
||||
|
||||
def create_course_fixture(self):
|
||||
""" Create course fixture """
|
||||
def create_course_fixture(self, num_chapters):
|
||||
"""
|
||||
Create course fixture
|
||||
|
||||
Arguments:
|
||||
num_chapters: number of chapters to create
|
||||
"""
|
||||
self.course_fixture = CourseFixture( # pylint: disable=attribute-defined-outside-init
|
||||
self.course_info['org'], self.course_info['number'],
|
||||
self.course_info['run'], self.course_info['display_name']
|
||||
)
|
||||
|
||||
self.course_fixture.add_children(
|
||||
XBlockFixtureDesc('chapter', self.COURSE_TREE_INFO[0][0]).add_children(
|
||||
XBlockFixtureDesc('sequential', self.COURSE_TREE_INFO[0][1]).add_children(
|
||||
XBlockFixtureDesc('problem', self.COURSE_TREE_INFO[0][2])
|
||||
xblocks = []
|
||||
for index in range(num_chapters):
|
||||
xblocks += [
|
||||
XBlockFixtureDesc('chapter', 'TestSection{}'.format(index)).add_children(
|
||||
XBlockFixtureDesc('sequential', 'TestSubsection{}'.format(index)).add_children(
|
||||
XBlockFixtureDesc('vertical', 'TestVertical{}'.format(index))
|
||||
)
|
||||
)
|
||||
),
|
||||
XBlockFixtureDesc('chapter', self.COURSE_TREE_INFO[1][0]).add_children(
|
||||
XBlockFixtureDesc('sequential', self.COURSE_TREE_INFO[1][1]).add_children(
|
||||
XBlockFixtureDesc('problem', self.COURSE_TREE_INFO[1][2])
|
||||
)
|
||||
)
|
||||
).install()
|
||||
]
|
||||
self.course_fixture.add_children(*xblocks).install()
|
||||
|
||||
|
||||
class BookmarksTest(BookmarksTestMixin):
|
||||
@@ -66,35 +63,64 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
self.course_info['run']
|
||||
)
|
||||
|
||||
self.create_course_fixture()
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.bookmarks_page = BookmarksPage(self.browser, self.course_id)
|
||||
self.course_nav = CourseNavPage(self.browser)
|
||||
|
||||
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.
|
||||
AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit()
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.courseware_page.visit()
|
||||
self.bookmarks = BookmarksPage(self.browser, self.course_id)
|
||||
|
||||
# Use auto-auth to retrieve the session for a logged in user
|
||||
self.session = requests.Session()
|
||||
response = self.session.get(LMS_BASE_URL + "/auto_auth?username=STUDENT&email=student@example.com")
|
||||
self.assertTrue(response.ok, "Failed to get session info")
|
||||
def _bookmark_unit(self, index):
|
||||
"""
|
||||
Bookmark a unit
|
||||
|
||||
def _bookmark_unit(self, course_id, usage_id):
|
||||
""" Bookmark a single unit """
|
||||
csrftoken = self.session.cookies['csrftoken']
|
||||
headers = {'Content-type': 'application/json', "X-CSRFToken": csrftoken}
|
||||
url = LMS_BASE_URL + "/api/bookmarks/v0/bookmarks/?course_id=" + course_id + '&fields=path'
|
||||
data = json.dumps({'usage_id': usage_id})
|
||||
Arguments:
|
||||
index: unit index to bookmark
|
||||
"""
|
||||
self.course_nav.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index))
|
||||
self.courseware_page.click_bookmark_unit_button()
|
||||
|
||||
response = self.session.post(url, data=data, headers=headers, cookies=self.session.cookies)
|
||||
response = json.loads(response.text)
|
||||
self.assertTrue(response['usage_id'] == usage_id, "Failed to bookmark unit")
|
||||
def _bookmark_units(self, num_units):
|
||||
"""
|
||||
Bookmark first `num_units` units by visiting them
|
||||
|
||||
def _bookmarks_blocks(self, xblocks):
|
||||
""" Bookmark all units in a course """
|
||||
for xblock in xblocks:
|
||||
self._bookmark_unit(self.course_id, usage_id=xblock.locator)
|
||||
Arguments:
|
||||
num_units(int): Number of units to bookmarks
|
||||
"""
|
||||
for index in range(num_units):
|
||||
self._bookmark_unit(index)
|
||||
|
||||
def _breadcrumb(self, num_units):
|
||||
"""
|
||||
Creates breadcrumbs for the first `num_units`
|
||||
|
||||
Arguments:
|
||||
num_units(int): Number of units for which we want to create breadcrumbs
|
||||
|
||||
Returns:
|
||||
list of breadcrumbs
|
||||
"""
|
||||
breadcrumbs = []
|
||||
for index in range(num_units):
|
||||
breadcrumbs.append(
|
||||
[
|
||||
'TestSection{}'.format(index),
|
||||
'TestSubsection{}'.format(index),
|
||||
'TestVertical{}'.format(index)
|
||||
]
|
||||
)
|
||||
return breadcrumbs
|
||||
|
||||
def _delete_section(self, index):
|
||||
""" Delete a section at index `index` """
|
||||
@@ -119,6 +145,39 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
self.courseware_page.visit()
|
||||
self.courseware_page.wait_for_page()
|
||||
|
||||
def _toggle_bookmark_and_verify(self, bookmark_icon_state, bookmark_button_state, bookmarked_count):
|
||||
"""
|
||||
Bookmark/Un-Bookmark a unit and then verify
|
||||
"""
|
||||
self.assertTrue(self.courseware_page.bookmark_button_visible)
|
||||
self.courseware_page.click_bookmark_unit_button()
|
||||
self.assertEqual(self.courseware_page.bookmark_icon_visible, bookmark_icon_state)
|
||||
self.assertEqual(self.courseware_page.bookmark_button_state, bookmark_button_state)
|
||||
self.bookmarks_page.click_bookmarks_button()
|
||||
self.assertEqual(self.bookmarks_page.count(), bookmarked_count)
|
||||
|
||||
def test_bookmark_button(self):
|
||||
"""
|
||||
Scenario: Bookmark unit button toggles correctly
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
For first 2 units
|
||||
I visit the unit
|
||||
And I can see the Bookmark button
|
||||
When I click on Bookmark button
|
||||
Then unit should be bookmarked
|
||||
Then I click again on the bookmark button
|
||||
And I should see a unit un-bookmarked
|
||||
"""
|
||||
self._test_setup()
|
||||
for index in range(2):
|
||||
self.course_nav.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index))
|
||||
|
||||
self._toggle_bookmark_and_verify(True, 'bookmarked', 1)
|
||||
self.bookmarks_page.click_bookmarks_button(False)
|
||||
self._toggle_bookmark_and_verify(False, '', 0)
|
||||
|
||||
def test_empty_bookmarks_list(self):
|
||||
"""
|
||||
Scenario: An empty bookmarks list is shown if there are no bookmarked units.
|
||||
@@ -130,15 +189,16 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
Then I should see an empty bookmarks list
|
||||
And empty bookmarks list content is correct
|
||||
"""
|
||||
self.assertTrue(self.bookmarks.bookmarks_button_visible())
|
||||
self.bookmarks.click_bookmarks_button()
|
||||
self.assertEqual(self.bookmarks.results_header_text(), 'MY BOOKMARKS')
|
||||
self.assertEqual(self.bookmarks.empty_header_text(), 'You have not bookmarked any courseware pages yet.')
|
||||
self._test_setup()
|
||||
self.assertTrue(self.bookmarks_page.bookmarks_button_visible())
|
||||
self.bookmarks_page.click_bookmarks_button()
|
||||
self.assertEqual(self.bookmarks_page.results_header_text(), 'MY BOOKMARKS')
|
||||
self.assertEqual(self.bookmarks_page.empty_header_text(), 'You have not bookmarked any courseware pages yet.')
|
||||
|
||||
empty_list_text = ("Use bookmarks to help you easily return to courseware pages. To bookmark a page, "
|
||||
"select Bookmark in the upper right corner of that page. To see a list of all your "
|
||||
"bookmarks, select Bookmarks in the upper left corner of any courseware page.")
|
||||
self.assertEqual(self.bookmarks.empty_list_text(), empty_list_text)
|
||||
self.assertEqual(self.bookmarks_page.empty_list_text(), empty_list_text)
|
||||
|
||||
def test_bookmarks_list(self):
|
||||
"""
|
||||
@@ -160,27 +220,30 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
# discarded by the current version of MySQL we are using due to the
|
||||
# lack of support. Due to which order of bookmarked units will be
|
||||
# incorrect.
|
||||
xblocks = self.course_fixture.get_nested_xblocks(category="problem")
|
||||
self._bookmarks_blocks(xblocks)
|
||||
self._test_setup()
|
||||
self._bookmark_units(2)
|
||||
|
||||
self.bookmarks.click_bookmarks_button()
|
||||
self.assertTrue(self.bookmarks.results_present())
|
||||
self.assertEqual(self.bookmarks.results_header_text(), 'MY BOOKMARKS')
|
||||
self.assertEqual(self.bookmarks.count(), 2)
|
||||
self.bookmarks_page.click_bookmarks_button()
|
||||
self.assertTrue(self.bookmarks_page.results_present())
|
||||
self.assertEqual(self.bookmarks_page.results_header_text(), 'MY BOOKMARKS')
|
||||
self.assertEqual(self.bookmarks_page.count(), 2)
|
||||
|
||||
bookmarked_breadcrumbs = self.bookmarks.breadcrumbs()
|
||||
bookmarked_breadcrumbs = self.bookmarks_page.breadcrumbs()
|
||||
|
||||
# Verify bookmarked breadcrumbs
|
||||
self.assertItemsEqual(bookmarked_breadcrumbs, self.COURSE_TREE_INFO)
|
||||
breadcrumbs = self._breadcrumb(2)
|
||||
self.assertItemsEqual(bookmarked_breadcrumbs, breadcrumbs)
|
||||
|
||||
# get usage ids for units
|
||||
xblocks = self.course_fixture.get_nested_xblocks(category="vertical")
|
||||
xblock_usage_ids = [xblock.locator for xblock in xblocks]
|
||||
# Verify link navigation
|
||||
for index in range(2):
|
||||
self.bookmarks.click_bookmark(index)
|
||||
self.bookmarks_page.click_bookmarked_block(index)
|
||||
self.courseware_page.wait_for_page()
|
||||
self.assertTrue(self.courseware_page.active_usage_id() in xblock_usage_ids)
|
||||
self.courseware_page.visit().wait_for_page()
|
||||
self.bookmarks.click_bookmarks_button()
|
||||
self.bookmarks_page.click_bookmarks_button()
|
||||
|
||||
def test_unreachable_bookmark(self):
|
||||
"""
|
||||
@@ -195,13 +258,34 @@ class BookmarksTest(BookmarksTestMixin):
|
||||
When I click on deleted bookmark
|
||||
Then I should navigated to 404 page
|
||||
"""
|
||||
self._bookmarks_blocks(self.course_fixture.get_nested_xblocks(category="problem"))
|
||||
|
||||
self._test_setup()
|
||||
self._bookmark_units(2)
|
||||
self._delete_section(0)
|
||||
|
||||
self.bookmarks.click_bookmarks_button()
|
||||
self.assertTrue(self.bookmarks.results_present())
|
||||
self.assertEqual(self.bookmarks.count(), 2)
|
||||
self.bookmarks_page.click_bookmarks_button()
|
||||
self.assertTrue(self.bookmarks_page.results_present())
|
||||
self.assertEqual(self.bookmarks_page.count(), 2)
|
||||
|
||||
self.bookmarks.click_bookmark(0)
|
||||
self.bookmarks_page.click_bookmarked_block(1)
|
||||
self.assertTrue(is_404_page(self.browser))
|
||||
|
||||
def test_page_size_limit(self):
|
||||
"""
|
||||
Scenario: We can get more bookmarks if page size is greater than default page size.
|
||||
Note:
|
||||
* Current Bookmarks API page_size value is 10.
|
||||
* page_size value in bookmarks client side is set to 500.
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
And I have bookmarked all the units available
|
||||
Then I click on Bookmarks button
|
||||
And I should see a bookmarked list
|
||||
And bookmark list contains 11 bookmarked items
|
||||
"""
|
||||
self._test_setup(11)
|
||||
self._bookmark_units(11)
|
||||
|
||||
self.bookmarks_page.click_bookmarks_button()
|
||||
self.assertTrue(self.bookmarks_page.results_present())
|
||||
self.assertEqual(self.bookmarks_page.count(), 11)
|
||||
|
||||
@@ -29,6 +29,7 @@ class CoursewareTest(UniqueCourseTest):
|
||||
super(CoursewareTest, self).setUp()
|
||||
|
||||
self.courseware_page = CoursewarePage(self.browser, self.course_id)
|
||||
self.course_nav = CourseNavPage(self.browser)
|
||||
|
||||
self.course_outline = CourseOutlinePage(
|
||||
self.browser,
|
||||
@@ -38,12 +39,12 @@ class CoursewareTest(UniqueCourseTest):
|
||||
)
|
||||
|
||||
# Install a course with sections/problems, tabs, updates, and handouts
|
||||
course_fix = CourseFixture(
|
||||
self.course_fix = CourseFixture(
|
||||
self.course_info['org'], self.course_info['number'],
|
||||
self.course_info['run'], self.course_info['display_name']
|
||||
)
|
||||
|
||||
course_fix.add_children(
|
||||
self.course_fix.add_children(
|
||||
XBlockFixtureDesc('chapter', 'Test Section 1').add_children(
|
||||
XBlockFixtureDesc('sequential', 'Test Subsection 1').add_children(
|
||||
XBlockFixtureDesc('problem', 'Test Problem 1')
|
||||
@@ -67,6 +68,18 @@ class CoursewareTest(UniqueCourseTest):
|
||||
self.problem_page = ProblemPage(self.browser)
|
||||
self.assertEqual(self.problem_page.problem_name, 'TEST PROBLEM 1')
|
||||
|
||||
def _change_problem_release_date_in_studio(self):
|
||||
"""
|
||||
|
||||
"""
|
||||
self.course_outline.q(css=".subsection-header-actions .configure-button").first.click()
|
||||
self.course_outline.q(css="#start_date").fill("01/01/2030")
|
||||
self.course_outline.q(css=".action-save").first.click()
|
||||
|
||||
def _create_breadcrumb(self, index):
|
||||
""" Create breadcrumb """
|
||||
return ['Test Section {}'.format(index), 'Test Subsection {}'.format(index), 'Test Problem {}'.format(index)]
|
||||
|
||||
def _auto_auth(self, username, email, staff):
|
||||
"""
|
||||
Logout and login with given credentials.
|
||||
@@ -92,6 +105,9 @@ class CoursewareTest(UniqueCourseTest):
|
||||
# Set release date for subsection in future.
|
||||
self.course_outline.change_problem_release_date_in_studio()
|
||||
|
||||
# Wait for 2 seconds to save new date.
|
||||
time.sleep(2)
|
||||
|
||||
# Logout and login as a student.
|
||||
LogoutPage(self.browser).visit()
|
||||
self._auto_auth(self.USERNAME, self.EMAIL, False)
|
||||
@@ -246,6 +262,23 @@ class ProctoredExamTest(UniqueCourseTest):
|
||||
self.courseware_page.start_timed_exam()
|
||||
self.assertTrue(self.courseware_page.is_timer_bar_present)
|
||||
|
||||
def test_course_tree_breadcrumb(self):
|
||||
"""
|
||||
Scenario: Correct course tree breadcrumb is shown.
|
||||
|
||||
Given that I am a registered user
|
||||
And I visit my courseware page
|
||||
Then I should see correct course tree breadcrumb
|
||||
"""
|
||||
self.courseware_page.visit()
|
||||
|
||||
xblocks = self.course_fix.get_nested_xblocks(category="problem")
|
||||
for index in range(1, len(xblocks) + 1):
|
||||
self.course_nav.go_to_section('Test Section {}'.format(index), 'Test Subsection {}'.format(index))
|
||||
courseware_page_breadcrumb = self.courseware_page.breadcrumb
|
||||
expected_breadcrumb = self._create_breadcrumb(index)
|
||||
self.assertEqual(courseware_page_breadcrumb, expected_breadcrumb)
|
||||
|
||||
def test_time_allotted_field_is_not_visible_with_none_exam(self):
|
||||
"""
|
||||
Given that I am a staff member
|
||||
|
||||
@@ -145,8 +145,8 @@ class BookmarksViewTestsMixin(ModuleStoreTestCase):
|
||||
class BookmarksListViewTests(BookmarksViewTestsMixin):
|
||||
"""
|
||||
This contains the tests for GET & POST methods of bookmark.views.BookmarksListView class
|
||||
GET /api/bookmarks/v0/bookmarks/?course_id={course_id1}
|
||||
POST /api/bookmarks/v0/bookmarks
|
||||
GET /api/bookmarks/v1/bookmarks/?course_id={course_id1}
|
||||
POST /api/bookmarks/v1/bookmarks
|
||||
"""
|
||||
@ddt.data(
|
||||
('course_id={}', False),
|
||||
|
||||
@@ -42,6 +42,7 @@ from courseware.entrance_exams import (
|
||||
)
|
||||
from edxmako.shortcuts import render_to_string
|
||||
from eventtracking import tracker
|
||||
from lms.djangoapps.bookmarks.services import BookmarksService
|
||||
from lms.djangoapps.lms_xblock.field_data import LmsFieldData
|
||||
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes
|
||||
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig
|
||||
@@ -715,6 +716,8 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to
|
||||
"reverification": ReverificationService(),
|
||||
'proctoring': ProctoringService(),
|
||||
'credit': CreditService(),
|
||||
'reverification': ReverificationService(),
|
||||
'bookmarks': BookmarksService(user=user),
|
||||
},
|
||||
get_user_role=lambda: get_user_role(user, course_id),
|
||||
descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access
|
||||
|
||||
@@ -73,6 +73,7 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
|
||||
@XBlock.needs("i18n")
|
||||
@XBlock.needs("fs")
|
||||
@XBlock.needs("user")
|
||||
@XBlock.needs("bookmarks")
|
||||
class PureXBlock(XBlock):
|
||||
"""
|
||||
Pure XBlock to use in tests.
|
||||
@@ -1232,6 +1233,7 @@ class ViewInStudioTest(ModuleStoreTestCase):
|
||||
self.request.user = self.staff_user
|
||||
self.request.session = {}
|
||||
self.module = None
|
||||
self.default_context = {'bookmarked': False, 'username': self.user.username}
|
||||
|
||||
def _get_module(self, course_id, descriptor, location):
|
||||
"""
|
||||
@@ -1290,14 +1292,14 @@ class MongoViewInStudioTest(ViewInStudioTest):
|
||||
def test_view_in_studio_link_studio_course(self):
|
||||
"""Regular Studio courses should see 'View in Studio' links."""
|
||||
self.setup_mongo_course()
|
||||
result_fragment = self.module.render(STUDENT_VIEW)
|
||||
result_fragment = self.module.render(STUDENT_VIEW, context=self.default_context)
|
||||
self.assertIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
def test_view_in_studio_link_only_in_top_level_vertical(self):
|
||||
"""Regular Studio courses should not see 'View in Studio' for child verticals of verticals."""
|
||||
self.setup_mongo_course()
|
||||
# Render the parent vertical, then check that there is only a single "View Unit in Studio" link.
|
||||
result_fragment = self.module.render(STUDENT_VIEW)
|
||||
result_fragment = self.module.render(STUDENT_VIEW, context=self.default_context)
|
||||
# The single "View Unit in Studio" link should appear before the first xmodule vertical definition.
|
||||
parts = result_fragment.content.split('data-block-type="vertical"')
|
||||
self.assertEqual(3, len(parts), "Did not find two vertical blocks")
|
||||
@@ -1308,7 +1310,7 @@ class MongoViewInStudioTest(ViewInStudioTest):
|
||||
def test_view_in_studio_link_xml_authored(self):
|
||||
"""Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
|
||||
self.setup_mongo_course(course_edit_method='XML')
|
||||
result_fragment = self.module.render(STUDENT_VIEW)
|
||||
result_fragment = self.module.render(STUDENT_VIEW, context=self.default_context)
|
||||
self.assertNotIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
|
||||
@@ -1321,19 +1323,19 @@ class MixedViewInStudioTest(ViewInStudioTest):
|
||||
def test_view_in_studio_link_mongo_backed(self):
|
||||
"""Mixed mongo courses that are mongo backed should see 'View in Studio' links."""
|
||||
self.setup_mongo_course()
|
||||
result_fragment = self.module.render(STUDENT_VIEW)
|
||||
result_fragment = self.module.render(STUDENT_VIEW, context=self.default_context)
|
||||
self.assertIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
def test_view_in_studio_link_xml_authored(self):
|
||||
"""Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
|
||||
self.setup_mongo_course(course_edit_method='XML')
|
||||
result_fragment = self.module.render(STUDENT_VIEW)
|
||||
result_fragment = self.module.render(STUDENT_VIEW, context=self.default_context)
|
||||
self.assertNotIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
def test_view_in_studio_link_xml_backed(self):
|
||||
"""Course in XML only modulestore should not see 'View in Studio' links."""
|
||||
self.setup_xml_course()
|
||||
result_fragment = self.module.render(STUDENT_VIEW)
|
||||
result_fragment = self.module.render(STUDENT_VIEW, context=self.default_context)
|
||||
self.assertNotIn('View Unit in Studio', result_fragment.content)
|
||||
|
||||
|
||||
@@ -1826,7 +1828,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
|
||||
self.request_token = Mock()
|
||||
|
||||
@XBlock.register_temp_plugin(PureXBlock, identifier='pure')
|
||||
@ddt.data("user", "i18n", "fs", "field-data")
|
||||
@ddt.data("user", "i18n", "fs", "field-data", "bookmarks")
|
||||
def test_expected_services_exist(self, expected_service):
|
||||
"""
|
||||
Tests that the 'user', 'i18n', and 'fs' services are provided by the LMS runtime.
|
||||
|
||||
@@ -119,7 +119,7 @@ class SplitTestBase(ModuleStoreTestCase):
|
||||
content = resp.content
|
||||
|
||||
# Assert we see the proper icon in the top display
|
||||
self.assertIn('<a class="{} inactive progress-0"'.format(self.ICON_CLASSES[user_tag]), content)
|
||||
self.assertIn('<a class="{} inactive progress-0 nav-item"'.format(self.ICON_CLASSES[user_tag]), content)
|
||||
# And proper tooltips
|
||||
for tooltip in self.TOOLTIPS[user_tag]:
|
||||
self.assertIn(tooltip, content)
|
||||
|
||||
@@ -403,6 +403,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
if survey.utils.must_answer_survey(course, user):
|
||||
return redirect(reverse('course_survey', args=[unicode(course.id)]))
|
||||
|
||||
bookmarks_api_url = reverse('bookmarks')
|
||||
|
||||
try:
|
||||
field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
|
||||
course_key, user, course, depth=2)
|
||||
@@ -428,7 +430,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
|
||||
'studio_url': studio_url,
|
||||
'masquerade': masquerade,
|
||||
'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"),
|
||||
'reverifications': fetch_reverify_banner_info(request, course_key),
|
||||
'bookmarks_api_url': bookmarks_api_url,
|
||||
'language_preference': language_preference,
|
||||
}
|
||||
|
||||
|
||||
@@ -5,8 +5,11 @@
|
||||
'use strict';
|
||||
|
||||
return Backbone.Collection.extend({
|
||||
model : BookmarkModel,
|
||||
url: '/api/bookmarks/v0/bookmarks/',
|
||||
model: BookmarkModel,
|
||||
|
||||
url: function() {
|
||||
return $(".courseware-bookmarks-button").data('bookmarksApiUrl');
|
||||
},
|
||||
|
||||
parse: function(response) {
|
||||
return response.results;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
RequireJS.require([
|
||||
'js/bookmarks/views/bookmarks_button'
|
||||
], function (BookmarksButton) {
|
||||
'js/bookmarks/views/bookmarks_list_button'
|
||||
], function (BookmarksListButton) {
|
||||
'use strict';
|
||||
|
||||
return new BookmarksButton();
|
||||
return new BookmarksListButton();
|
||||
});
|
||||
|
||||
93
lms/static/js/bookmarks/views/bookmark_button.js
Normal file
93
lms/static/js/bookmarks/views/bookmark_button.js
Normal file
@@ -0,0 +1,93 @@
|
||||
;(function (define, undefined) {
|
||||
'use strict';
|
||||
define(['gettext', 'jquery', 'underscore', 'backbone', 'js/views/message'],
|
||||
function (gettext, $, _, Backbone, MessageView) {
|
||||
|
||||
return Backbone.View.extend({
|
||||
|
||||
errorIcon: '<i class="fa fa-fw fa-exclamation-triangle message-error" aria-hidden="true"></i>',
|
||||
errorMessage: gettext('An error has occurred. Please try again.'),
|
||||
|
||||
srAddBookmarkText: gettext('Click to add'),
|
||||
srRemoveBookmarkText: gettext('Click to remove'),
|
||||
|
||||
events: {
|
||||
'click': 'toggleBookmark'
|
||||
},
|
||||
|
||||
initialize: function (options) {
|
||||
this.apiUrl = options.apiUrl;
|
||||
this.bookmarkId = options.bookmarkId;
|
||||
this.bookmarked = options.bookmarked;
|
||||
this.usageId = options.usageId;
|
||||
this.setBookmarkState(this.bookmarked);
|
||||
},
|
||||
|
||||
toggleBookmark: function(event) {
|
||||
event.preventDefault();
|
||||
|
||||
if (this.$el.hasClass('bookmarked')) {
|
||||
this.removeBookmark();
|
||||
} else {
|
||||
this.addBookmark();
|
||||
}
|
||||
},
|
||||
|
||||
addBookmark: function() {
|
||||
var view = this;
|
||||
$.ajax({
|
||||
data: {usage_id: view.usageId},
|
||||
type: "POST",
|
||||
url: view.apiUrl,
|
||||
dataType: 'json',
|
||||
success: function () {
|
||||
view.$el.trigger('bookmark:add');
|
||||
view.setBookmarkState(true);
|
||||
},
|
||||
error: function() {
|
||||
view.showError();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeBookmark: function() {
|
||||
var view = this;
|
||||
var deleteUrl = view.apiUrl + view.bookmarkId + '/';
|
||||
|
||||
$.ajax({
|
||||
type: "DELETE",
|
||||
url: deleteUrl,
|
||||
success: function () {
|
||||
view.$el.trigger('bookmark:remove');
|
||||
view.setBookmarkState(false);
|
||||
},
|
||||
error: function() {
|
||||
view.showError();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
setBookmarkState: function(bookmarked) {
|
||||
if (bookmarked) {
|
||||
this.$el.addClass('bookmarked');
|
||||
this.$el.attr('aria-pressed', 'true');
|
||||
this.$el.find('.bookmark-sr').text(this.srRemoveBookmarkText);
|
||||
} else {
|
||||
this.$el.removeClass('bookmarked');
|
||||
this.$el.attr('aria-pressed', 'false');
|
||||
this.$el.find('.bookmark-sr').text(this.srAddBookmarkText);
|
||||
}
|
||||
},
|
||||
|
||||
showError: function() {
|
||||
if (!this.messageView) {
|
||||
this.messageView = new MessageView({
|
||||
el: $('.coursewide-message-banner'),
|
||||
templateId: '#message_banner-tpl'
|
||||
});
|
||||
}
|
||||
this.messageView.showMessage(this.errorMessage, this.errorIcon);
|
||||
}
|
||||
});
|
||||
});
|
||||
}).call(this, define || RequireJS.define);
|
||||
@@ -16,6 +16,8 @@
|
||||
errorMessage: gettext('An error has occurred. Please try again.'),
|
||||
loadingMessage: gettext('Loading'),
|
||||
|
||||
PAGE_SIZE: 500,
|
||||
|
||||
events : {
|
||||
'click .bookmarks-results-list-item': 'visitBookmark'
|
||||
},
|
||||
@@ -48,7 +50,7 @@
|
||||
|
||||
this.collection.fetch({
|
||||
reset: true,
|
||||
data: {course_id: this.courseId, fields: 'display_name,path'}
|
||||
data: {course_id: this.courseId, page_size: this.PAGE_SIZE, fields: 'display_name,path'}
|
||||
}).done(function () {
|
||||
view.hideLoadingMessage();
|
||||
view.render();
|
||||
|
||||
@@ -16,8 +16,6 @@
|
||||
},
|
||||
|
||||
initialize: function () {
|
||||
this.template = _.template($('#bookmarks_button-tpl').text());
|
||||
|
||||
this.bookmarksListView = new BookmarksListView({
|
||||
collection: new BookmarksCollection(),
|
||||
loadingMessageView: new MessageView({el: $(this.loadingMessageElement)}),
|
||||
13
lms/static/js/fixtures/bookmarks/bookmark_button.html
Normal file
13
lms/static/js/fixtures/bookmarks/bookmark_button.html
Normal file
@@ -0,0 +1,13 @@
|
||||
|
||||
<div class="coursewide-message-banner" aria-live="polite"></div>
|
||||
|
||||
<div class="xblock xblock-student_view xblock-student_view-vertical xblock-initialized">
|
||||
<div class="bookmark-button-wrapper">
|
||||
<button class="btn bookmark-button"
|
||||
aria-pressed="false"
|
||||
data-bookmark-id="bilbo,usage_1">
|
||||
<span class="sr bookmark-sr"></span>
|
||||
Bookmark
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="courseware-bookmarks-button">
|
||||
<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>
|
||||
|
||||
114
lms/static/js/spec/bookmarks/bookmark_button_view_spec.js
Normal file
114
lms/static/js/spec/bookmarks/bookmark_button_view_spec.js
Normal file
@@ -0,0 +1,114 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/bookmarks/views/bookmark_button'
|
||||
],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, BookmarkButtonView) {
|
||||
'use strict';
|
||||
|
||||
describe("bookmarks.button", function () {
|
||||
|
||||
var API_URL = 'bookmarks/api/v1/bookmarks/';
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/bookmarks/bookmark_button.html');
|
||||
TemplateHelpers.installTemplates(
|
||||
[
|
||||
'templates/fields/message_banner'
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
var createBookmarkButtonView = function(isBookmarked) {
|
||||
return new BookmarkButtonView({
|
||||
el: '.bookmark-button',
|
||||
bookmarked: isBookmarked,
|
||||
bookmarkId: 'bilbo,usage_1',
|
||||
usageId: 'usage_1',
|
||||
apiUrl: API_URL
|
||||
});
|
||||
};
|
||||
|
||||
var verifyBookmarkButtonState = function (view, bookmarked) {
|
||||
if (bookmarked) {
|
||||
expect(view.$el).toHaveAttr('aria-pressed', 'true');
|
||||
expect(view.$el).toHaveClass('bookmarked');
|
||||
expect(view.$el.find('.bookmark-sr').text()).toBe('Click to remove');
|
||||
} else {
|
||||
expect(view.$el).toHaveAttr('aria-pressed', 'false');
|
||||
expect(view.$el).not.toHaveClass('bookmarked');
|
||||
expect(view.$el.find('.bookmark-sr').text()).toBe('Click to add');
|
||||
}
|
||||
expect(view.$el.data('bookmarkId')).toBe('bilbo,usage_1');
|
||||
};
|
||||
|
||||
it("rendered correctly ", function () {
|
||||
var view = createBookmarkButtonView(false);
|
||||
verifyBookmarkButtonState(view, false);
|
||||
|
||||
// with bookmarked true
|
||||
view = createBookmarkButtonView(true);
|
||||
verifyBookmarkButtonState(view, true);
|
||||
});
|
||||
|
||||
it("bookmark/un-bookmark the block correctly", function () {
|
||||
var addBookmarkedData = {bookmarked: true, handler: 'removeBookmark', event: 'bookmark:remove', method: 'DELETE', url: API_URL + 'bilbo,usage_1/', body: null};
|
||||
var removeBookmarkData = {bookmarked: false, handler: 'addBookmark', event: 'bookmark:add', method: 'POST', url: API_URL, body: 'usage_id=usage_1'};
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
|
||||
_.each([[addBookmarkedData, removeBookmarkData], [removeBookmarkData, addBookmarkedData]], function(actionsData) {
|
||||
var firstActionData = actionsData[0];
|
||||
var secondActionData = actionsData[1];
|
||||
|
||||
var bookmarkButtonView = createBookmarkButtonView(firstActionData.bookmarked);
|
||||
verifyBookmarkButtonState(bookmarkButtonView, firstActionData.bookmarked);
|
||||
|
||||
spyOn(bookmarkButtonView, firstActionData.handler).andCallThrough();
|
||||
spyOnEvent(bookmarkButtonView.$el, firstActionData.event);
|
||||
|
||||
bookmarkButtonView.$el.click();
|
||||
|
||||
AjaxHelpers.expectRequest(requests, firstActionData.method, firstActionData.url, firstActionData.body);
|
||||
|
||||
expect(bookmarkButtonView[firstActionData.handler]).toHaveBeenCalled();
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
expect(firstActionData.event).toHaveBeenTriggeredOn(bookmarkButtonView.$el);
|
||||
bookmarkButtonView[firstActionData.handler].reset();
|
||||
|
||||
verifyBookmarkButtonState(bookmarkButtonView, secondActionData.bookmarked);
|
||||
|
||||
spyOn(bookmarkButtonView, secondActionData.handler).andCallThrough();
|
||||
spyOnEvent(bookmarkButtonView.$el, secondActionData.event);
|
||||
|
||||
bookmarkButtonView.$el.click();
|
||||
|
||||
AjaxHelpers.expectRequest(requests, secondActionData.method, secondActionData.url, secondActionData.body);
|
||||
|
||||
expect(bookmarkButtonView[secondActionData.handler]).toHaveBeenCalled();
|
||||
AjaxHelpers.respondWithJson(requests, {});
|
||||
expect(secondActionData.event).toHaveBeenTriggeredOn(bookmarkButtonView.$el);
|
||||
|
||||
verifyBookmarkButtonState(bookmarkButtonView, firstActionData.bookmarked);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
it("shows an error message for HTTP 500", function () {
|
||||
var requests = AjaxHelpers.requests(this),
|
||||
$messageBanner = $('.coursewide-message-banner'),
|
||||
bookmarkButtonView = createBookmarkButtonView(false);
|
||||
bookmarkButtonView.$el.click();
|
||||
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
|
||||
expect($messageBanner.text().trim()).toBe(bookmarkButtonView.errorMessage);
|
||||
|
||||
// For bookmarked button.
|
||||
bookmarkButtonView = createBookmarkButtonView(true);
|
||||
bookmarkButtonView.$el.click();
|
||||
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
|
||||
expect($messageBanner.text().trim()).toBe(bookmarkButtonView.errorMessage);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,13 @@
|
||||
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers',
|
||||
'js/bookmarks/views/bookmarks_button'
|
||||
'js/bookmarks/views/bookmarks_list_button'
|
||||
],
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, BookmarksButtonView) {
|
||||
function (Backbone, $, _, AjaxHelpers, TemplateHelpers, BookmarksListButtonView) {
|
||||
'use strict';
|
||||
|
||||
describe("lms.courseware.bookmarks", function () {
|
||||
|
||||
var bookmarksButtonView;
|
||||
var BOOKMARKS_API_URL = '/api/bookmarks/v0/bookmarks/';
|
||||
var BOOKMARKS_API_URL = '/api/bookmarks/v1/bookmarks/';
|
||||
|
||||
beforeEach(function () {
|
||||
loadFixtures('js/fixtures/bookmarks/bookmarks.html');
|
||||
@@ -18,7 +18,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
|
||||
]
|
||||
);
|
||||
|
||||
bookmarksButtonView = new BookmarksButtonView();
|
||||
bookmarksButtonView = new BookmarksListButtonView();
|
||||
|
||||
this.addMatchers({
|
||||
toHaveBeenCalledWithUrl: function (expectedUrl) {
|
||||
@@ -124,7 +124,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
|
||||
|
||||
it("has rendered bookmarked list correctly", function () {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var url = BOOKMARKS_API_URL + '?course_id=COURSE_ID&fields=display_name%2Cpath';
|
||||
var url = BOOKMARKS_API_URL + '?course_id=COURSE_ID&page_size=500&fields=display_name%2Cpath';
|
||||
var expectedData = createBookmarksData(3);
|
||||
|
||||
spyOn(bookmarksButtonView.bookmarksListView, 'courseId').andReturn('COURSE_ID');
|
||||
@@ -87,6 +87,13 @@
|
||||
// Discussion classes loaded explicitly until they are converted to use RequireJS
|
||||
'DiscussionModuleView': 'xmodule_js/common_static/coffee/src/discussion/discussion_module_view',
|
||||
|
||||
'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': 'js/views/message',
|
||||
|
||||
// edxnotes
|
||||
'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min',
|
||||
|
||||
@@ -738,7 +745,8 @@
|
||||
'lms/include/teams/js/spec/views/team_join_spec.js'
|
||||
'lms/include/js/spec/discovery/discovery_spec.js',
|
||||
'lms/include/js/spec/ccx/schedule_spec.js',
|
||||
'lms/include/js/spec/bookmarks/bookmarks_spec.js'
|
||||
'lms/include/js/spec/bookmarks/bookmarks_list_view_spec.js',
|
||||
'lms/include/js/spec/bookmarks/bookmark_button_view_spec.js'
|
||||
]);
|
||||
|
||||
}).call(this, requirejs, define);
|
||||
|
||||
@@ -403,6 +403,10 @@ div.course-wrapper {
|
||||
}
|
||||
}
|
||||
|
||||
.sequence .path {
|
||||
margin-bottom: ($baseline/2);
|
||||
}
|
||||
|
||||
div#seq_content {
|
||||
h1 {
|
||||
background: none;
|
||||
|
||||
@@ -67,8 +67,8 @@
|
||||
margin-bottom: $baseline;
|
||||
|
||||
&:hover {
|
||||
border-color: $link-color;
|
||||
color: $link-color;
|
||||
border-color: $m-blue;
|
||||
color: $m-blue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,6 +87,7 @@
|
||||
position: relative;
|
||||
top: -7px;
|
||||
font-family: FontAwesome;
|
||||
color: $m-blue;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
@@ -124,4 +125,59 @@
|
||||
|
||||
.bookmarks-empty-detail {
|
||||
@extend %t-copy-sub1;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Rules for bookmark icon shown on each sequence nav item
|
||||
i.bookmarked {
|
||||
top: -3px;
|
||||
position: absolute;
|
||||
left: ($baseline/4);
|
||||
}
|
||||
|
||||
|
||||
// Rules for bookmark button's different styles
|
||||
.bookmark-button-wrapper {
|
||||
text-align: right;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
@mixin base-style($border-color, $content-color) {
|
||||
background: none;
|
||||
border: 1px solid $border-color;
|
||||
border-radius: ($baseline/4);
|
||||
color: $content-color;
|
||||
|
||||
&:focus, &:active {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin icon-style($content, $color) {
|
||||
&:before {
|
||||
content: $content;
|
||||
font-family: FontAwesome;
|
||||
color: $color;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin hover-style($border-color, $content-color, $icon-content) {
|
||||
&:hover {
|
||||
background: none;
|
||||
border: 1px solid $border-color;
|
||||
color: $content-color;
|
||||
@include icon-style($icon-content, $content-color);
|
||||
}
|
||||
}
|
||||
|
||||
.bookmark-button.bookmarked {
|
||||
@include base-style($m-blue, $m-blue);
|
||||
@include icon-style("\f02e", $m-blue);
|
||||
@include hover-style($light-gray, $black, "\f097");
|
||||
}
|
||||
|
||||
.bookmark-button:not(.bookmarked) {
|
||||
@include base-style($light-gray, $black);
|
||||
@include icon-style("\f097", $black);
|
||||
@include hover-style($m-blue, $m-blue, "\f02e");
|
||||
}
|
||||
|
||||
11
lms/templates/bookmark_button.html
Normal file
11
lms/templates/bookmark_button.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
<%page args="bookmark_id, is_bookmarked" />
|
||||
|
||||
<div class="bookmark-button-wrapper">
|
||||
<button class="btn bookmark-button ${"bookmarked" if is_bookmarked else ""}"
|
||||
aria-pressed="${"true" if is_bookmarked else "false"}"
|
||||
data-bookmark-id="${bookmark_id}">
|
||||
<span class="sr bookmark-sr">${_("Click to remove") if is_bookmarked else _("Click to add")}</span>
|
||||
${_("Bookmark")}
|
||||
</button>
|
||||
</div>
|
||||
@@ -24,6 +24,13 @@ ${page_title_breadcrumbs(course_name())}
|
||||
</title></%block>
|
||||
|
||||
<%block name="header_extras">
|
||||
|
||||
% for template_name in ["message_banner"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="fields/${template_name}.underscore" />
|
||||
</script>
|
||||
% endfor
|
||||
|
||||
% for template_name in ["image-modal"]:
|
||||
<script type="text/template" id="${template_name}-tpl">
|
||||
<%static:include path="common/templates/${template_name}.underscore" />
|
||||
@@ -124,6 +131,8 @@ ${fragment.foot_html()}
|
||||
|
||||
</%block>
|
||||
|
||||
<div class="coursewide-message-banner" aria-live="polite"></div>
|
||||
|
||||
% if default_tab:
|
||||
<%include file="/courseware/course_navigation.html" />
|
||||
% else:
|
||||
@@ -149,7 +158,7 @@ ${fragment.foot_html()}
|
||||
|
||||
<div class="wrapper-course-modes">
|
||||
|
||||
<div class="courseware-bookmarks-button">
|
||||
<div class="courseware-bookmarks-button" data-bookmarks-api-url="${bookmarks_api_url}">
|
||||
<button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false">
|
||||
${_('Bookmarks')}
|
||||
</button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<%! from django.utils.translation import ugettext as _ %>
|
||||
|
||||
<div id="sequence_${element_id}" class="sequence" data-id="${item_id}" data-position="${position}" data-ajax-url="${ajax_url}" >
|
||||
<div class="path"></div>
|
||||
<div class="sequence-nav">
|
||||
<button class="sequence-nav-button button-previous">
|
||||
<span class="icon fa fa-chevron-prev" aria-hidden="true"></span><span class="sr">${_('Previous')}</span>
|
||||
@@ -13,16 +14,18 @@
|
||||
## implementation note: will need to figure out how to handle combining detail
|
||||
## statuses of multiple modules in js.
|
||||
<li>
|
||||
<a class="seq_${item['type']} inactive progress-${item['progress_status']}"
|
||||
<a class="seq_${item['type']} inactive progress-${item['progress_status']} nav-item"
|
||||
data-id="${item['id']}"
|
||||
data-element="${idx+1}"
|
||||
href="javascript:void(0);"
|
||||
data-page-title="${item['page_title']|h}"
|
||||
data-path="${item['path']}"
|
||||
aria-controls="seq_contents_${idx}"
|
||||
id="tab_${idx}"
|
||||
tabindex="0">
|
||||
<i class="icon fa seq_${item['type']}" aria-hidden="true"></i>
|
||||
<p><span class="sr">${item['type']}</span>${item['title']}</p>
|
||||
<i class="fa fa-fw fa-bookmark bookmark-icon ${"is-hidden" if not item['bookmarked'] else "bookmarked"}" aria-hidden="true"></i>
|
||||
<p><span class="sr">${item['type']}</span> ${item['title']}<span class="sr bookmark-icon-sr"> ${_("Bookmarked") if item['bookmarked'] else ""}</span></p>
|
||||
</a>
|
||||
</li>
|
||||
% endfor
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
|
||||
% if show_bookmark_button:
|
||||
<%include file='bookmark_button.html' args="bookmark_id=bookmark_id, is_bookmarked=bookmarked"/>
|
||||
% endif
|
||||
|
||||
<div class="vert-mod">
|
||||
% for idx, item in enumerate(items):
|
||||
<div class="vert vert-${idx}" data-id="${item['id']}">
|
||||
|
||||
Reference in New Issue
Block a user