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 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 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 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 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)