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)