Ejemplo n.º 1
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)
    # 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:
    return get_library_block(usage_key)
 def test_map_into_course(self):
     """
     Test that key.map_into_course(key.course_key) won't raise an error as
     this pattern is used in several places in the LMS that still support
     old mongo.
     """
     key = LibraryUsageLocatorV2(self.VALID_LIB_KEY,
                                 block_type="problem",
                                 usage_id="p1")
     self.assertEqual(key.map_into_course(key.course_key), key)
Ejemplo n.º 3
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)
Ejemplo n.º 4
0
    def get_all_usages(self):
        """
        Get usage keys of all the blocks in this bundle
        """
        usage_keys = []
        for olx_file_path in self.get_olx_files():
            block_type, usage_id, _unused = olx_file_path.split('/')
            usage_key = LibraryUsageLocatorV2(self.library_key, block_type, usage_id)
            usage_keys.append(usage_key)

        return usage_keys
Ejemplo n.º 5
0
    def get_top_level_usages(self):
        """
        Get the set of usage keys in this bundle that have no parent.
        """
        own_usage_keys = []
        for olx_file_path in self.get_olx_files():
            block_type, usage_id, _unused = olx_file_path.split('/')
            usage_key = LibraryUsageLocatorV2(self.library_key, block_type, usage_id)
            own_usage_keys.append(usage_key)

        usage_keys_with_parents = self.get_bundle_includes().keys()
        return [usage_key for usage_key in own_usage_keys if usage_key not in usage_keys_with_parents]
Ejemplo n.º 6
0
def usage_for_child_include(parent_usage, parent_definition, parsed_include):
    """
    Get the usage ID for a child XBlock, given the parent's keys and the
    <xblock-include /> element that specifies the child.

    Consider two bundles, one with three definitions:
        main-unit, html1, subunit1
    And a second bundle with two definitions:
        unit1, html1
    Note that both bundles have a definition called "html1". Now, with the
    following tree structure, where "unit/unit1" and the second "html/html1"
    are in a linked bundle:

    <unit> in unit/main-unit/definition.xml
        <xblock-include definition="html/html1" />
        <xblock-include definition="unit/subunit1" />
            <xblock-include source="linked_bundle" definition="unit/unit1" usage="alias1" />
                <xblock-include definition="html/html1" />

    The following usage IDs would result:

    main-unit
        html1
        subunit1
            alias1
                alias1-html1

    Notice that "html1" in the linked bundle is prefixed so its ID stays
    unique from the "html1" in the original library.
    """
    assert isinstance(parent_usage, LibraryUsageLocatorV2)
    usage_id = parsed_include.usage_hint if parsed_include.usage_hint else parsed_include.definition_id
    library_bundle_uuid = bundle_uuid_for_library_key(parent_usage.context_key)
    # Is the parent usage from the same bundle as the library?
    parent_usage_from_library_bundle = parent_definition.bundle_uuid == library_bundle_uuid
    if not parent_usage_from_library_bundle:
        # This XBlock has been linked in to the library via a chain of one
        # or more bundle links. In order to keep usage_id collisions from
        # happening, any descdenants of the first linked block must have
        # their usage_id prefixed with the parent usage's usage_id.
        # (It would be possible to only change the prefix when the block is
        # a child of a block with an explicit usage="" attribute on its
        # <xblock-include> but that requires much more complex logic.)
        usage_id = parent_usage.usage_id + "-" + usage_id
    return LibraryUsageLocatorV2(
        lib_key=parent_usage.lib_key,
        block_type=parsed_include.block_type,
        usage_id=usage_id,
    )
 def test_invalid_args(self, key_args):
     with self.assertRaises((InvalidKeyError, TypeError, ValueError)):
         LibraryUsageLocatorV2(**key_args)
 def test_roundtrip_from_key(self, key_args):
     key = LibraryUsageLocatorV2(**key_args)
     serialized = text_type(key)
     deserialized = UsageKey.from_string(serialized)
     self.assertEqual(key, deserialized)
 def test_inheritance(self):
     """
     A LibraryUsageLocatorV2 is a usage key
     """
     usage_key = LibraryUsageLocatorV2(self.VALID_LIB_KEY, "problem", "p1")
     self.assertIsInstance(usage_key, UsageKey)
Ejemplo n.º 10
0
    def get_bundle_includes(self):
        """
        Scan through the bundle and all linked bundles as needed to generate
        a complete list of all the blocks that are included as
        child/grandchild/... blocks of the blocks in this bundle.

        Returns a dict of {usage_key -> BundleDefinitionLocator}

        Blocks in the bundle that have no parent are not included.
        """
        cache_key = ("bundle_includes", )
        usages_found = self.cache.get(cache_key)
        if usages_found is not None:
            return usages_found

        usages_found = {}

        def add_definitions_children(usage_key, def_key):
            """
            Recursively add any children of the given XBlock usage+definition to
            usages_found.
            """
            if not does_block_type_support_children(def_key.block_type):
                return
            try:
                xml_node = xml_for_definition(def_key)
            except:  # pylint:disable=bare-except
                log.exception("Unable to load definition {}".format(def_key))
                return

            for child in xml_node:
                if child.tag != 'xblock-include':
                    continue
                try:
                    parsed_include = parse_xblock_include(child)
                    child_usage = usage_for_child_include(
                        usage_key, def_key, parsed_include)
                    child_def_key = definition_for_include(
                        parsed_include, def_key)
                except BundleFormatException:
                    log.exception(
                        "Unable to parse a child of {}".format(def_key))
                    continue
                usages_found[child_usage] = child_def_key
                add_definitions_children(child_usage, child_def_key)

        # Find all the definitions in this bundle and recursively add all their descendants:
        bundle_files = get_bundle_files_cached(self.bundle_uuid,
                                               draft_name=self.draft_name)
        if self.draft_name:
            version_arg = {"draft_name": self.draft_name}
        else:
            version_arg = {
                "bundle_version": get_bundle_version_number(self.bundle_uuid)
            }
        for bfile in bundle_files:
            if not bfile.path.endswith(
                    "/definition.xml") or bfile.path.count('/') != 2:
                continue  # Not an OLX file.
            block_type, usage_id, _unused = bfile.path.split('/')
            def_key = BundleDefinitionLocator(bundle_uuid=self.bundle_uuid,
                                              block_type=block_type,
                                              olx_path=bfile.path,
                                              **version_arg)
            usage_key = LibraryUsageLocatorV2(self.library_key, block_type,
                                              usage_id)
            add_definitions_children(usage_key, def_key)

        self.cache.set(cache_key, usages_found)
        return usages_found
Ejemplo n.º 11
0
    def handle(self, *args, **options):
        """
        Validate the arguments, and start the transfer.
        """
        self.set_logging(options['verbosity'])

        # Create an API client for interacting with Studio:
        self.studio_client = StudioClient(
            studio_url='https://' + options['cms_domain'],
            config=settings.LX_EXPORTER_CMS_TARGETS[options['cms_domain']],
        )

        # Verify that we can connect to Studio:
        response = self.studio_client.api_call('get', '/api/user/v1/me')
        print("Connecting to studio as {}".format(response["username"]))

        # Read in the list of IDs (each line is an old modulestore ID and the new blockstore ID)
        with open(options['id_file'], 'r') as id_fh:
            block_key_list = [line.split() for line in id_fh.readlines() if line.strip() and line.strip()[0] != '#']

        unhandled_items = [] # List of tuples of (old_key, new_key) for any items we can't do automatically

        # Upload the OLX file by file:
        for old_key_str, new_key_str in block_key_list:
            old_key = UsageKey.from_string(old_key_str)
            new_key = UsageKey.from_string(new_key_str)
            print("Processing {} ({})".format(old_key, new_key))

            olx_dir = os.path.join(options["olx_dir"], old_key.block_type + "-" + old_key.block_id)

            # Various cases:
            if old_key.block_type == 'vertical':
                # This is a vertical block. What kind of children does it have?

                def read_olx(filename):
                    with open(olx_dir + '/' + filename, 'r') as fh:
                        return fh.read()

                vertical_olx_str = read_olx('definition-1.xml')
                olx_root = etree.fromstring(vertical_olx_str)
                children_refs = [node.attrib["definition"] for node in olx_root.iter("xblock-include")]
                if len(children_refs) == 1:
                    # This vertical actually contains only a single block:
                    old_block_type, old_block_id = children_refs[0].split("/")
                    result = self.convert_and_upload_olx_file(
                        olx_dir, "definition-{}-{}.xml".format(old_block_type, old_block_id), old_block_type, new_key,
                    )
                    if not result:
                        unhandled_items.append((old_key, new_key))
                elif new_key.block_type == 'unit':
                    # Upload each child OLX:
                    worked = True
                    for child_ref in children_refs:
                        child_block_type, child_block_id = child_ref.split("/")
                        child_new_key = LibraryUsageLocatorV2(
                            lib_key=new_key.lib_key,
                            block_type=child_block_type,
                            usage_id=child_block_id,
                        )
                        if self.studio_client.get_library_block(child_new_key) is None:
                            # Before we can upload the OLX we have to create the child block:
                            self.studio_client.add_block_to_library(
                                child_new_key.lib_key,
                                child_block_type,
                                child_block_id,
                                parent_block=new_key,
                            )
                        # Now update the OLX of the child block:
                        result = self.convert_and_upload_olx_file(
                            olx_dir,
                            "definition-{}-{}.xml".format(child_block_type, child_block_id),
                            child_block_type,
                            child_new_key,
                        )
                        worked = worked and result
                    if worked:
                        self.set_block_olx(new_key, vertical_olx_str)
                    else:
                        unhandled_items.append((old_key, new_key))
                else:
                    print(" -> can't handle this type, no known conversion")
                    unhandled_items.append((old_key, new_key))
            else:
                # This is a single block. Convert and upload it:
                result = self.convert_and_upload_olx_file(olx_dir, "definition-1.xml", old_key.block_type, new_key)
                if not result:
                    print(" -> can't handle this type, no known conversion")
                    unhandled_items.append((old_key, new_key))

        if unhandled_items:
            print("\n\nThe following items could not be migrated:")
            for old_key, new_key in unhandled_items:
                print("{} {}".format(old_key, new_key))