diff --git a/common/lib/xmodule/xmodule/library_content_module.py b/common/lib/xmodule/xmodule/library_content_module.py index 476c97b011..d9e28e93fd 100644 --- a/common/lib/xmodule/xmodule/library_content_module.py +++ b/common/lib/xmodule/xmodule/library_content_module.py @@ -1,3 +1,4 @@ +# -*- coding: utf-8 -*- """ LibraryContent: The XBlock used to include blocks from a library in a course. """ @@ -280,9 +281,21 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule): ) ) return validation - for library_key, version in self.source_libraries: # pylint: disable=unused-variable + for library_key, version in self.source_libraries: library = _get_library(self.runtime.descriptor_runtime.modulestore, library_key) - if library is None: + if library is not None: + latest_version = library.location.library_key.version_guid + if version is None or version != latest_version: + validation.set_summary( + StudioValidationMessage( + StudioValidationMessage.WARNING, + _(u'This component is out of date. The library has new content.'), + action_class='library-update-btn', # TODO: change this to action_runtime_event='...' once the unit page supports that feature. + action_label=_(u"↻ Update now") + ) + ) + break + else: validation.set_summary( StudioValidationMessage( StudioValidationMessage.ERROR, @@ -298,7 +311,7 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule): def author_view(self, context): """ Renders the Studio views. - Normal studio view: displays library status and has an "Update" button. + Normal studio view: If block is properly configured, displays library status summary Studio container view: displays a preview of all possible children. """ fragment = Fragment() @@ -311,45 +324,25 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule): fragment.add_content(self.system.render_template("library-block-author-preview-header.html", { 'max_count': self.max_count, 'display_name': self.display_name or self.url_name, - 'mode': self.mode, })) self.render_children(context, fragment, can_reorder=False, can_add=False) - else: - fragment.add_content(u'
{}
'.format( - _('No matching content found in library, no library configured, or not yet loaded from library.') - )) else: - UpdateStatus = enum( # pylint: disable=invalid-name - CANNOT=0, # Cannot update - library is not set, invalid, deleted, etc. - NEEDED=1, # An update is needed - prompt the user to update - UP_TO_DATE=2, # No update necessary - library is up to date - ) # When shown on a unit page, don't show any sort of preview - just the status of this block. - library_ok = bool(self.source_libraries) # True if at least one source library is defined library_names = [] - update_status = UpdateStatus.UP_TO_DATE - for library_key, version in self.source_libraries: + for library_key, version in self.source_libraries: # pylint: disable=unused-variable library = _get_library(self.runtime.descriptor_runtime.modulestore, library_key) - if library is None: - update_status = UpdateStatus.CANNOT - library_ok = False - break - library_names.append(library.display_name) - latest_version = library.location.library_key.version_guid - if version is None or version != latest_version: - update_status = UpdateStatus.NEEDED + if library is not None: + library_names.append(library.display_name) - fragment.add_content(self.system.render_template('library-block-author-view.html', { - 'library_names': library_names, - 'library_ok': library_ok, - 'UpdateStatus': UpdateStatus, - 'update_status': update_status, - 'max_count': self.max_count, - 'mode': self.mode, - 'num_children': len(self.children), # pylint: disable=no-member - })) - fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/library_content_edit.js')) - fragment.initialize_js('LibraryContentAuthorView') + if library_names: + fragment.add_content(self.system.render_template('library-block-author-view.html', { + 'library_names': library_names, + 'max_count': self.max_count, + 'num_children': len(self.children), # pylint: disable=no-member + })) + # The following JS is used to make the "Update now" button work on the unit page and the container view: + fragment.add_javascript_url(self.runtime.local_resource_url(self, 'public/js/library_content_edit.js')) + fragment.initialize_js('LibraryContentAuthorView') return fragment def get_child_descriptors(self): diff --git a/common/lib/xmodule/xmodule/public/js/library_content_edit.js b/common/lib/xmodule/xmodule/public/js/library_content_edit.js index 9a84a21404..2db019fedd 100644 --- a/common/lib/xmodule/xmodule/public/js/library_content_edit.js +++ b/common/lib/xmodule/xmodule/public/js/library_content_edit.js @@ -1,6 +1,14 @@ -/* JavaScript for editing operations that can be done on LibraryContentXBlock */ +/* JavaScript for special editing operations that can be done on LibraryContentXBlock */ window.LibraryContentAuthorView = function (runtime, element) { - $(element).find('.library-update-btn').on('click', function(e) { + "use strict"; + var usage_id = $(element).data('usage-id'); + // The "Update Now" button is not a child of 'element', as it is in the validation message area + // But it is still inside this xblock's wrapper element, which we can easily find: + var $wrapper = $(element).parents('*[data-locator="'+usage_id+'"]'); + + // We can't bind to the button itself because in the bok choy test environment, + // it may not yet exist at this point in time... not sure why. + $wrapper.on('click', '.library-update-btn', function(e) { e.preventDefault(); // Update the XBlock with the latest matching content from the library: runtime.notify('save', { diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py index 3833b2581c..58c4fe9235 100644 --- a/common/test/acceptance/pages/studio/container.py +++ b/common/test/acceptance/pages/studio/container.py @@ -336,6 +336,45 @@ class XBlockWrapper(PageObject): grand_locators = [grandkid.locator for grandkid in grandkids] return [descendant for descendant in descendants if descendant.locator not in grand_locators] + @property + def has_validation_message(self): + """ Is a validation warning/error/message shown? """ + return self.q(css=self._bounded_selector('.xblock-message.validation')).present + + def _validation_paragraph(self, css_class): + """ Helper method to return theelement of a validation warning """ + return self.q(css=self._bounded_selector('.xblock-message.validation p.{}'.format(css_class))) + + @property + def has_validation_warning(self): + """ Is a validation warning shown? """ + return self._validation_paragraph('warning').present + + @property + def has_validation_error(self): + """ Is a validation error shown? """ + return self._validation_paragraph('error').present + + @property + def has_validation_not_configured_warning(self): + """ Is a validation "not configured" message shown? """ + return self._validation_paragraph('not-configured').present + + @property + def validation_warning_text(self): + """ Get the text of the validation warning. """ + return self._validation_paragraph('warning').text[0] + + @property + def validation_error_text(self): + """ Get the text of the validation error. """ + return self._validation_paragraph('error').text[0] + + @property + def validation_not_configured_warning_text(self): + """ Get the text of the validation "not configured" message. """ + return self._validation_paragraph('not-configured').text[0] + @property def preview_selector(self): return self._bounded_selector('.xblock-student_view,.xblock-author_view') diff --git a/common/test/acceptance/pages/studio/library.py b/common/test/acceptance/pages/studio/library.py index 3151324cd0..ea7f2299f9 100644 --- a/common/test/acceptance/pages/studio/library.py +++ b/common/test/acceptance/pages/studio/library.py @@ -246,13 +246,6 @@ class StudioLibraryContainerXBlockWrapper(XBlockWrapper): """ return cls(xblock_wrapper.browser, xblock_wrapper.locator) - @property - def header_text(self): - """ - Gets library content text - """ - return self.get_body_paragraphs().first.text[0] - def get_body_paragraphs(self): """ Gets library content body paragraphs @@ -263,5 +256,7 @@ class StudioLibraryContainerXBlockWrapper(XBlockWrapper): """ Click "Update now..." button """ - refresh_button = self.q(css=self._bounded_selector(".library-update-btn")) + btn_selector = self._bounded_selector(".library-update-btn") + refresh_button = self.q(css=btn_selector) refresh_button.click() + self.wait_for_element_absence(btn_selector, 'Wait for the XBlock to reload') diff --git a/common/test/acceptance/tests/studio/test_studio_library_container.py b/common/test/acceptance/tests/studio/test_studio_library_container.py index 7bb712c779..ba66bdd7b1 100644 --- a/common/test/acceptance/tests/studio/test_studio_library_container.py +++ b/common/test/acceptance/tests/studio/test_studio_library_container.py @@ -94,40 +94,61 @@ class StudioLibraryContainerTest(ContainerBase, StudioLibraryTest): And I edit set library key to none Then I can see that library content block is misconfigured """ - expected_text = 'No library or filters configured. Press "Edit" to configure.' + expected_text = 'A library has not yet been selected.' + expected_action = 'Select a Library' library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0]) - # precondition check - assert library is configured before we remove it - self.assertNotIn(expected_text, library_container.header_text) + # precondition check - the library block should be configured before we remove the library setting + self.assertFalse(library_container.has_validation_not_configured_warning) edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit()) edit_modal.library_key = None - library_container.save_settings() - self.assertIn(expected_text, library_container.header_text) + self.assertTrue(library_container.has_validation_not_configured_warning) + self.assertIn(expected_text, library_container.validation_not_configured_warning_text) + self.assertIn(expected_action, library_container.validation_not_configured_warning_text) - @ddt.data( - 'library-v1:111+111', - 'library-v1:edX+L104', - ) - def test_set_missing_library_shows_correct_label(self, library_key): + def test_set_missing_library_shows_correct_label(self): """ Scenario: Given I have a library, a course and library content xblock in a course When I go to studio unit page for library content block And I edit set library key to non-existent library Then I can see that library content block is misconfigured """ + nonexistent_lib_key = 'library-v1:111+111' expected_text = "Library is invalid, corrupt, or has been deleted." library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0]) # precondition check - assert library is configured before we remove it - self.assertNotIn(expected_text, library_container.header_text) + self.assertFalse(library_container.has_validation_error) edit_modal = StudioLibraryContentXBlockEditModal(library_container.edit()) - edit_modal.library_key = library_key + edit_modal.library_key = nonexistent_lib_key library_container.save_settings() - self.assertIn(expected_text, library_container.header_text) + self.assertTrue(library_container.has_validation_error) + self.assertIn(expected_text, library_container.validation_error_text) + + def test_out_of_date_message(self): + """ + Scenario: Given I have a library, a course and library content xblock in a course + When I go to studio unit page for library content block + Then I can see that library content block needs to be updated + When I click on the update link + Then I can see that the content no longer needs to be updated + """ + expected_text = "This component is out of date. The library has new content." + library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0]) + + self.assertTrue(library_container.has_validation_warning) + self.assertIn(expected_text, library_container.validation_warning_text) + + library_container.refresh_children() + + self.unit_page.wait_for_page() # Wait for the page to reload + library_container = self._get_library_xblock_wrapper(self.unit_page.xblocks[0]) + + self.assertFalse(library_container.has_validation_message) diff --git a/lms/templates/library-block-author-preview-header.html b/lms/templates/library-block-author-preview-header.html index 4596281b67..ad76623deb 100644 --- a/lms/templates/library-block-author-preview-header.html +++ b/lms/templates/library-block-author-preview-header.html @@ -3,7 +3,7 @@
diff --git a/lms/templates/library-block-author-view.html b/lms/templates/library-block-author-view.html index ce1542cc58..46202aa2a9 100644 --- a/lms/templates/library-block-author-view.html +++ b/lms/templates/library-block-author-view.html @@ -2,12 +2,5 @@ from django.utils.translation import ugettext as _ %>${_('This component will be replaced by {mode} {max_count} components from the {num_children} matching components from {lib_names}.').format(mode=mode, max_count=max_count, num_children=num_children, lib_names=', '.join(library_names))}
- % if update_status == UpdateStatus.NEEDED: -${_('This component is out of date.')} ↻ ${_('Update now with latest components from the library')}
- % elif update_status == UpdateStatus.UP_TO_DATE: -${_(u'✓ Up to date.')}
- % endif - % endif +${_('This component will be replaced by {max_count} component[s] randomly chosen from the {num_children} matching components in {lib_names}.').format(mode=mode, max_count=max_count, num_children=num_children, lib_names=', '.join(library_names))}