diff --git a/cms/envs/common.py b/cms/envs/common.py index ce1d565bba..1b734c5433 100644 --- a/cms/envs/common.py +++ b/cms/envs/common.py @@ -803,6 +803,9 @@ INSTALLED_APPS = ( # edX Proctoring 'edx_proctoring', + # Bookmarks + 'openedx.core.djangoapps.bookmarks', + # programs support 'openedx.core.djangoapps.programs', diff --git a/common/djangoapps/enrollment/urls.py b/common/djangoapps/enrollment/urls.py index 438ad259df..8b7b41652a 100644 --- a/common/djangoapps/enrollment/urls.py +++ b/common/djangoapps/enrollment/urls.py @@ -11,13 +11,13 @@ from .views import ( EnrollmentCourseDetailView ) -USERNAME_PATTERN = '(?P[\w.@+-]+)' urlpatterns = patterns( 'enrollment.views', url( - r'^enrollment/{username},{course_key}$'.format(username=USERNAME_PATTERN, - course_key=settings.COURSE_ID_PATTERN), + r'^enrollment/{username},{course_key}$'.format( + username=settings.USERNAME_PATTERN, course_key=settings.COURSE_ID_PATTERN + ), EnrollmentView.as_view(), name='courseenrollment' ), diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index d184499477..c86ff553f5 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -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; } } + diff --git a/common/lib/xmodule/xmodule/js/src/sequence/display.coffee b/common/lib/xmodule/xmodule/js/src/sequence/display.coffee index 959ed75343..b5445a4c03 100644 --- a/common/lib/xmodule/xmodule/js/src/sequence/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/sequence/display.coffee @@ -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('') diff --git a/common/lib/xmodule/xmodule/library_content_module.py b/common/lib/xmodule/xmodule/library_content_module.py index 9eae5e4ec9..9a5593274e 100644 --- a/common/lib/xmodule/xmodule/library_content_module.py +++ b/common/lib/xmodule/xmodule/library_content_module.py @@ -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 diff --git a/common/lib/xmodule/xmodule/modulestore/search.py b/common/lib/xmodule/xmodule/modulestore/search.py index 0ee4e37d41..307ed51a05 100644 --- a/common/lib/xmodule/xmodule/modulestore/search.py +++ b/common/lib/xmodule/xmodule/modulestore/search.py @@ -6,7 +6,7 @@ from .exceptions import (ItemNotFoundError, NoPathToItem) LOGGER = getLogger(__name__) -def path_to_location(modulestore, usage_key): +def path_to_location(modulestore, usage_key, full_path=False): ''' Try to find a course_id/chapter/section[/position] path to location in modulestore. The courseware insists that the first level in the course is @@ -15,6 +15,7 @@ def path_to_location(modulestore, usage_key): Args: modulestore: which store holds the relevant objects usage_key: :class:`UsageKey` the id of the location to which to generate the path + full_path: :class:`Bool` if True, return the full path to location. Default is False. Raises ItemNotFoundError if the location doesn't exist. @@ -81,6 +82,9 @@ def path_to_location(modulestore, usage_key): if path is None: raise NoPathToItem(usage_key) + if full_path: + return path + n = len(path) course_id = path[0].course_key # pull out the location names diff --git a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py index 224a1dbc22..8d3224db2a 100644 --- a/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py +++ b/common/lib/xmodule/xmodule/modulestore/tests/django_utils.py @@ -18,10 +18,12 @@ from openedx.core.lib.tempdir import mkdtemp_clean from xmodule.contentstore.django import _CONTENTSTORE from xmodule.modulestore import ModuleStoreEnum -from xmodule.modulestore.django import modulestore, clear_existing_modulestores +from xmodule.modulestore.django import modulestore, clear_existing_modulestores, SignalHandler from xmodule.modulestore.tests.mongo_connection import MONGO_PORT_NUM, MONGO_HOST from xmodule.modulestore.tests.factories import XMODULE_FACTORY_LOCK +from openedx.core.djangoapps.bookmarks.signals import trigger_update_xblocks_cache_task + class StoreConstructors(object): """Enumeration of store constructor types.""" @@ -405,6 +407,8 @@ class ModuleStoreTestCase(TestCase): super(ModuleStoreTestCase, self).setUp() + SignalHandler.course_published.disconnect(trigger_update_xblocks_cache_task) + self.store = modulestore() uname = 'testuser' diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py index 7cd2c615fe..a0033951f5 100644 --- a/common/lib/xmodule/xmodule/modulestore/xml.py +++ b/common/lib/xmodule/xmodule/modulestore/xml.py @@ -917,6 +917,14 @@ class XMLModuleStore(ModuleStoreReadBase): log.warning("get_all_asset_metadata request of XML modulestore - not implemented.") return [] + def fill_in_run(self, course_key): + """ + A no-op. + + Added to simplify tests which use the XML-store directly. + """ + return course_key + class LibraryXMLModuleStore(XMLModuleStore): """ diff --git a/common/lib/xmodule/xmodule/public/js/vertical_student_view.js b/common/lib/xmodule/xmodule/public/js/vertical_student_view.js new file mode 100644 index 0000000000..7efd1342d0 --- /dev/null +++ b/common/lib/xmodule/xmodule/public/js/vertical_student_view.js @@ -0,0 +1,17 @@ +/* JavaScript for Vertical Student View. */ +window.VerticalStudentView = function (runtime, element) { + + 'use strict'; + 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') + }); + }); +}; diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 1a4ef41869..6389f36df7 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -119,9 +119,12 @@ class ProctoringFields(object): @XBlock.wants('proctoring') @XBlock.wants('credit') +@XBlock.needs("user") +@XBlock.needs("bookmarks") class SequenceModule(SequenceFields, ProctoringFields, XModule): - ''' Layout module which lays out content in a temporal sequence - ''' + """ + 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 +185,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 +202,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 +220,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 diff --git a/common/lib/xmodule/xmodule/tests/test_vertical.py b/common/lib/xmodule/xmodule/tests/test_vertical.py index 5997b2324c..f381f7dda7 100644 --- a/common/lib/xmodule/xmodule/tests/test_vertical.py +++ b/common/lib/xmodule/xmodule/tests/test_vertical.py @@ -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): """ diff --git a/common/lib/xmodule/xmodule/vertical_block.py b/common/lib/xmodule/xmodule/vertical_block.py index edeccd9b75..252735f8ba 100644 --- a/common/lib/xmodule/xmodule/vertical_block.py +++ b/common/lib/xmodule/xmodule/vertical_block.py @@ -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): diff --git a/common/static/common/templates/components/paging-footer.underscore b/common/static/common/templates/components/paging-footer.underscore index d92eec41db..d04bbc0d2d 100644 --- a/common/static/common/templates/components/paging-footer.underscore +++ b/common/static/common/templates/components/paging-footer.underscore @@ -1,4 +1,4 @@ -