From 3cbbb8f3b138fc57034ed98e0b1db72cd93f4db4 Mon Sep 17 00:00:00 2001 From: muzaffaryousaf Date: Mon, 25 May 2015 13:57:03 +0500 Subject: [PATCH] Add/Remove Bookmark button to each unit in LMS courseware. TNL-1957 --- .../xmodule/xmodule/css/sequence/display.scss | 13 +- .../xmodule/js/src/sequence/display.coffee | 17 +- .../xmodule/xmodule/library_content_module.py | 1 + .../public/js/vertical_student_view.js | 16 ++ common/lib/xmodule/xmodule/seq_module.py | 20 +- .../xmodule/xmodule/tests/test_vertical.py | 15 +- common/lib/xmodule/xmodule/vertical_block.py | 7 + common/test/acceptance/pages/lms/bookmarks.py | 11 +- .../test/acceptance/pages/lms/course_nav.py | 4 +- .../test/acceptance/pages/lms/courseware.py | 27 +++ .../acceptance/tests/lms/test_bookmarks.py | 206 ++++++++++++------ .../tests/lms/test_lms_courseware.py | 37 +++- lms/djangoapps/bookmarks/tests/test_views.py | 4 +- lms/djangoapps/courseware/module_render.py | 3 + .../courseware/tests/test_module_render.py | 16 +- .../courseware/tests/test_split_module.py | 2 +- lms/djangoapps/courseware/views.py | 4 +- .../js/bookmarks/collections/bookmarks.js | 7 +- lms/static/js/bookmarks/main.js | 6 +- .../js/bookmarks/views/bookmark_button.js | 93 ++++++++ .../js/bookmarks/views/bookmarks_list.js | 4 +- ...rks_button.js => bookmarks_list_button.js} | 2 - .../fixtures/bookmarks/bookmark_button.html | 13 ++ .../js/fixtures/bookmarks/bookmarks.html | 2 +- .../bookmarks/bookmark_button_view_spec.js | 114 ++++++++++ ...ks_spec.js => bookmarks_list_view_spec.js} | 10 +- lms/static/js/spec/main.js | 10 +- .../sass/course/courseware/_courseware.scss | 4 + lms/static/sass/views/_bookmarks.scss | 62 +++++- lms/templates/bookmark_button.html | 11 + lms/templates/courseware/courseware.html | 11 +- lms/templates/seq_module.html | 7 +- lms/templates/vert_module.html | 5 + 33 files changed, 654 insertions(+), 110 deletions(-) create mode 100644 common/lib/xmodule/xmodule/public/js/vertical_student_view.js create mode 100644 lms/static/js/bookmarks/views/bookmark_button.js rename lms/static/js/bookmarks/views/{bookmarks_button.js => bookmarks_list_button.js} (95%) create mode 100644 lms/static/js/fixtures/bookmarks/bookmark_button.html create mode 100644 lms/static/js/spec/bookmarks/bookmark_button_view_spec.js rename lms/static/js/spec/bookmarks/{bookmarks_spec.js => bookmarks_list_view_spec.js} (96%) create mode 100644 lms/templates/bookmark_button.html 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/public/js/vertical_student_view.js b/common/lib/xmodule/xmodule/public/js/vertical_student_view.js new file mode 100644 index 0000000000..e7f9a8be05 --- /dev/null +++ b/common/lib/xmodule/xmodule/public/js/vertical_student_view.js @@ -0,0 +1,16 @@ +/* JavaScript for Vertical Student View. */ +window.VerticalStudentView = function (runtime, element) { + + RequireJS.require(['js/bookmarks/views/bookmark_button'], function (BookmarkButton) { + var $element = $(element); + var $bookmarkButtonElement = $element.find('.bookmark-button'); + + return new BookmarkButton({ + el: $bookmarkButtonElement, + bookmarkId: $bookmarkButtonElement.data('bookmarkId'), + usageId: $element.data('usageId'), + bookmarked: $element.parent('#seq_content').data('bookmarked'), + apiUrl: $(".courseware-bookmarks-button").data('bookmarksApiUrl') + }); + }); +}; diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py index 1a4ef41869..88e9b8b0ce 100644 --- a/common/lib/xmodule/xmodule/seq_module.py +++ b/common/lib/xmodule/xmodule/seq_module.py @@ -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 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/test/acceptance/pages/lms/bookmarks.py b/common/test/acceptance/pages/lms/bookmarks.py index 88ef1f8def..ec9384f45d 100644 --- a/common/test/acceptance/pages/lms/bookmarks.py +++ b/common/test/acceptance/pages/lms/bookmarks.py @@ -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 diff --git a/common/test/acceptance/pages/lms/course_nav.py b/common/test/acceptance/pages/lms/course_nav.py index 5134b5bacf..d2f2d6e543 100644 --- a/common/test/acceptance/pages/lms/course_nav.py +++ b/common/test/acceptance/pages/lms/course_nav.py @@ -193,13 +193,13 @@ class CourseNavPage(PageObject): ) # Regular expression to remove HTML span tags from a string - REMOVE_SPAN_TAG_RE = re.compile(r'') + REMOVE_SPAN_TAG_RE = re.compile(r'(.+)')] + + 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): """ diff --git a/common/test/acceptance/tests/lms/test_bookmarks.py b/common/test/acceptance/tests/lms/test_bookmarks.py index 696dbded6f..cb819e756f 100644 --- a/common/test/acceptance/tests/lms/test_bookmarks.py +++ b/common/test/acceptance/tests/lms/test_bookmarks.py @@ -2,17 +2,15 @@ """ End-to-end tests for the courseware unit bookmarks. """ -import json -import requests from ...pages.studio.auto_auth import AutoAuthPage from ...pages.lms.bookmarks import BookmarksPage from ...pages.lms.courseware import CoursewarePage +from ...pages.lms.course_nav import CourseNavPage from ...pages.studio.overview import CourseOutlinePage from ...pages.common.logout import LogoutPage from ...fixtures.course import CourseFixture, XBlockFixtureDesc -from ...fixtures import LMS_BASE_URL from ..helpers import EventsTestMixin, UniqueCourseTest, is_404_page @@ -22,30 +20,29 @@ class BookmarksTestMixin(EventsTestMixin, UniqueCourseTest): """ USERNAME = "STUDENT" EMAIL = "student@example.com" - COURSE_TREE_INFO = [ - ['TestSection1', 'TestSubsection1', 'TestProblem1'], - ['TestSection2', 'TestSubsection2', 'TestProblem2'] - ] - def create_course_fixture(self): - """ Create course fixture """ + def create_course_fixture(self, num_chapters): + """ + Create course fixture + + Arguments: + num_chapters: number of chapters to create + """ self.course_fixture = CourseFixture( # pylint: disable=attribute-defined-outside-init self.course_info['org'], self.course_info['number'], self.course_info['run'], self.course_info['display_name'] ) - self.course_fixture.add_children( - XBlockFixtureDesc('chapter', self.COURSE_TREE_INFO[0][0]).add_children( - XBlockFixtureDesc('sequential', self.COURSE_TREE_INFO[0][1]).add_children( - XBlockFixtureDesc('problem', self.COURSE_TREE_INFO[0][2]) + xblocks = [] + for index in range(num_chapters): + xblocks += [ + XBlockFixtureDesc('chapter', 'TestSection{}'.format(index)).add_children( + XBlockFixtureDesc('sequential', 'TestSubsection{}'.format(index)).add_children( + XBlockFixtureDesc('vertical', 'TestVertical{}'.format(index)) + ) ) - ), - XBlockFixtureDesc('chapter', self.COURSE_TREE_INFO[1][0]).add_children( - XBlockFixtureDesc('sequential', self.COURSE_TREE_INFO[1][1]).add_children( - XBlockFixtureDesc('problem', self.COURSE_TREE_INFO[1][2]) - ) - ) - ).install() + ] + self.course_fixture.add_children(*xblocks).install() class BookmarksTest(BookmarksTestMixin): @@ -66,35 +63,64 @@ class BookmarksTest(BookmarksTestMixin): self.course_info['run'] ) - self.create_course_fixture() + self.courseware_page = CoursewarePage(self.browser, self.course_id) + self.bookmarks_page = BookmarksPage(self.browser, self.course_id) + self.course_nav = CourseNavPage(self.browser) + + def _test_setup(self, num_chapters=2): + """ + Setup test settings. + + Arguments: + num_chapters: number of chapters to create in course + """ + self.create_course_fixture(num_chapters) # Auto-auth register for the course. AutoAuthPage(self.browser, username=self.USERNAME, email=self.EMAIL, course_id=self.course_id).visit() - self.courseware_page = CoursewarePage(self.browser, self.course_id) self.courseware_page.visit() - self.bookmarks = BookmarksPage(self.browser, self.course_id) - # Use auto-auth to retrieve the session for a logged in user - self.session = requests.Session() - response = self.session.get(LMS_BASE_URL + "/auto_auth?username=STUDENT&email=student@example.com") - self.assertTrue(response.ok, "Failed to get session info") + def _bookmark_unit(self, index): + """ + Bookmark a unit - def _bookmark_unit(self, course_id, usage_id): - """ Bookmark a single unit """ - csrftoken = self.session.cookies['csrftoken'] - headers = {'Content-type': 'application/json', "X-CSRFToken": csrftoken} - url = LMS_BASE_URL + "/api/bookmarks/v0/bookmarks/?course_id=" + course_id + '&fields=path' - data = json.dumps({'usage_id': usage_id}) + Arguments: + index: unit index to bookmark + """ + self.course_nav.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index)) + self.courseware_page.click_bookmark_unit_button() - response = self.session.post(url, data=data, headers=headers, cookies=self.session.cookies) - response = json.loads(response.text) - self.assertTrue(response['usage_id'] == usage_id, "Failed to bookmark unit") + def _bookmark_units(self, num_units): + """ + Bookmark first `num_units` units by visiting them - def _bookmarks_blocks(self, xblocks): - """ Bookmark all units in a course """ - for xblock in xblocks: - self._bookmark_unit(self.course_id, usage_id=xblock.locator) + Arguments: + num_units(int): Number of units to bookmarks + """ + for index in range(num_units): + self._bookmark_unit(index) + + def _breadcrumb(self, num_units): + """ + Creates breadcrumbs for the first `num_units` + + Arguments: + num_units(int): Number of units for which we want to create breadcrumbs + + Returns: + list of breadcrumbs + """ + breadcrumbs = [] + for index in range(num_units): + breadcrumbs.append( + [ + 'TestSection{}'.format(index), + 'TestSubsection{}'.format(index), + 'TestVertical{}'.format(index) + ] + ) + return breadcrumbs def _delete_section(self, index): """ Delete a section at index `index` """ @@ -119,6 +145,39 @@ class BookmarksTest(BookmarksTestMixin): self.courseware_page.visit() self.courseware_page.wait_for_page() + def _toggle_bookmark_and_verify(self, bookmark_icon_state, bookmark_button_state, bookmarked_count): + """ + Bookmark/Un-Bookmark a unit and then verify + """ + self.assertTrue(self.courseware_page.bookmark_button_visible) + self.courseware_page.click_bookmark_unit_button() + self.assertEqual(self.courseware_page.bookmark_icon_visible, bookmark_icon_state) + self.assertEqual(self.courseware_page.bookmark_button_state, bookmark_button_state) + self.bookmarks_page.click_bookmarks_button() + self.assertEqual(self.bookmarks_page.count(), bookmarked_count) + + def test_bookmark_button(self): + """ + Scenario: Bookmark unit button toggles correctly + + Given that I am a registered user + And I visit my courseware page + For first 2 units + I visit the unit + And I can see the Bookmark button + When I click on Bookmark button + Then unit should be bookmarked + Then I click again on the bookmark button + And I should see a unit un-bookmarked + """ + self._test_setup() + for index in range(2): + self.course_nav.go_to_section('TestSection{}'.format(index), 'TestSubsection{}'.format(index)) + + self._toggle_bookmark_and_verify(True, 'bookmarked', 1) + self.bookmarks_page.click_bookmarks_button(False) + self._toggle_bookmark_and_verify(False, '', 0) + def test_empty_bookmarks_list(self): """ Scenario: An empty bookmarks list is shown if there are no bookmarked units. @@ -130,15 +189,16 @@ class BookmarksTest(BookmarksTestMixin): Then I should see an empty bookmarks list And empty bookmarks list content is correct """ - self.assertTrue(self.bookmarks.bookmarks_button_visible()) - self.bookmarks.click_bookmarks_button() - self.assertEqual(self.bookmarks.results_header_text(), 'MY BOOKMARKS') - self.assertEqual(self.bookmarks.empty_header_text(), 'You have not bookmarked any courseware pages yet.') + self._test_setup() + self.assertTrue(self.bookmarks_page.bookmarks_button_visible()) + self.bookmarks_page.click_bookmarks_button() + self.assertEqual(self.bookmarks_page.results_header_text(), 'MY BOOKMARKS') + self.assertEqual(self.bookmarks_page.empty_header_text(), 'You have not bookmarked any courseware pages yet.') empty_list_text = ("Use bookmarks to help you easily return to courseware pages. To bookmark a page, " "select Bookmark in the upper right corner of that page. To see a list of all your " "bookmarks, select Bookmarks in the upper left corner of any courseware page.") - self.assertEqual(self.bookmarks.empty_list_text(), empty_list_text) + self.assertEqual(self.bookmarks_page.empty_list_text(), empty_list_text) def test_bookmarks_list(self): """ @@ -160,27 +220,30 @@ class BookmarksTest(BookmarksTestMixin): # discarded by the current version of MySQL we are using due to the # lack of support. Due to which order of bookmarked units will be # incorrect. - xblocks = self.course_fixture.get_nested_xblocks(category="problem") - self._bookmarks_blocks(xblocks) + self._test_setup() + self._bookmark_units(2) - self.bookmarks.click_bookmarks_button() - self.assertTrue(self.bookmarks.results_present()) - self.assertEqual(self.bookmarks.results_header_text(), 'MY BOOKMARKS') - self.assertEqual(self.bookmarks.count(), 2) + self.bookmarks_page.click_bookmarks_button() + self.assertTrue(self.bookmarks_page.results_present()) + self.assertEqual(self.bookmarks_page.results_header_text(), 'MY BOOKMARKS') + self.assertEqual(self.bookmarks_page.count(), 2) - bookmarked_breadcrumbs = self.bookmarks.breadcrumbs() + bookmarked_breadcrumbs = self.bookmarks_page.breadcrumbs() # Verify bookmarked breadcrumbs - self.assertItemsEqual(bookmarked_breadcrumbs, self.COURSE_TREE_INFO) + breadcrumbs = self._breadcrumb(2) + self.assertItemsEqual(bookmarked_breadcrumbs, breadcrumbs) + # get usage ids for units + xblocks = self.course_fixture.get_nested_xblocks(category="vertical") xblock_usage_ids = [xblock.locator for xblock in xblocks] # Verify link navigation for index in range(2): - self.bookmarks.click_bookmark(index) + self.bookmarks_page.click_bookmarked_block(index) self.courseware_page.wait_for_page() self.assertTrue(self.courseware_page.active_usage_id() in xblock_usage_ids) self.courseware_page.visit().wait_for_page() - self.bookmarks.click_bookmarks_button() + self.bookmarks_page.click_bookmarks_button() def test_unreachable_bookmark(self): """ @@ -195,13 +258,34 @@ class BookmarksTest(BookmarksTestMixin): When I click on deleted bookmark Then I should navigated to 404 page """ - self._bookmarks_blocks(self.course_fixture.get_nested_xblocks(category="problem")) - + self._test_setup() + self._bookmark_units(2) self._delete_section(0) - self.bookmarks.click_bookmarks_button() - self.assertTrue(self.bookmarks.results_present()) - self.assertEqual(self.bookmarks.count(), 2) + self.bookmarks_page.click_bookmarks_button() + self.assertTrue(self.bookmarks_page.results_present()) + self.assertEqual(self.bookmarks_page.count(), 2) - self.bookmarks.click_bookmark(0) + self.bookmarks_page.click_bookmarked_block(1) self.assertTrue(is_404_page(self.browser)) + + def test_page_size_limit(self): + """ + Scenario: We can get more bookmarks if page size is greater than default page size. + Note: + * Current Bookmarks API page_size value is 10. + * page_size value in bookmarks client side is set to 500. + + Given that I am a registered user + And I visit my courseware page + And I have bookmarked all the units available + Then I click on Bookmarks button + And I should see a bookmarked list + And bookmark list contains 11 bookmarked items + """ + self._test_setup(11) + self._bookmark_units(11) + + self.bookmarks_page.click_bookmarks_button() + self.assertTrue(self.bookmarks_page.results_present()) + self.assertEqual(self.bookmarks_page.count(), 11) diff --git a/common/test/acceptance/tests/lms/test_lms_courseware.py b/common/test/acceptance/tests/lms/test_lms_courseware.py index 5e7df1ed8c..79e0934aa6 100644 --- a/common/test/acceptance/tests/lms/test_lms_courseware.py +++ b/common/test/acceptance/tests/lms/test_lms_courseware.py @@ -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 diff --git a/lms/djangoapps/bookmarks/tests/test_views.py b/lms/djangoapps/bookmarks/tests/test_views.py index 9778294057..eca6eb736e 100644 --- a/lms/djangoapps/bookmarks/tests/test_views.py +++ b/lms/djangoapps/bookmarks/tests/test_views.py @@ -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), diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py index 0620c0af7a..08fb4c33cb 100644 --- a/lms/djangoapps/courseware/module_render.py +++ b/lms/djangoapps/courseware/module_render.py @@ -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 diff --git a/lms/djangoapps/courseware/tests/test_module_render.py b/lms/djangoapps/courseware/tests/test_module_render.py index cb49c0acb4..4368a19e70 100644 --- a/lms/djangoapps/courseware/tests/test_module_render.py +++ b/lms/djangoapps/courseware/tests/test_module_render.py @@ -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. diff --git a/lms/djangoapps/courseware/tests/test_split_module.py b/lms/djangoapps/courseware/tests/test_split_module.py index 7821a2c0df..d6002bad35 100644 --- a/lms/djangoapps/courseware/tests/test_split_module.py +++ b/lms/djangoapps/courseware/tests/test_split_module.py @@ -119,7 +119,7 @@ class SplitTestBase(ModuleStoreTestCase): content = resp.content # Assert we see the proper icon in the top display - self.assertIn('