diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index fc876a37db..36e1e877fb 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -593,18 +593,17 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_ runtime=source_item.runtime, ) - handle_children = True - handle_parenting = True + children_handled = False if hasattr(dest_module, 'studio_post_duplicate'): # Allow an XBlock to do anything fancy it may need to when duplicated from another block. # These blocks may handle their own children or parenting if needed. Let them return booleans to # let us know if we need to handle these or not. - handle_children, handle_parenting = dest_module.studio_post_duplicate(store, parent_usage_key, source_item) + children_handled = dest_module.studio_post_duplicate(store, source_item) # Children are not automatically copied over (and not all xblocks have a 'children' attribute). # Because DAGs are not fully supported, we need to actually duplicate each child as well. - if source_item.has_children and handle_children: + if source_item.has_children and not children_handled: dest_module.children = dest_module.children or [] for child in source_item.children: dupe = _duplicate_item(dest_module.location, child, user=user) @@ -613,7 +612,7 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_ store.update_item(dest_module, user.id) # pylint: disable=protected-access - if ('detached' not in source_item.runtime.load_block_type(category)._class_tags) and handle_parenting: + if ('detached' not in source_item.runtime.load_block_type(category)._class_tags): parent = store.get_item(parent_usage_key) # If source was already a child of the parent, add duplicate immediately afterward. # Otherwise, add child to end. diff --git a/cms/djangoapps/contentstore/views/preview.py b/cms/djangoapps/contentstore/views/preview.py index e831ca7dd1..fbcd7f3bba 100644 --- a/cms/djangoapps/contentstore/views/preview.py +++ b/cms/djangoapps/contentstore/views/preview.py @@ -240,7 +240,6 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): # Only add the Studio wrapper when on the container page. The "Pages" page will remain as is for now. if not context.get('is_pages_view', None) and view in PREVIEW_VIEWS: root_xblock = context.get('root_xblock') - can_edit_visibility = not isinstance(xblock.location, LibraryUsageLocator) is_root = root_xblock and xblock.location == root_xblock.location is_reorderable = _is_xblock_reorderable(xblock, context) template_context = { @@ -251,7 +250,8 @@ def _studio_wrap_xblock(xblock, view, frag, context, display_name_only=False): 'is_root': is_root, 'is_reorderable': is_reorderable, 'can_edit': context.get('can_edit', True), - 'can_edit_visibility': can_edit_visibility, + 'can_edit_visibility': context.get('can_edit_visibility', True), + 'can_add': context.get('can_add', True), } html = render_to_string('studio_xblock_wrapper.html', template_context) frag = wrap_fragment(frag, html) diff --git a/cms/templates/studio_xblock_wrapper.html b/cms/templates/studio_xblock_wrapper.html index 8ffbbc7f4d..f947bf1038 100644 --- a/cms/templates/studio_xblock_wrapper.html +++ b/cms/templates/studio_xblock_wrapper.html @@ -80,19 +80,24 @@ messages = json.dumps(xblock.validate().to_json()) % endif -
  • - - - ${_("Duplicate")} + % if can_add: +
  • + + + ${_("Duplicate")} + +
  • + % endif + % endif + % if can_add: + +
  • + + + ${_("Delete")}
  • % endif -
  • - - - ${_("Delete")} - -
  • % if is_reorderable:
  • diff --git a/common/lib/xmodule/xmodule/library_content_module.py b/common/lib/xmodule/xmodule/library_content_module.py index a8e329effc..bbe696fd3f 100644 --- a/common/lib/xmodule/xmodule/library_content_module.py +++ b/common/lib/xmodule/xmodule/library_content_module.py @@ -7,6 +7,7 @@ from lxml import etree from copy import copy from capa.responsetypes import registry from gettext import ngettext +from lazy import lazy from .mako_module import MakoModuleDescriptor from opaque_keys.edx.locator import LibraryLocator @@ -269,6 +270,7 @@ class LibraryContentModule(LibraryContentFields, XModule, StudioEditableModule): 'max_count': self.max_count, 'display_name': self.display_name or self.url_name, })) + context['can_edit_visibility'] = False self.render_children(context, fragment, can_reorder=False, can_add=False) # else: When shown on a unit page, don't show any sort of preview - # just the status of this block in the validation area. @@ -306,16 +308,12 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe non_editable_fields.extend([LibraryContentFields.mode, LibraryContentFields.source_library_version]) return non_editable_fields - def get_tools(self): + @lazy + def tools(self): """ Grab the library tools service or raise an error. """ - lib_tools = self.runtime.service(self, 'library_tools') - if not lib_tools: - # This error is diagnostic. The user won't see it, but it may be helpful - # during debugging. - return Response(_(u"Course does not support Library tools."), status=400) - return lib_tools + return self.runtime.service(self, 'library_tools') def get_user_id(self): """ @@ -343,14 +341,12 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe the version number of the libraries used, so we easily determine if this block is up to date or not. """ - lib_tools = self.get_tools() user_perms = self.runtime.service(self, 'studio_user_permissions') user_id = self.get_user_id() - lib_tools.update_children(self, user_id, user_perms) + self.tools.update_children(self, user_id, user_perms) return Response() - # pylint: disable=unused-argument - def studio_post_duplicate(self, store, parent_usage_key, source_block): + def studio_post_duplicate(self, store, source_block): """ Used by the studio after basic duplication of a source block. We handle the children ourselves, because we have to properly reference the library upstream and set the overrides. @@ -360,32 +356,30 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe # The first task will be to refresh our copy of the library to generate the children. # We must do this at the currently set version of the library block. Otherwise we may not have # exactly the same children-- someone may be duplicating an out of date block, after all. - lib_tools = self.get_tools() user_id = self.get_user_id() user_perms = self.runtime.service(self, 'studio_user_permissions') # pylint: disable=no-member - lib_tools.update_children(self, user_id, user_perms, version=self.source_library_version) + self.tools.update_children(self, user_id, user_perms, version=self.source_library_version) # Copy over any overridden settings the course author may have applied to the blocks. def copy_overrides(source, dest): """ Copy any overrides the user has made on blocks in this library. """ - for field_name in source.fields.keys(): - field = dest.fields[field_name] + for field in source.fields.itervalues(): if field.scope == Scope.settings and field.is_set_on(source): - setattr(dest, field_name, getattr(source, field_name)) + setattr(dest, field.name, field.read_from(source)) if source.has_children: - source_children = [store.get_item(source_key) for source_key in source.children] - dest_children = [store.get_item(dest_key) for dest_key in dest.children] + source_children = [self.runtime.get_block(source_key) for source_key in source.children] + dest_children = [self.runtime.get_block(dest_key) for dest_key in dest.children] for source_child, dest_child in zip(source_children, dest_children): copy_overrides(source_child, dest_child) store.update_item(dest, user_id) copy_overrides(source_block, self) - # Don't handle children. Handle parenting. - return False, True + # Children have been handled. + return True def _validate_library_version(self, validation, lib_tools, version, library_key): """ diff --git a/common/lib/xmodule/xmodule/library_root_xblock.py b/common/lib/xmodule/xmodule/library_root_xblock.py index 9da0d9eaf9..2b6f642bf0 100644 --- a/common/lib/xmodule/xmodule/library_root_xblock.py +++ b/common/lib/xmodule/xmodule/library_root_xblock.py @@ -82,6 +82,7 @@ class LibraryRoot(XBlock): # Children must have a separate context from the library itself. Make a copy. child_context = context.copy() child_context['show_preview'] = self.show_children_previews + child_context['can_edit_visibility'] = False child = self.runtime.get_block(child_key) child_view_name = StudioEditableModule.get_preview_view_name(child) diff --git a/common/lib/xmodule/xmodule/library_tools.py b/common/lib/xmodule/xmodule/library_tools.py index da248457da..33d79619b0 100644 --- a/common/lib/xmodule/xmodule/library_tools.py +++ b/common/lib/xmodule/xmodule/library_tools.py @@ -28,9 +28,8 @@ class LibraryToolsService(object): if not isinstance(library_key, LibraryLocator): library_key = LibraryLocator.from_string(library_key) - library_key = LibraryLocator( - org=library_key.org, library=library_key.library, branch=library_key.branch, version_guid=version - ) + if version: + library_key.for_version(version) try: return self.store.get_library(library_key, remove_version=False, remove_branch=False) diff --git a/common/lib/xmodule/xmodule/studio_editable.py b/common/lib/xmodule/xmodule/studio_editable.py index 28e44d4bcc..d06c20c4fe 100644 --- a/common/lib/xmodule/xmodule/studio_editable.py +++ b/common/lib/xmodule/xmodule/studio_editable.py @@ -22,6 +22,7 @@ class StudioEditableBlock(object): for child in self.get_children(): # pylint: disable=no-member if can_reorder: context['reorderable_items'].add(child.location) + context['can_add'] = can_add rendered_child = child.render(StudioEditableModule.get_preview_view_name(child), context) fragment.add_frag_resources(rendered_child) diff --git a/common/test/acceptance/pages/studio/container.py b/common/test/acceptance/pages/studio/container.py index 3bd58bd2bb..b06b6956be 100644 --- a/common/test/acceptance/pages/studio/container.py +++ b/common/test/acceptance/pages/studio/container.py @@ -406,6 +406,20 @@ class XBlockWrapper(PageObject): def has_group_visibility_set(self): return self.q(css=self._bounded_selector('.wrapper-xblock.has-group-visibility-set')).is_present() + @property + def has_duplicate_button(self): + """ + Returns true if this xblock has a 'duplicate' button + """ + return self.q(css=self._bounded_selector('a.duplicate-button')) + + @property + def has_delete_button(self): + """ + Returns true if this xblock has a 'delete' button + """ + return self.q(css=self._bounded_selector('a.delete-button')) + @property def has_edit_visibility_button(self): """ 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 772b9013a0..01591203d8 100644 --- a/common/test/acceptance/tests/studio/test_studio_library_container.py +++ b/common/test/acceptance/tests/studio/test_studio_library_container.py @@ -290,3 +290,21 @@ class StudioLibraryContainerTest(StudioLibraryTest, UniqueCourseTest): block.reset_field_val("Display Name") block.save_settings() self.assertEqual(block.name, name_default) + + def test_cannot_manage(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 when I click the "View" link + Then I can see a preview of the blocks drawn from the library. + + And I do not see a duplicate button + And I do not see a delete button + """ + block_wrapper_unit_page = self._get_library_xblock_wrapper(self.unit_page.xblocks[0].children[0]) + container_page = block_wrapper_unit_page.go_to_container() + + for block in container_page.xblocks: + self.assertFalse(block.has_duplicate_button) + self.assertFalse(block.has_delete_button) + self.assertFalse(block.has_edit_visibility_button)