diff --git a/cms/djangoapps/contentstore/tests/test_libraries.py b/cms/djangoapps/contentstore/tests/test_libraries.py index 0ff0bfabce..8a83dfd7cc 100644 --- a/cms/djangoapps/contentstore/tests/test_libraries.py +++ b/cms/djangoapps/contentstore/tests/test_libraries.py @@ -3,6 +3,7 @@ Content library unit tests that require the CMS runtime. """ from contentstore.tests.utils import AjaxEnabledTestClient, parse_json from contentstore.utils import reverse_url, reverse_usage_url, reverse_library_url +from contentstore.views.item import _duplicate_item from contentstore.views.preview import _load_preview_module from contentstore.views.tests.test_library import LIBRARY_REST_URL import ddt @@ -726,6 +727,7 @@ class TestLibraryAccess(SignalDisconnectTestMixin, LibraryTestCase): self.assertEqual(len(lc_block.children), 1 if expected_result else 0) +@ddt.ddt class TestOverrides(LibraryTestCase): """ Test that overriding block Scope.settings fields from a library in a specific course works @@ -745,6 +747,9 @@ class TestOverrides(LibraryTestCase): publish_item=False, ) + # Refresh library now that we've added something. + self.library = modulestore().get_library(self.lib_key) + # Also create a course: with modulestore().default_store(ModuleStoreEnum.Type.split): self.course = CourseFactory.create() @@ -822,7 +827,8 @@ class TestOverrides(LibraryTestCase): self.assertEqual(self.problem.definition_locator.definition_id, definition_id) self.assertEqual(self.problem_in_course.definition_locator.definition_id, definition_id) - def test_persistent_overrides(self): + @ddt.data(False, True) + def test_persistent_overrides(self, duplicate): """ Test that when we override Scope.settings values in a course, the override values persist even when the block is refreshed @@ -834,7 +840,14 @@ class TestOverrides(LibraryTestCase): self.problem_in_course.weight = new_weight modulestore().update_item(self.problem_in_course, self.user.id) - self.problem_in_course = modulestore().get_item(self.problem_in_course.location) + if duplicate: + # Check that this also works when the RCB is duplicated. + self.lc_block = modulestore().get_item( + _duplicate_item(self.course.location, self.lc_block.location, self.user) + ) + self.problem_in_course = modulestore().get_item(self.lc_block.children[0]) + else: + self.problem_in_course = modulestore().get_item(self.problem_in_course.location) self.assertEqual(self.problem_in_course.display_name, new_display_name) self.assertEqual(self.problem_in_course.weight, new_weight) @@ -852,6 +865,52 @@ class TestOverrides(LibraryTestCase): self.assertEqual(self.problem_in_course.weight, new_weight) self.assertEqual(self.problem_in_course.data, new_data_value) + def test_duplicated_version(self): + """ + Test that if a library is updated, and the content block is duplicated, + the new block will use the old library version and not the new one. + """ + store = modulestore() + self.assertEqual(len(self.library.children), 1) + self.assertEqual(len(self.lc_block.children), 1) + + # Edit the only problem in the library: + self.problem.display_name = "--changed in library--" + store.update_item(self.problem, self.user.id) + # Create an additional problem block in the library: + ItemFactory.create( + category="problem", + parent_location=self.library.location, + user_id=self.user.id, + publish_item=False, + ) + + # Refresh our reference to the library + self.library = store.get_library(self.lib_key) + + # Refresh our reference to the block + self.lc_block = store.get_item(self.lc_block.location) + self.problem_in_course = store.get_item(self.problem_in_course.location) + + # The library has changed... + self.assertEqual(len(self.library.children), 2) + + # But the block hasn't. + self.assertEqual(len(self.lc_block.children), 1) + self.assertEqual(self.problem_in_course.location, self.lc_block.children[0]) + self.assertEqual(self.problem_in_course.display_name, self.original_display_name) + + # Duplicate self.lc_block: + duplicate = store.get_item( + _duplicate_item(self.course.location, self.lc_block.location, self.user) + ) + # The duplicate should have identical children to the original: + self.assertEqual(len(duplicate.children), 1) + self.assertTrue(self.lc_block.source_library_version) + self.assertEqual(self.lc_block.source_library_version, duplicate.source_library_version) + problem2_in_course = store.get_item(duplicate.children[0]) + self.assertEqual(problem2_in_course.display_name, self.original_display_name) + class TestIncompatibleModuleStore(LibraryTestCase): """ diff --git a/cms/djangoapps/contentstore/views/item.py b/cms/djangoapps/contentstore/views/item.py index 1de8db3413..10f2bfed56 100644 --- a/cms/djangoapps/contentstore/views/item.py +++ b/cms/djangoapps/contentstore/views/item.py @@ -593,16 +593,25 @@ def _duplicate_item(parent_usage_key, duplicate_source_usage_key, user, display_ runtime=source_item.runtime, ) + 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. + 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: - dest_module.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) if dupe not in dest_module.children: # _duplicate_item may add the child for us. dest_module.children.append(dupe) store.update_item(dest_module, user.id) + # pylint: disable=protected-access 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. 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 d80ae4f82b..fb2723b880 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,6 +308,25 @@ class LibraryContentDescriptor(LibraryContentFields, MakoModuleDescriptor, XmlDe non_editable_fields.extend([LibraryContentFields.mode, LibraryContentFields.source_library_version]) return non_editable_fields + @lazy + def tools(self): + """ + Grab the library tools service or raise an error. + """ + return self.runtime.service(self, 'library_tools') + + def get_user_id(self): + """ + Get the ID of the current user. + """ + user_service = self.runtime.service(self, 'user') + if user_service: + # May be None when creating bok choy test fixtures + user_id = user_service.get_current_user().opt_attrs.get('edx-platform.user_id', None) + else: + user_id = None + return user_id + @XBlock.handler def refresh_children(self, request=None, suffix=None): # pylint: disable=unused-argument """ @@ -320,21 +341,50 @@ 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.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) - user_service = self.runtime.service(self, 'user') user_perms = self.runtime.service(self, 'studio_user_permissions') - if user_service: - # May be None when creating bok choy test fixtures - user_id = user_service.get_current_user().opt_attrs.get('edx-platform.user_id', None) - else: - user_id = None - lib_tools.update_children(self, user_id, user_perms) + user_id = self.get_user_id() + if not self.tools: + return Response("Library Tools unavailable in current runtime.", status=400) + self.tools.update_children(self, user_id, user_perms) return Response() + # Copy over any overridden settings the course author may have applied to the blocks. + def _copy_overrides(self, store, user_id, source, dest): + """ + Copy any overrides the user has made on blocks in this library. + """ + for field in source.fields.itervalues(): + if field.scope == Scope.settings and field.is_set_on(source): + setattr(dest, field.name, field.read_from(source)) + if source.has_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): + self._copy_overrides(store, user_id, source_child, dest_child) + store.update_item(dest, user_id) + + 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. + + Otherwise we'll end up losing data on the next refresh. + """ + # 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. + user_id = self.get_user_id() + user_perms = self.runtime.service(self, 'studio_user_permissions') + # pylint: disable=no-member + if not self.tools: + raise RuntimeError("Library tools unavailable, duplication will not be sane!") + self.tools.update_children(self, user_id, user_perms, version=self.source_library_version) + + self._copy_overrides(store, user_id, source_block, self) + + # Children have been handled. + return True + def _validate_library_version(self, validation, lib_tools, version, library_key): """ Validates library version 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 40afbbb549..28a490607c 100644 --- a/common/lib/xmodule/xmodule/library_tools.py +++ b/common/lib/xmodule/xmodule/library_tools.py @@ -4,6 +4,7 @@ XBlock runtime services for LibraryContentModule from django.core.exceptions import PermissionDenied from opaque_keys.edx.locator import LibraryLocator from xmodule.library_content_module import ANY_CAPA_TYPE_VALUE +from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.capa_module import CapaDescriptor @@ -21,14 +22,17 @@ class LibraryToolsService(object): Given a library key like "library-v1:ProblemX+PR0B", return the 'library' XBlock with meta-information about the library. + A specific version may be specified. + Returns None on error. """ if not isinstance(library_key, LibraryLocator): library_key = LibraryLocator.from_string(library_key) - assert library_key.version_guid is None try: - return self.store.get_library(library_key, remove_version=False, remove_branch=False) + return self.store.get_library( + library_key, remove_version=False, remove_branch=False, head_validation=False + ) except ItemNotFoundError: return None @@ -102,7 +106,7 @@ class LibraryToolsService(object): """ return self.store.check_supports(block.location.course_key, 'copy_from_template') - def update_children(self, dest_block, user_id, user_perms=None): + def update_children(self, dest_block, user_id, user_perms=None, version=None): """ This method is to be used when the library that a LibraryContentModule references has been updated. It will re-fetch all matching blocks from @@ -123,6 +127,8 @@ class LibraryToolsService(object): source_blocks = [] library_key = dest_block.source_library_key + if version: + library_key = library_key.replace(branch=ModuleStoreEnum.BranchName.library, version_guid=version) library = self._get_library(library_key) if library is None: raise ValueError("Requested library not found.") @@ -138,7 +144,10 @@ class LibraryToolsService(object): with self.store.bulk_operations(dest_block.location.course_key): dest_block.source_library_version = unicode(library.location.library_key.version_guid) self.store.update_item(dest_block, user_id) - dest_block.children = self.store.copy_from_template(source_blocks, dest_block.location, user_id) + head_validation = not version + dest_block.children = self.store.copy_from_template( + source_blocks, dest_block.location, user_id, head_validation=head_validation + ) # ^-- copy_from_template updates the children in the DB # but we must also set .children here to avoid overwriting the DB again diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py index 631902c6ad..e76eca743a 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py @@ -790,7 +790,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): else: self.request_cache.data['course_cache'] = {} - def _lookup_course(self, course_key): + def _lookup_course(self, course_key, head_validation=True): """ Decode the locator into the right series of db access. Does not return the CourseDescriptor! It returns the actual db json from @@ -799,11 +799,14 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): Semantics: if course id and branch given, then it will get that branch. If also give a version_guid, it will see if the current head of that branch == that guid. If not it raises VersionConflictError (the version now differs from what it was when you got your - reference) + reference) unless you specify head_validation = False, in which case it will return the + revision (if specified) by the course_key. :param course_key: any subclass of CourseLocator """ - if course_key.org and course_key.course and course_key.run: + if not course_key.version_guid: + head_validation = True + if head_validation and course_key.org and course_key.course and course_key.run: if course_key.branch is None: raise InsufficientSpecificationError(course_key) @@ -937,11 +940,11 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): """ return CourseLocator(org, course, run) - def _get_structure(self, structure_id, depth, **kwargs): + def _get_structure(self, structure_id, depth, head_validation=True, **kwargs): """ Gets Course or Library by locator """ - structure_entry = self._lookup_course(structure_id) + structure_entry = self._lookup_course(structure_id, head_validation=head_validation) root = structure_entry.structure['root'] result = self._load_items(structure_entry, [root], depth, **kwargs) return result[0] @@ -955,14 +958,14 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): raise ItemNotFoundError(course_id) return self._get_structure(course_id, depth, **kwargs) - def get_library(self, library_id, depth=0, **kwargs): + def get_library(self, library_id, depth=0, head_validation=True, **kwargs): """ Gets the 'library' root block for the library identified by the locator """ if not isinstance(library_id, LibraryLocator): # The supplied CourseKey is of the wrong type, so it can't possibly be stored in this modulestore. raise ItemNotFoundError(library_id) - return self._get_structure(library_id, depth, **kwargs) + return self._get_structure(library_id, depth, head_validation=head_validation, **kwargs) def has_course(self, course_id, ignore_case=False, **kwargs): """ @@ -2170,7 +2173,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): self._update_head(destination_course, index_entry, destination_course.branch, destination_structure['_id']) @contract(source_keys="list(BlockUsageLocator)", dest_usage=BlockUsageLocator) - def copy_from_template(self, source_keys, dest_usage, user_id): + def copy_from_template(self, source_keys, dest_usage, user_id, head_validation=True): """ Flexible mechanism for inheriting content from an external course/library/etc. @@ -2204,12 +2207,14 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): # so that we can access descendant information quickly source_structures = {} for key in source_keys: - course_key = key.course_key.for_version(None) + course_key = key.course_key if course_key.branch is None: raise ItemNotFoundError("branch is required for all source keys when using copy_from_template") if course_key not in source_structures: with self.bulk_operations(course_key): - source_structures[course_key] = self._lookup_course(course_key).structure + source_structures[course_key] = self._lookup_course( + course_key, head_validation=head_validation + ).structure destination_course = dest_usage.course_key with self.bulk_operations(destination_course): @@ -2226,7 +2231,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): # The descendants() method used above adds the block itself, which we don't consider a descendant. orig_descendants.remove(block_key) new_descendants = self._copy_from_template( - source_structures, source_keys, dest_structure, block_key, user_id + source_structures, source_keys, dest_structure, block_key, user_id, head_validation ) # Update the edit info: @@ -2250,7 +2255,9 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): for k in dest_structure['blocks'][block_key].fields['children'] ] - def _copy_from_template(self, source_structures, source_keys, dest_structure, new_parent_block_key, user_id): + def _copy_from_template( + self, source_structures, source_keys, dest_structure, new_parent_block_key, user_id, head_validation + ): """ Internal recursive implementation of copy_from_template() @@ -2263,9 +2270,11 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): new_children = list() # ordered list of the new children of new_parent_block_key for usage_key in source_keys: - src_course_key = usage_key.course_key.for_version(None) + src_course_key = usage_key.course_key + hashable_source_id = src_course_key.for_version(None) block_key = BlockKey(usage_key.block_type, usage_key.block_id) - source_structure = source_structures.get(src_course_key, []) + source_structure = source_structures[src_course_key] + if block_key not in source_structure['blocks']: raise ItemNotFoundError(usage_key) source_block_info = source_structure['blocks'][block_key] @@ -2273,7 +2282,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): # Compute a new block ID. This new block ID must be consistent when this # method is called with the same (source_key, dest_structure) pair unique_data = "{}:{}:{}".format( - unicode(src_course_key).encode("utf-8"), + unicode(hashable_source_id).encode("utf-8"), block_key.id, new_parent_block_key.id, ) @@ -2319,7 +2328,7 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): if children: children = [src_course_key.make_usage_key(child.type, child.id) for child in children] new_blocks |= self._copy_from_template( - source_structures, children, dest_structure, new_block_key, user_id + source_structures, children, dest_structure, new_block_key, user_id, head_validation ) new_blocks.add(new_block_key) diff --git a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py index 5830a64f01..22ca4342e3 100644 --- a/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py +++ b/common/lib/xmodule/xmodule/modulestore/split_mongo/split_draft.py @@ -58,7 +58,11 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli course_id = self._map_revision_to_branch(course_id) return super(DraftVersioningModuleStore, self).get_course(course_id, depth=depth, **kwargs) - def get_library(self, library_id, depth=0, **kwargs): + def get_library(self, library_id, depth=0, head_validation=True, **kwargs): + if not head_validation and library_id.version_guid: + return SplitMongoModuleStore.get_library( + self, library_id, depth=depth, head_validation=head_validation, **kwargs + ) library_id = self._map_revision_to_branch(library_id) return super(DraftVersioningModuleStore, self).get_library(library_id, depth=depth, **kwargs) @@ -100,7 +104,10 @@ class DraftVersioningModuleStore(SplitMongoModuleStore, ModuleStoreDraftAndPubli """ source_keys = [self._map_revision_to_branch(key) for key in source_keys] dest_key = self._map_revision_to_branch(dest_key) - new_keys = super(DraftVersioningModuleStore, self).copy_from_template(source_keys, dest_key, user_id) + head_validation = kwargs.get('head_validation') + new_keys = super(DraftVersioningModuleStore, self).copy_from_template( + source_keys, dest_key, user_id, head_validation + ) if dest_key.branch == ModuleStoreEnum.BranchName.draft: # Check if any of new_keys or their descendants need to be auto-published. # We don't use _auto_publish_no_children since children may need to be published. 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)