Commit 3cbbb8f3 by muzaffaryousaf

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

TNL-1957
parent c9b87aa0
$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
......
/* 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):
"""
......
......@@ -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;
......
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();
});
;(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)}),
......
<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">
Bookmarks
</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',
'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;
}
\ 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())}
</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>
......
<%! 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">&nbsp;${_("Bookmarked") if item['bookmarked'] else ""}</span></p>
</a>
</li>
% endfor
......
% 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']}">
......
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