Example #1
0
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()
Example #2
0
    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,
            },
        }
Example #3
0
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
    ]
Example #4
0
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()
Example #5
0
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
Example #6
0
    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())
Example #7
0
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,
    )
Example #8
0
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)
Example #9
0
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)
Example #10
0
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
Example #11
0
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 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)
Example #13
0
    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)
Example #14
0
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),
    )
Example #15
0
    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)
Example #16
0
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()
Example #17
0
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,
    )
Example #18
0
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)