Commit 3cbbb8f3 by muzaffaryousaf

Add/Remove Bookmark button to each unit in LMS courseware.

TNL-1957
parent c9b87aa0
$sequence--border-color: #C8C8C8; $sequence--border-color: #C8C8C8;
$link-color: rgb(26, 161, 222);
// repeated extends - needed since LMS styling was referenced // repeated extends - needed since LMS styling was referenced
.block-link { .block-link {
border-left: 1px solid lighten($sequence--border-color, 10%); border-left: 1px solid lighten($sequence--border-color, 10%);
...@@ -36,7 +36,7 @@ $sequence--border-color: #C8C8C8; ...@@ -36,7 +36,7 @@ $sequence--border-color: #C8C8C8;
// TODO (cpennington): This doesn't work anymore. XModules aren't able to // TODO (cpennington): This doesn't work anymore. XModules aren't able to
// import from external sources. // import from external sources.
@extend .topbar; @extend .topbar;
margin: -4px 0 ($baseline*1.5); margin: -4px 0 $baseline;
position: relative; position: relative;
border-bottom: none; border-bottom: none;
z-index: 0; z-index: 0;
...@@ -119,6 +119,10 @@ $sequence--border-color: #C8C8C8; ...@@ -119,6 +119,10 @@ $sequence--border-color: #C8C8C8;
-webkit-font-smoothing: antialiased; // Clear up the lines on the icons -webkit-font-smoothing: antialiased; // Clear up the lines on the icons
} }
i.fa-bookmark {
color: $link-color;
}
&.inactive { &.inactive {
.icon { .icon {
...@@ -142,6 +146,10 @@ $sequence--border-color: #C8C8C8; ...@@ -142,6 +146,10 @@ $sequence--border-color: #C8C8C8;
.icon { .icon {
color: rgb(10, 10, 10); color: rgb(10, 10, 10);
} }
i.fa-bookmark {
color: $link-color;
}
} }
} }
...@@ -295,3 +303,4 @@ nav.sequence-bottom { ...@@ -295,3 +303,4 @@ nav.sequence-bottom {
outline: none; outline: none;
} }
} }
...@@ -18,6 +18,8 @@ class @Sequence ...@@ -18,6 +18,8 @@ class @Sequence
bind: -> bind: ->
@$('#sequence-list a').click @goto @$('#sequence-list a').click @goto
@el.on 'bookmark:add', @addBookmarkIconToActiveNavItem
@el.on 'bookmark:remove', @removeBookmarkIconFromActiveNavItem
initProgress: -> initProgress: ->
@progressTable = {} # "#problem_#{id}" -> progress @progressTable = {} # "#problem_#{id}" -> progress
...@@ -102,8 +104,9 @@ class @Sequence ...@@ -102,8 +104,9 @@ class @Sequence
@mark_active new_position @mark_active new_position
current_tab = @contents.eq(new_position - 1) 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) XBlock.initializeBlocks(@content_container, @requestToken)
window.update_schematics() # For embedded circuit simulator exercises in 6.002x window.update_schematics() # For embedded circuit simulator exercises in 6.002x
...@@ -116,6 +119,8 @@ class @Sequence ...@@ -116,6 +119,8 @@ class @Sequence
sequence_links = @content_container.find('a.seqnav') sequence_links = @content_container.find('a.seqnav')
sequence_links.click @goto sequence_links.click @goto
@el.find('.path').html(@el.find('.nav-item.active').data('path'))
@sr_container.focus(); @sr_container.focus();
# @$("a.active").blur() # @$("a.active").blur()
...@@ -180,3 +185,13 @@ class @Sequence ...@@ -180,3 +185,13 @@ class @Sequence
element.removeClass("inactive") element.removeClass("inactive")
.removeClass("visited") .removeClass("visited")
.addClass("active") .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): ...@@ -316,6 +316,7 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule):
fragment.add_content(self.system.render_template('vert_module.html', { fragment.add_content(self.system.render_template('vert_module.html', {
'items': contents, 'items': contents,
'xblock_context': context, 'xblock_context': context,
'show_bookmark_button': False,
})) }))
return fragment return fragment
......
/* 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): ...@@ -55,7 +55,6 @@ class SequenceFields(object):
scope=Scope.settings, scope=Scope.settings,
) )
class ProctoringFields(object): class ProctoringFields(object):
""" """
Fields that are specific to Proctored or Timed Exams Fields that are specific to Proctored or Timed Exams
...@@ -119,9 +118,12 @@ class ProctoringFields(object): ...@@ -119,9 +118,12 @@ class ProctoringFields(object):
@XBlock.wants('proctoring') @XBlock.wants('proctoring')
@XBlock.wants('credit') @XBlock.wants('credit')
class SequenceModule(SequenceFields, ProctoringFields, XModule): @XBlock.needs("user")
''' Layout module which lays out content in a temporal sequence @XBlock.needs("bookmarks")
''' class SequenceModule(SequenceFields, XModule):
"""
Layout module which lays out content in a temporal sequence
"""
js = { js = {
'coffee': [resource_string(__name__, 'js/src/sequence/display.coffee')], 'coffee': [resource_string(__name__, 'js/src/sequence/display.coffee')],
'js': [resource_string(__name__, 'js/src/sequence/display/jquery.sequence.js')], 'js': [resource_string(__name__, 'js/src/sequence/display/jquery.sequence.js')],
...@@ -182,7 +184,12 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -182,7 +184,12 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
contents = [] contents = []
fragment = Fragment() 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? # Is this sequential part of a timed or proctored exam?
if self.is_time_limited: if self.is_time_limited:
view_html = self._time_limited_student_view(context) view_html = self._time_limited_student_view(context)
...@@ -194,6 +201,9 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -194,6 +201,9 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
return fragment return fragment
for child in self.get_display_items(): 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() progress = child.get_progress()
rendered_child = child.render(STUDENT_VIEW, context) rendered_child = child.render(STUDENT_VIEW, context)
fragment.add_frag_resources(rendered_child) fragment.add_frag_resources(rendered_child)
...@@ -209,6 +219,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule): ...@@ -209,6 +219,8 @@ class SequenceModule(SequenceFields, ProctoringFields, XModule):
'progress_detail': Progress.to_js_detail_str(progress), 'progress_detail': Progress.to_js_detail_str(progress),
'type': child.get_icon_class(), 'type': child.get_icon_class(),
'id': child.scope_ids.usage_id.to_deprecated_string(), 'id': child.scope_ids.usage_id.to_deprecated_string(),
'bookmarked': is_bookmarked,
'path': " > ".join(display_names + [child.display_name or '']),
} }
if childinfo['title'] == '': if childinfo['title'] == '':
childinfo['title'] = child.display_name_with_default childinfo['title'] = child.display_name_with_default
......
...@@ -37,18 +37,31 @@ class BaseVerticalBlockTest(XModuleXmlImportTest): ...@@ -37,18 +37,31 @@ class BaseVerticalBlockTest(XModuleXmlImportTest):
self.vertical = course_seq.get_children()[0] self.vertical = course_seq.get_children()[0]
self.vertical.xmodule_runtime = self.module_system self.vertical.xmodule_runtime = self.module_system
self.username = "bilbo"
self.default_context = {"bookmarked": False, "username": self.username}
class VerticalBlockTestCase(BaseVerticalBlockTest): class VerticalBlockTestCase(BaseVerticalBlockTest):
""" """
Tests for the VerticalBlock. 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): def test_render_student_view(self):
""" """
Test the rendering of the student view. 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_1, html)
self.assertIn(self.test_html_2, html) self.assertIn(self.test_html_2, html)
self.assert_bookmark_info_in(html)
def test_render_studio_view(self): def test_render_studio_view(self):
""" """
......
...@@ -54,7 +54,14 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse ...@@ -54,7 +54,14 @@ class VerticalBlock(SequenceFields, XModuleFields, StudioEditableBlock, XmlParse
fragment.add_content(self.system.render_template('vert_module.html', { fragment.add_content(self.system.render_template('vert_module.html', {
'items': contents, 'items': contents,
'xblock_context': context, '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 return fragment
def author_view(self, context): def author_view(self, context):
......
...@@ -7,7 +7,7 @@ from .course_page import CoursePage ...@@ -7,7 +7,7 @@ from .course_page import CoursePage
class BookmarksPage(CoursePage): class BookmarksPage(CoursePage):
""" """
Coursware Bookmarks Page. Courseware Bookmarks Page.
""" """
url = None url = None
url_path = "courseware/" url_path = "courseware/"
...@@ -23,10 +23,11 @@ class BookmarksPage(CoursePage): ...@@ -23,10 +23,11 @@ class BookmarksPage(CoursePage):
""" 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): def click_bookmarks_button(self, wait_for_results=True):
""" Click on Bookmarks button """ """ Click on Bookmarks button """
self.q(css=self.BOOKMARKS_BUTTON_SELECTOR).first.click() 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): def results_present(self):
""" Check if bookmarks results are present """ """ Check if bookmarks results are present """
...@@ -53,9 +54,9 @@ class BookmarksPage(CoursePage): ...@@ -53,9 +54,9 @@ class BookmarksPage(CoursePage):
breadcrumbs = self.q(css=self.BOOKMARKED_BREADCRUMBS).text breadcrumbs = self.q(css=self.BOOKMARKED_BREADCRUMBS).text
return [breadcrumb.replace('\n', '').split('-') for breadcrumb in breadcrumbs] 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: Arguments:
index (int): bookmark index in the list index (int): bookmark index in the list
......
...@@ -193,13 +193,13 @@ class CourseNavPage(PageObject): ...@@ -193,13 +193,13 @@ class CourseNavPage(PageObject):
) )
# Regular expression to remove HTML span tags from a string # 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): def _clean_seq_titles(self, element):
""" """
Clean HTML of sequence titles, stripping out span tags and returning the first line. 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): def go_to_sequential_position(self, sequential_position):
""" """
......
...@@ -3,6 +3,7 @@ Courseware page. ...@@ -3,6 +3,7 @@ Courseware page.
""" """
from .course_page import CoursePage from .course_page import CoursePage
from bok_choy.promise import EmptyPromise
from selenium.webdriver.common.action_chains import ActionChains from selenium.webdriver.common.action_chains import ActionChains
...@@ -177,6 +178,32 @@ class CoursewarePage(CoursePage): ...@@ -177,6 +178,32 @@ class CoursewarePage(CoursePage):
attribute_value = lambda el: el.get_attribute('data-id') attribute_value = lambda el: el.get_attribute('data-id')
return self.q(css='#sequence-list a').filter(get_active).map(attribute_value).results[0] 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): class CoursewareSequentialTabPage(CoursePage):
""" """
......
...@@ -29,6 +29,7 @@ class CoursewareTest(UniqueCourseTest): ...@@ -29,6 +29,7 @@ class CoursewareTest(UniqueCourseTest):
super(CoursewareTest, self).setUp() super(CoursewareTest, self).setUp()
self.courseware_page = CoursewarePage(self.browser, self.course_id) self.courseware_page = CoursewarePage(self.browser, self.course_id)
self.course_nav = CourseNavPage(self.browser)
self.course_outline = CourseOutlinePage( self.course_outline = CourseOutlinePage(
self.browser, self.browser,
...@@ -38,12 +39,12 @@ class CoursewareTest(UniqueCourseTest): ...@@ -38,12 +39,12 @@ class CoursewareTest(UniqueCourseTest):
) )
# Install a course with sections/problems, tabs, updates, and handouts # 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['org'], self.course_info['number'],
self.course_info['run'], self.course_info['display_name'] 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('chapter', 'Test Section 1').add_children(
XBlockFixtureDesc('sequential', 'Test Subsection 1').add_children( XBlockFixtureDesc('sequential', 'Test Subsection 1').add_children(
XBlockFixtureDesc('problem', 'Test Problem 1') XBlockFixtureDesc('problem', 'Test Problem 1')
...@@ -67,6 +68,18 @@ class CoursewareTest(UniqueCourseTest): ...@@ -67,6 +68,18 @@ class CoursewareTest(UniqueCourseTest):
self.problem_page = ProblemPage(self.browser) self.problem_page = ProblemPage(self.browser)
self.assertEqual(self.problem_page.problem_name, 'TEST PROBLEM 1') 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): def _auto_auth(self, username, email, staff):
""" """
Logout and login with given credentials. Logout and login with given credentials.
...@@ -92,6 +105,9 @@ class CoursewareTest(UniqueCourseTest): ...@@ -92,6 +105,9 @@ class CoursewareTest(UniqueCourseTest):
# Set release date for subsection in future. # Set release date for subsection in future.
self.course_outline.change_problem_release_date_in_studio() 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. # Logout and login as a student.
LogoutPage(self.browser).visit() LogoutPage(self.browser).visit()
self._auto_auth(self.USERNAME, self.EMAIL, False) self._auto_auth(self.USERNAME, self.EMAIL, False)
...@@ -246,6 +262,23 @@ class ProctoredExamTest(UniqueCourseTest): ...@@ -246,6 +262,23 @@ class ProctoredExamTest(UniqueCourseTest):
self.courseware_page.start_timed_exam() self.courseware_page.start_timed_exam()
self.assertTrue(self.courseware_page.is_timer_bar_present) 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): def test_time_allotted_field_is_not_visible_with_none_exam(self):
""" """
Given that I am a staff member Given that I am a staff member
......
...@@ -145,8 +145,8 @@ class BookmarksViewTestsMixin(ModuleStoreTestCase): ...@@ -145,8 +145,8 @@ class BookmarksViewTestsMixin(ModuleStoreTestCase):
class BookmarksListViewTests(BookmarksViewTestsMixin): class BookmarksListViewTests(BookmarksViewTestsMixin):
""" """
This contains the tests for GET & POST methods of bookmark.views.BookmarksListView class This contains the tests for GET & POST methods of bookmark.views.BookmarksListView class
GET /api/bookmarks/v0/bookmarks/?course_id={course_id1} GET /api/bookmarks/v1/bookmarks/?course_id={course_id1}
POST /api/bookmarks/v0/bookmarks POST /api/bookmarks/v1/bookmarks
""" """
@ddt.data( @ddt.data(
('course_id={}', False), ('course_id={}', False),
......
...@@ -42,6 +42,7 @@ from courseware.entrance_exams import ( ...@@ -42,6 +42,7 @@ from courseware.entrance_exams import (
) )
from edxmako.shortcuts import render_to_string from edxmako.shortcuts import render_to_string
from eventtracking import tracker 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.field_data import LmsFieldData
from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes from lms.djangoapps.lms_xblock.runtime import LmsModuleSystem, unquote_slashes, quote_slashes
from lms.djangoapps.lms_xblock.models import XBlockAsidesConfig 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 ...@@ -715,6 +716,8 @@ def get_module_system_for_user(user, student_data, # TODO # pylint: disable=to
"reverification": ReverificationService(), "reverification": ReverificationService(),
'proctoring': ProctoringService(), 'proctoring': ProctoringService(),
'credit': CreditService(), 'credit': CreditService(),
'reverification': ReverificationService(),
'bookmarks': BookmarksService(user=user),
}, },
get_user_role=lambda: get_user_role(user, course_id), get_user_role=lambda: get_user_role(user, course_id),
descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access descriptor_runtime=descriptor._runtime, # pylint: disable=protected-access
......
...@@ -73,6 +73,7 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT ...@@ -73,6 +73,7 @@ TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
@XBlock.needs("i18n") @XBlock.needs("i18n")
@XBlock.needs("fs") @XBlock.needs("fs")
@XBlock.needs("user") @XBlock.needs("user")
@XBlock.needs("bookmarks")
class PureXBlock(XBlock): class PureXBlock(XBlock):
""" """
Pure XBlock to use in tests. Pure XBlock to use in tests.
...@@ -1232,6 +1233,7 @@ class ViewInStudioTest(ModuleStoreTestCase): ...@@ -1232,6 +1233,7 @@ class ViewInStudioTest(ModuleStoreTestCase):
self.request.user = self.staff_user self.request.user = self.staff_user
self.request.session = {} self.request.session = {}
self.module = None self.module = None
self.default_context = {'bookmarked': False, 'username': self.user.username}
def _get_module(self, course_id, descriptor, location): def _get_module(self, course_id, descriptor, location):
""" """
...@@ -1290,14 +1292,14 @@ class MongoViewInStudioTest(ViewInStudioTest): ...@@ -1290,14 +1292,14 @@ class MongoViewInStudioTest(ViewInStudioTest):
def test_view_in_studio_link_studio_course(self): def test_view_in_studio_link_studio_course(self):
"""Regular Studio courses should see 'View in Studio' links.""" """Regular Studio courses should see 'View in Studio' links."""
self.setup_mongo_course() 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) self.assertIn('View Unit in Studio', result_fragment.content)
def test_view_in_studio_link_only_in_top_level_vertical(self): 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.""" """Regular Studio courses should not see 'View in Studio' for child verticals of verticals."""
self.setup_mongo_course() self.setup_mongo_course()
# Render the parent vertical, then check that there is only a single "View Unit in Studio" link. # 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. # The single "View Unit in Studio" link should appear before the first xmodule vertical definition.
parts = result_fragment.content.split('data-block-type="vertical"') parts = result_fragment.content.split('data-block-type="vertical"')
self.assertEqual(3, len(parts), "Did not find two vertical blocks") self.assertEqual(3, len(parts), "Did not find two vertical blocks")
...@@ -1308,7 +1310,7 @@ class MongoViewInStudioTest(ViewInStudioTest): ...@@ -1308,7 +1310,7 @@ class MongoViewInStudioTest(ViewInStudioTest):
def test_view_in_studio_link_xml_authored(self): def test_view_in_studio_link_xml_authored(self):
"""Courses that change 'course_edit_method' setting can hide 'View in Studio' links.""" """Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
self.setup_mongo_course(course_edit_method='XML') 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) self.assertNotIn('View Unit in Studio', result_fragment.content)
...@@ -1321,19 +1323,19 @@ class MixedViewInStudioTest(ViewInStudioTest): ...@@ -1321,19 +1323,19 @@ class MixedViewInStudioTest(ViewInStudioTest):
def test_view_in_studio_link_mongo_backed(self): def test_view_in_studio_link_mongo_backed(self):
"""Mixed mongo courses that are mongo backed should see 'View in Studio' links.""" """Mixed mongo courses that are mongo backed should see 'View in Studio' links."""
self.setup_mongo_course() 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) self.assertIn('View Unit in Studio', result_fragment.content)
def test_view_in_studio_link_xml_authored(self): def test_view_in_studio_link_xml_authored(self):
"""Courses that change 'course_edit_method' setting can hide 'View in Studio' links.""" """Courses that change 'course_edit_method' setting can hide 'View in Studio' links."""
self.setup_mongo_course(course_edit_method='XML') 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) self.assertNotIn('View Unit in Studio', result_fragment.content)
def test_view_in_studio_link_xml_backed(self): def test_view_in_studio_link_xml_backed(self):
"""Course in XML only modulestore should not see 'View in Studio' links.""" """Course in XML only modulestore should not see 'View in Studio' links."""
self.setup_xml_course() 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) self.assertNotIn('View Unit in Studio', result_fragment.content)
...@@ -1826,7 +1828,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase): ...@@ -1826,7 +1828,7 @@ class LMSXBlockServiceBindingTest(ModuleStoreTestCase):
self.request_token = Mock() self.request_token = Mock()
@XBlock.register_temp_plugin(PureXBlock, identifier='pure') @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): def test_expected_services_exist(self, expected_service):
""" """
Tests that the 'user', 'i18n', and 'fs' services are provided by the LMS runtime. Tests that the 'user', 'i18n', and 'fs' services are provided by the LMS runtime.
......
...@@ -119,7 +119,7 @@ class SplitTestBase(ModuleStoreTestCase): ...@@ -119,7 +119,7 @@ class SplitTestBase(ModuleStoreTestCase):
content = resp.content content = resp.content
# Assert we see the proper icon in the top display # 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 # And proper tooltips
for tooltip in self.TOOLTIPS[user_tag]: for tooltip in self.TOOLTIPS[user_tag]:
self.assertIn(tooltip, content) self.assertIn(tooltip, content)
......
...@@ -403,6 +403,8 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -403,6 +403,8 @@ def _index_bulk_op(request, course_key, chapter, section, position):
if survey.utils.must_answer_survey(course, user): if survey.utils.must_answer_survey(course, user):
return redirect(reverse('course_survey', args=[unicode(course.id)])) return redirect(reverse('course_survey', args=[unicode(course.id)]))
bookmarks_api_url = reverse('bookmarks')
try: try:
field_data_cache = FieldDataCache.cache_for_descriptor_descendents( field_data_cache = FieldDataCache.cache_for_descriptor_descendents(
course_key, user, course, depth=2) course_key, user, course, depth=2)
...@@ -428,7 +430,7 @@ def _index_bulk_op(request, course_key, chapter, section, position): ...@@ -428,7 +430,7 @@ def _index_bulk_op(request, course_key, chapter, section, position):
'studio_url': studio_url, 'studio_url': studio_url,
'masquerade': masquerade, 'masquerade': masquerade,
'xqa_server': settings.FEATURES.get('XQA_SERVER', "http://your_xqa_server.com"), '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, 'language_preference': language_preference,
} }
......
...@@ -5,8 +5,11 @@ ...@@ -5,8 +5,11 @@
'use strict'; 'use strict';
return Backbone.Collection.extend({ return Backbone.Collection.extend({
model : BookmarkModel, model: BookmarkModel,
url: '/api/bookmarks/v0/bookmarks/',
url: function() {
return $(".courseware-bookmarks-button").data('bookmarksApiUrl');
},
parse: function(response) { parse: function(response) {
return response.results; return response.results;
......
RequireJS.require([ RequireJS.require([
'js/bookmarks/views/bookmarks_button' 'js/bookmarks/views/bookmarks_list_button'
], function (BookmarksButton) { ], function (BookmarksListButton) {
'use strict'; 'use strict';
return new BookmarksButton(); return new BookmarksListButton();
}); });
;(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 @@ ...@@ -16,6 +16,8 @@
errorMessage: gettext('An error has occurred. Please try again.'), errorMessage: gettext('An error has occurred. Please try again.'),
loadingMessage: gettext('Loading'), loadingMessage: gettext('Loading'),
PAGE_SIZE: 500,
events : { events : {
'click .bookmarks-results-list-item': 'visitBookmark' 'click .bookmarks-results-list-item': 'visitBookmark'
}, },
...@@ -48,7 +50,7 @@ ...@@ -48,7 +50,7 @@
this.collection.fetch({ this.collection.fetch({
reset: true, 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 () { }).done(function () {
view.hideLoadingMessage(); view.hideLoadingMessage();
view.render(); view.render();
......
...@@ -16,8 +16,6 @@ ...@@ -16,8 +16,6 @@
}, },
initialize: function () { initialize: function () {
this.template = _.template($('#bookmarks_button-tpl').text());
this.bookmarksListView = new BookmarksListView({ this.bookmarksListView = new BookmarksListView({
collection: new BookmarksCollection(), collection: new BookmarksCollection(),
loadingMessageView: new MessageView({el: $(this.loadingMessageElement)}), loadingMessageView: new MessageView({el: $(this.loadingMessageElement)}),
......
<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>&nbsp;
Bookmark
</button>
</div>
</div>
<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"> <button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false">
Bookmarks Bookmarks
</button> </button>
......
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);
});
});
});
define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'js/common_helpers/template_helpers', 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'; 'use strict';
describe("lms.courseware.bookmarks", function () { describe("lms.courseware.bookmarks", function () {
var bookmarksButtonView; var bookmarksButtonView;
var BOOKMARKS_API_URL = '/api/bookmarks/v0/bookmarks/'; var BOOKMARKS_API_URL = '/api/bookmarks/v1/bookmarks/';
beforeEach(function () { beforeEach(function () {
loadFixtures('js/fixtures/bookmarks/bookmarks.html'); loadFixtures('js/fixtures/bookmarks/bookmarks.html');
...@@ -18,7 +18,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -18,7 +18,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
] ]
); );
bookmarksButtonView = new BookmarksButtonView(); bookmarksButtonView = new BookmarksListButtonView();
this.addMatchers({ this.addMatchers({
toHaveBeenCalledWithUrl: function (expectedUrl) { toHaveBeenCalledWithUrl: function (expectedUrl) {
...@@ -124,7 +124,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j ...@@ -124,7 +124,7 @@ define(['backbone', 'jquery', 'underscore', 'js/common_helpers/ajax_helpers', 'j
it("has rendered bookmarked list correctly", function () { it("has rendered bookmarked list correctly", function () {
var requests = AjaxHelpers.requests(this); 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); var expectedData = createBookmarksData(3);
spyOn(bookmarksButtonView.bookmarksListView, 'courseId').andReturn('COURSE_ID'); spyOn(bookmarksButtonView.bookmarksListView, 'courseId').andReturn('COURSE_ID');
......
...@@ -87,6 +87,13 @@ ...@@ -87,6 +87,13 @@
// Discussion classes loaded explicitly until they are converted to use RequireJS // Discussion classes loaded explicitly until they are converted to use RequireJS
'DiscussionModuleView': 'xmodule_js/common_static/coffee/src/discussion/discussion_module_view', '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 // edxnotes
'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min', 'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min',
...@@ -738,7 +745,8 @@ ...@@ -738,7 +745,8 @@
'lms/include/teams/js/spec/views/team_join_spec.js' 'lms/include/teams/js/spec/views/team_join_spec.js'
'lms/include/js/spec/discovery/discovery_spec.js', 'lms/include/js/spec/discovery/discovery_spec.js',
'lms/include/js/spec/ccx/schedule_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); }).call(this, requirejs, define);
...@@ -403,6 +403,10 @@ div.course-wrapper { ...@@ -403,6 +403,10 @@ div.course-wrapper {
} }
} }
.sequence .path {
margin-bottom: ($baseline/2);
}
div#seq_content { div#seq_content {
h1 { h1 {
background: none; background: none;
......
...@@ -67,8 +67,8 @@ ...@@ -67,8 +67,8 @@
margin-bottom: $baseline; margin-bottom: $baseline;
&:hover { &:hover {
border-color: $link-color; border-color: $m-blue;
color: $link-color; color: $m-blue;
} }
} }
...@@ -87,6 +87,7 @@ ...@@ -87,6 +87,7 @@
position: relative; position: relative;
top: -7px; top: -7px;
font-family: FontAwesome; font-family: FontAwesome;
color: $m-blue;
} }
.list-item-content { .list-item-content {
...@@ -124,4 +125,59 @@ ...@@ -124,4 +125,59 @@
.bookmarks-empty-detail { .bookmarks-empty-detail {
@extend %t-copy-sub1; @extend %t-copy-sub1;
} }
\ No newline at end of file
// 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");
}
<%! 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>&nbsp;
${_("Bookmark")}
</button>
</div>
...@@ -24,6 +24,13 @@ ${page_title_breadcrumbs(course_name())} ...@@ -24,6 +24,13 @@ ${page_title_breadcrumbs(course_name())}
</title></%block> </title></%block>
<%block name="header_extras"> <%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"]: % for template_name in ["image-modal"]:
<script type="text/template" id="${template_name}-tpl"> <script type="text/template" id="${template_name}-tpl">
<%static:include path="common/templates/${template_name}.underscore" /> <%static:include path="common/templates/${template_name}.underscore" />
...@@ -124,6 +131,8 @@ ${fragment.foot_html()} ...@@ -124,6 +131,8 @@ ${fragment.foot_html()}
</%block> </%block>
<div class="coursewide-message-banner" aria-live="polite"></div>
% if default_tab: % if default_tab:
<%include file="/courseware/course_navigation.html" /> <%include file="/courseware/course_navigation.html" />
% else: % else:
...@@ -149,7 +158,7 @@ ${fragment.foot_html()} ...@@ -149,7 +158,7 @@ ${fragment.foot_html()}
<div class="wrapper-course-modes"> <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"> <button type="button" class="bookmarks-list-button is-inactive" aria-pressed="false">
${_('Bookmarks')} ${_('Bookmarks')}
</button> </button>
......
<%! from django.utils.translation import ugettext as _ %> <%! 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 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"> <div class="sequence-nav">
<button class="sequence-nav-button button-previous"> <button class="sequence-nav-button button-previous">
<span class="icon fa fa-chevron-prev" aria-hidden="true"></span><span class="sr">${_('Previous')}</span> <span class="icon fa fa-chevron-prev" aria-hidden="true"></span><span class="sr">${_('Previous')}</span>
...@@ -13,16 +14,18 @@ ...@@ -13,16 +14,18 @@
## implementation note: will need to figure out how to handle combining detail ## implementation note: will need to figure out how to handle combining detail
## statuses of multiple modules in js. ## statuses of multiple modules in js.
<li> <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-id="${item['id']}"
data-element="${idx+1}" data-element="${idx+1}"
href="javascript:void(0);" href="javascript:void(0);"
data-page-title="${item['page_title']|h}" data-page-title="${item['page_title']|h}"
data-path="${item['path']}"
aria-controls="seq_contents_${idx}" aria-controls="seq_contents_${idx}"
id="tab_${idx}" id="tab_${idx}"
tabindex="0"> tabindex="0">
<i class="icon fa seq_${item['type']}" aria-hidden="true"></i> <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">&nbsp;${_("Bookmarked") if item['bookmarked'] else ""}</span></p>
</a> </a>
</li> </li>
% endfor % endfor
......
% if show_bookmark_button:
<%include file='bookmark_button.html' args="bookmark_id=bookmark_id, is_bookmarked=bookmarked"/>
% endif
<div class="vert-mod"> <div class="vert-mod">
% for idx, item in enumerate(items): % for idx, item in enumerate(items):
<div class="vert vert-${idx}" data-id="${item['id']}"> <div class="vert vert-${idx}" data-id="${item['id']}">
......
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