def get_item_definition(cls, item): ref = ContentLibrary.objects.get_by_key(item) lib_bundle = LibraryBundle(item, ref.bundle_uuid, draft_name=DRAFT_NAME) num_blocks = len(lib_bundle.get_top_level_usages()) last_published = lib_bundle.get_last_published_time() last_published_str = None if last_published: last_published_str = last_published.strftime('%Y-%m-%dT%H:%M:%SZ') (has_unpublished_changes, has_unpublished_deletes) = lib_bundle.has_changes() bundle_metadata = get_bundle(ref.bundle_uuid) # NOTE: Increment ContentLibraryIndexer.SCHEMA_VERSION if the following schema is updated to avoid dealing # with outdated indexes which might cause errors due to missing/invalid attributes. return { "schema_version": ContentLibraryIndexer.SCHEMA_VERSION, "id": str(item), "uuid": str(bundle_metadata.uuid), "title": bundle_metadata.title, "description": bundle_metadata.description, "num_blocks": num_blocks, "version": bundle_metadata.latest_version, "last_published": last_published_str, "has_unpublished_changes": has_unpublished_changes, "has_unpublished_deletes": has_unpublished_deletes, # only 'content' field is analyzed by elastisearch, and allows text-search "content": { "id": str(item), "title": bundle_metadata.title, "description": bundle_metadata.description, }, }
def delete_library_block(usage_key, remove_from_parent=True): """ Delete the specified block from this library (and any children it has). If the block's definition (OLX file) is within this same library as the usage key, both the definition and the usage will be deleted. If the usage points to a definition in a linked bundle, the usage will be deleted but the link and the linked bundle will be unaffected. If the block is in use by some other bundle that links to this one, that will not prevent deletion of the definition. remove_from_parent: modify the parent to remove the reference to this delete block. This should always be true except when this function calls itself recursively. """ assert isinstance(usage_key, LibraryUsageLocatorV2) library_context = get_learning_context_impl(usage_key) library_ref = ContentLibrary.objects.get_by_key(usage_key.context_key) def_key = library_context.definition_for_usage(usage_key) if def_key is None: raise ContentLibraryBlockNotFound(usage_key) lib_bundle = LibraryBundle(usage_key.context_key, library_ref.bundle_uuid, draft_name=DRAFT_NAME) # Create a draft: draft_uuid = get_or_create_bundle_draft(def_key.bundle_uuid, DRAFT_NAME).uuid # Does this block have a parent? if usage_key not in lib_bundle.get_top_level_usages( ) and remove_from_parent: # Yes: this is not a top-level block. # First need to modify the parent to remove this block as a child. raise NotImplementedError # Does this block have children? block = load_block(usage_key, user=None) if block.has_children: # Next, recursively call delete_library_block(...) on each child usage for child_usage in block.children: # Specify remove_from_parent=False to avoid unnecessary work to # modify this block's children list when deleting each child, since # we're going to delete this block anyways. delete_library_block(child_usage, remove_from_parent=False) # Delete the definition: if def_key.bundle_uuid == library_ref.bundle_uuid: # This definition is in the library, so delete it: path_prefix = lib_bundle.olx_prefix(def_key) for bundle_file in get_bundle_files(def_key.bundle_uuid, use_draft=DRAFT_NAME): if bundle_file.path.startswith(path_prefix): # Delete this file, within this definition's "folder" write_draft_file(draft_uuid, bundle_file.path, contents=None) else: # The definition must be in a linked bundle, so we don't want to delete # it; just the <xblock-include /> in the parent, which was already # deleted above. pass # Clear the bundle cache so everyone sees the deleted block immediately: lib_bundle.cache.clear()
def get_library_blocks(library_key): """ Get the list of top-level XBlocks in the specified library. Returns a list of LibraryXBlockMetadata objects """ ref = ContentLibrary.objects.get_by_key(library_key) lib_bundle = LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME) usages = lib_bundle.get_top_level_usages() blocks = [] for usage_key in usages: # For top-level definitions, we can go from definition key to usage key using the following, but this would not # work for non-top-level blocks as they may have multiple usages. Top level blocks are guaranteed to have only # a single usage in the library, which is part of the definition of top level block. def_key = lib_bundle.definition_for_usage(usage_key) blocks.append( LibraryXBlockMetadata( usage_key=usage_key, def_key=def_key, display_name=get_block_display_name(def_key), has_unpublished_changes=lib_bundle. does_definition_have_unpublished_changes(def_key), )) return blocks
def handle(self, *args, **options): if options['clear-all']: if options['force'] or query_yes_no(self.CONFIRMATION_PROMPT_CLEAR, default="no"): logging.info("Removing all libraries from the index") ContentLibraryIndexer.remove_all_items() LibraryBlockIndexer.remove_all_items() return if options['all']: if options['force'] or query_yes_no(self.CONFIRMATION_PROMPT_ALL, default="no"): logging.info("Indexing all libraries") library_keys = [ library.library_key for library in ContentLibrary.objects.all() ] else: return else: logging.info("Indexing libraries: {}".format( options['library_ids'])) library_keys = list( map(LibraryLocatorV2.from_string, options['library_ids'])) ContentLibraryIndexer.index_items(library_keys) for library_key in library_keys: ref = ContentLibrary.objects.get_by_key(library_key) lib_bundle = LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME) LibraryBlockIndexer.index_items(lib_bundle.get_all_usages())
def get_library(library_key): """ Get the library with the specified key. Does not check permissions. returns a ContentLibraryMetadata instance. Raises ContentLibraryNotFound if the library doesn't exist. """ assert isinstance(library_key, LibraryLocatorV2) ref = ContentLibrary.objects.get_by_key(library_key) bundle_metadata = get_bundle(ref.bundle_uuid) lib_bundle = LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME) num_blocks = len(lib_bundle.get_top_level_usages()) last_published = lib_bundle.get_last_published_time() (has_unpublished_changes, has_unpublished_deletes) = lib_bundle.has_changes() return ContentLibraryMetadata( key=library_key, bundle_uuid=ref.bundle_uuid, title=bundle_metadata.title, description=bundle_metadata.description, num_blocks=num_blocks, version=bundle_metadata.latest_version, last_published=last_published, allow_public_learning=ref.allow_public_learning, allow_public_read=ref.allow_public_read, has_unpublished_changes=has_unpublished_changes, has_unpublished_deletes=has_unpublished_deletes, )
def get_library_blocks(library_key, text_search=None, block_types=None): """ Get the list of top-level XBlocks in the specified library. Returns a list of LibraryXBlockMetadata objects """ metadata = None if LibraryBlockIndexer.indexing_is_enabled(): try: filter_terms = { 'library_key': [str(library_key)], 'is_child': [False], } if block_types: filter_terms['block_type'] = block_types metadata = [ { **item, "id": LibraryUsageLocatorV2.from_string(item['id']), } for item in LibraryBlockIndexer.get_items(filter_terms=filter_terms, text_search=text_search) if item is not None ] except ElasticConnectionError as e: log.exception(e) # If indexing is disabled, or connection to elastic failed if metadata is None: metadata = [] ref = ContentLibrary.objects.get_by_key(library_key) lib_bundle = LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME) usages = lib_bundle.get_top_level_usages() for usage_key in usages: # For top-level definitions, we can go from definition key to usage key using the following, but this would # not work for non-top-level blocks as they may have multiple usages. Top level blocks are guaranteed to # have only a single usage in the library, which is part of the definition of top level block. def_key = lib_bundle.definition_for_usage(usage_key) display_name = get_block_display_name(def_key) text_match = (text_search is None or text_search.lower() in display_name.lower() or text_search.lower() in str(usage_key).lower()) type_match = (block_types is None or usage_key.block_type in block_types) if text_match and type_match: metadata.append({ "id": usage_key, "def_key": def_key, "display_name": display_name, "has_unpublished_changes": lib_bundle.does_definition_have_unpublished_changes(def_key), }) return [ LibraryXBlockMetadata( usage_key=item['id'], def_key=item['def_key'], display_name=item['display_name'], has_unpublished_changes=item['has_unpublished_changes'], ) for item in metadata ]
def create_library_block(library_key, block_type, definition_id): """ Create a new XBlock in this library of the specified type (e.g. "html"). The 'definition_id' value (which should be a string like "problem1") will be used as both the definition_id and the usage_id. """ assert isinstance(library_key, LibraryLocatorV2) ref = ContentLibrary.objects.get_by_key(library_key) if ref.type != COMPLEX: if block_type != ref.type: raise IncompatibleTypesError( _('Block type "{block_type}" is not compatible with library type "{library_type}".' ).format( block_type=block_type, library_type=ref.type, )) lib_bundle = LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME) # Total number of blocks should not exceed the maximum allowed total_blocks = len(lib_bundle.get_top_level_usages()) if total_blocks + 1 > settings.MAX_BLOCKS_PER_CONTENT_LIBRARY: raise BlockLimitReachedError( _(u"Library cannot have more than {} XBlocks").format( settings.MAX_BLOCKS_PER_CONTENT_LIBRARY)) # Make sure the proposed ID will be valid: validate_unicode_slug(definition_id) # Ensure the XBlock type is valid and installed: XBlock.load_class(block_type) # Will raise an exception if invalid # Make sure the new ID is not taken already: new_usage_id = definition_id # Since this is a top level XBlock, usage_id == definition_id usage_key = LibraryUsageLocatorV2( lib_key=library_key, block_type=block_type, usage_id=new_usage_id, ) library_context = get_learning_context_impl(usage_key) if library_context.definition_for_usage(usage_key) is not None: raise LibraryBlockAlreadyExists( "An XBlock with ID '{}' already exists".format(new_usage_id)) new_definition_xml = '<{}/>'.format( block_type) # xss-lint: disable=python-wrap-html path = "{}/{}/definition.xml".format(block_type, definition_id) # Write the new XML/OLX file into the library bundle's draft draft = get_or_create_bundle_draft(ref.bundle_uuid, DRAFT_NAME) write_draft_file(draft.uuid, path, new_definition_xml.encode('utf-8')) # Clear the bundle cache so everyone sees the new block immediately: BundleCache(ref.bundle_uuid, draft_name=DRAFT_NAME).clear() # Now return the metadata about the new block: LIBRARY_BLOCK_CREATED.send(sender=None, library_key=ref.library_key, usage_key=usage_key) return get_library_block(usage_key)
def publish_changes(library_key): """ Publish all pending changes to the specified library. """ ref = ContentLibrary.objects.get_by_key(library_key) bundle = get_bundle(ref.bundle_uuid) if DRAFT_NAME in bundle.drafts: # pylint: disable=unsupported-membership-test draft_uuid = bundle.drafts[DRAFT_NAME] # pylint: disable=unsubscriptable-object commit_draft(draft_uuid) else: return # If there is no draft, no action is needed. LibraryBundle(library_key, ref.bundle_uuid).cache.clear() LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME).cache.clear()
def definition_for_usage(self, usage_key): """ Given a usage key for an XBlock in this context, return the BundleDefinitionLocator which specifies the actual XBlock definition (as a path to an OLX in a specific blockstore bundle). Must return a BundleDefinitionLocator if the XBlock exists in this context, or None otherwise. """ library_key = usage_key.context_key try: bundle_uuid = bundle_uuid_for_library_key(library_key) except ContentLibrary.DoesNotExist: return None bundle = LibraryBundle(library_key, bundle_uuid, self.use_draft) return bundle.definition_for_usage(usage_key)
def create_bundle_link(library_key, link_id, target_opaque_key, version=None): """ Create a new link to the resource with the specified opaque key. For now, only LibraryLocatorV2 opaque keys are supported. """ ref = ContentLibrary.objects.get_by_key(library_key) # Make sure this link ID/name is not already in use: links = blockstore_cache.get_bundle_draft_direct_links_cached( ref.bundle_uuid, DRAFT_NAME) if link_id in links: raise InvalidNameError("That link ID is already in use.") # Determine the target: if not isinstance(target_opaque_key, LibraryLocatorV2): raise TypeError( "For now, only LibraryLocatorV2 opaque keys are supported by create_bundle_link" ) target_bundle_uuid = ContentLibrary.objects.get_by_key( target_opaque_key).bundle_uuid if version is None: version = get_bundle(target_bundle_uuid).latest_version # Create the new link: draft = get_or_create_bundle_draft(ref.bundle_uuid, DRAFT_NAME) set_draft_link(draft.uuid, link_id, target_bundle_uuid, version) # Clear the cache: LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME).cache.clear() CONTENT_LIBRARY_UPDATED.send(sender=None, library_key=library_key)
def definition_for_usage(self, usage_key, **kwargs): """ Given a usage key for an XBlock in this context, return the BundleDefinitionLocator which specifies the actual XBlock definition (as a path to an OLX in a specific blockstore bundle). Must return a BundleDefinitionLocator if the XBlock exists in this context, or None otherwise. """ library_key = usage_key.context_key try: bundle_uuid = bundle_uuid_for_library_key(library_key) except ContentLibrary.DoesNotExist: return None if 'force_draft' in kwargs: # lint-amnesty, pylint: disable=consider-using-get use_draft = kwargs['force_draft'] else: use_draft = self.use_draft bundle = LibraryBundle(library_key, bundle_uuid, use_draft) return bundle.definition_for_usage(usage_key)
def get_library_block(usage_key): """ Get metadata (LibraryXBlockMetadata) about one specific XBlock in a library To load the actual XBlock instance, use openedx.core.djangoapps.xblock.api.load_block() instead. """ assert isinstance(usage_key, LibraryUsageLocatorV2) lib_context = get_learning_context_impl(usage_key) def_key = lib_context.definition_for_usage(usage_key) if def_key is None: raise ContentLibraryBlockNotFound(usage_key) lib_bundle = LibraryBundle(usage_key.library_slug, def_key.bundle_uuid, draft_name=DRAFT_NAME) return LibraryXBlockMetadata( usage_key=usage_key, def_key=def_key, display_name=get_block_display_name(def_key), has_unpublished_changes=lib_bundle.does_definition_have_unpublished_changes(def_key), )
def index_libraries(cls, library_keys): """ Index the specified libraries. If they already exist, replace them with new ones. """ searcher = SearchEngine.get_search_engine(cls.INDEX_NAME) library_dicts = [] for library_key in library_keys: ref = ContentLibrary.objects.get_by_key(library_key) lib_bundle = LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME) num_blocks = len(lib_bundle.get_top_level_usages()) last_published = lib_bundle.get_last_published_time() last_published_str = None if last_published: last_published_str = last_published.strftime( '%Y-%m-%dT%H:%M:%SZ') (has_unpublished_changes, has_unpublished_deletes) = lib_bundle.has_changes() bundle_metadata = get_bundle(ref.bundle_uuid) # NOTE: Increment ContentLibraryIndexer.SCHEMA_VERSION if the following schema is updated to avoid dealing # with outdated indexes which might cause errors due to missing/invalid attributes. library_dict = { "schema_version": ContentLibraryIndexer.SCHEMA_VERSION, "id": str(library_key), "uuid": str(bundle_metadata.uuid), "title": bundle_metadata.title, "description": bundle_metadata.description, "num_blocks": num_blocks, "version": bundle_metadata.latest_version, "last_published": last_published_str, "has_unpublished_changes": has_unpublished_changes, "has_unpublished_deletes": has_unpublished_deletes, } library_dicts.append(library_dict) return searcher.index(cls.LIBRARY_DOCUMENT_TYPE, library_dicts)
def get_library(library_key): """ Get the library with the specified key. Does not check permissions. returns a ContentLibraryMetadata instance. Raises ContentLibraryNotFound if the library doesn't exist. """ assert isinstance(library_key, LibraryLocatorV2) ref = ContentLibrary.objects.get_by_key(library_key) bundle_metadata = get_bundle(ref.bundle_uuid) lib_bundle = LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME) (has_unpublished_changes, has_unpublished_deletes) = lib_bundle.has_changes() return ContentLibraryMetadata( key=library_key, bundle_uuid=ref.bundle_uuid, title=bundle_metadata.title, description=bundle_metadata.description, version=bundle_metadata.latest_version, has_unpublished_changes=has_unpublished_changes, has_unpublished_deletes=has_unpublished_deletes, )
def _lookup_usage_key(usage_key): """ Given a LibraryUsageLocatorV2 (usage key for an XBlock in a content library) return the definition key and LibraryBundle or raise ContentLibraryBlockNotFound """ assert isinstance(usage_key, LibraryUsageLocatorV2) lib_context = get_learning_context_impl(usage_key) def_key = lib_context.definition_for_usage(usage_key, force_draft=DRAFT_NAME) if def_key is None: raise ContentLibraryBlockNotFound(usage_key) lib_bundle = LibraryBundle(usage_key.lib_key, def_key.bundle_uuid, draft_name=DRAFT_NAME) return def_key, lib_bundle
def revert_changes(library_key): """ Revert all pending changes to the specified library, restoring it to the last published version. """ ref = ContentLibrary.objects.get_by_key(library_key) bundle = get_bundle(ref.bundle_uuid) if DRAFT_NAME in bundle.drafts: # pylint: disable=unsupported-membership-test draft_uuid = bundle.drafts[DRAFT_NAME] # pylint: disable=unsubscriptable-object delete_draft(draft_uuid) else: return # If there is no draft, no action is needed. LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME).cache.clear() CONTENT_LIBRARY_UPDATED.send(sender=None, library_key=library_key, update_blocks=True)
def update_bundle_link(library_key, link_id, version=None, delete=False): """ Update a bundle's link to point to the specified version of its target bundle. Use version=None to automatically point to the latest version. Use delete=True to delete the link. """ ref = ContentLibrary.objects.get_by_key(library_key) draft = get_or_create_bundle_draft(ref.bundle_uuid, DRAFT_NAME) if delete: set_draft_link(draft.uuid, link_id, None, None) else: links = blockstore_cache.get_bundle_draft_direct_links_cached(ref.bundle_uuid, DRAFT_NAME) try: link = links[link_id] except KeyError: raise InvalidNameError("That link does not exist.") if version is None: version = get_bundle(link.bundle_uuid).latest_version set_draft_link(draft.uuid, link_id, link.bundle_uuid, version) # Clear the cache: LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME).cache.clear()
def update_library( library_key, title=None, description=None, allow_public_learning=None, allow_public_read=None, library_type=None, library_license=None, ): """ Update a library's metadata (Slug cannot be changed as it would break IDs throughout the system.) A value of None means "don't change". """ ref = ContentLibrary.objects.get_by_key(library_key) # Update MySQL model: changed = False if allow_public_learning is not None: ref.allow_public_learning = allow_public_learning changed = True if allow_public_read is not None: ref.allow_public_read = allow_public_read changed = True if library_type is not None: if library_type not in (COMPLEX, ref.type): lib_bundle = LibraryBundle(library_key, ref.bundle_uuid, draft_name=DRAFT_NAME) (has_unpublished_changes, has_unpublished_deletes) = lib_bundle.has_changes() if has_unpublished_changes or has_unpublished_deletes: raise IncompatibleTypesError( _('You may not change a library\'s type to {library_type} if it still has unpublished changes.' ).format(library_type=library_type)) for block in get_library_blocks(library_key): if block.usage_key.block_type != library_type: raise IncompatibleTypesError( _('You can only set a library to {library_type} if all existing blocks are of that type. ' 'Found incompatible block {block_id} with type {block_type}.' ).format( library_type=library_type, block_type=block.usage_key.block_type, block_id=block.usage_key.block_id, ), ) ref.type = library_type changed = True if library_license is not None: ref.license = library_license changed = True if changed: ref.save() # Update Blockstore: fields = { # We don't ever read the "slug" value from the Blockstore bundle, but # we might as well always do our best to keep it in sync with the "slug" # value in the LMS that we do use. "slug": ref.slug, } if title is not None: assert isinstance(title, str) fields["title"] = title if description is not None: assert isinstance(description, str) fields["description"] = description update_bundle(ref.bundle_uuid, **fields) CONTENT_LIBRARY_UPDATED.send(sender=None, library_key=ref.library_key)