Example #1
0
 def test_identical_olx(self):
     """
     Test library blocks with children that also have identical OLX. Since
     the blockstore runtime caches authored field data based on the hash of
     the OLX, this can catch some potential bugs, especially given that the
     "children" field stores usage IDs, not definition IDs.
     """
     # Create a unit containing a <problem>
     unit_block_key = library_api.create_library_block(self.library.key, "unit", "u1").usage_key
     library_api.create_library_block_child(unit_block_key, "problem", "p1")
     library_api.publish_changes(self.library.key)
     # Now do the same in a different library:
     library2 = library_api.create_library(
         collection_uuid=self.collection.uuid,
         org=self.organization,
         slug="idolx",
         title=("Identical OLX Test Lib 2"),
         description="",
         library_type=COMPLEX,
         allow_public_learning=True,
         allow_public_read=False,
         library_license=CC_4_BY,
     )
     unit_block2_key = library_api.create_library_block(library2.key, "unit", "u1").usage_key
     library_api.create_library_block_child(unit_block2_key, "problem", "p1")
     library_api.publish_changes(library2.key)
     # Load both blocks:
     unit_block = xblock_api.load_block(unit_block_key, self.student_a)
     unit_block2 = xblock_api.load_block(unit_block2_key, self.student_a)
     assert library_api.get_library_block_olx(unit_block_key) == library_api.get_library_block_olx(unit_block2_key)
     assert unit_block.children != unit_block2.children
Example #2
0
    def test_modify_state_directly(self):
        """
        Test that we can modify user-specific XBlock fields directly in Python
        """
        # Create two XBlocks, block1 and block2
        block1_metadata = library_api.create_library_block(
            self.library.key, UserStateTestBlock.BLOCK_TYPE, "b2-1")
        block1_usage_key = block1_metadata.usage_key
        block2_metadata = library_api.create_library_block(
            self.library.key, UserStateTestBlock.BLOCK_TYPE, "b2-2")
        block2_usage_key = block2_metadata.usage_key
        library_api.publish_changes(self.library.key)

        # Alice changes all the fields of block1:
        block1_alice = xblock_api.load_block(block1_usage_key, self.student_a)
        block1_alice.user_str = 'Alice was here'
        block1_alice.uss_str = 'Alice was here (USS)'
        block1_alice.pref_str = 'Alice was here (prefs)'
        block1_alice.user_info_str = 'Alice was here (user info)'
        block1_alice.save()

        # Now load it back and expect the same field data:
        block1_alice = xblock_api.load_block(block1_usage_key, self.student_a)

        self.assertEqual(block1_alice.scope_ids.user_id, self.student_a.id)
        self.assertEqual(block1_alice.user_str, 'Alice was here')
        self.assertEqual(block1_alice.uss_str, 'Alice was here (USS)')
        self.assertEqual(block1_alice.pref_str, 'Alice was here (prefs)')
        self.assertEqual(block1_alice.user_info_str,
                         'Alice was here (user info)')

        # Now load a different block for Alice:
        block2_alice = xblock_api.load_block(block2_usage_key, self.student_a)
        # User state should be default:
        self.assertEqual(block2_alice.user_str, 'default value')
        # User state summary should be default:
        self.assertEqual(block2_alice.uss_str, 'default value')
        # But prefs and user info should be shared:
        self.assertEqual(block2_alice.pref_str, 'Alice was here (prefs)')
        self.assertEqual(block2_alice.user_info_str,
                         'Alice was here (user info)')

        # Now load the first block, block1, for Bob:
        block1_bob = xblock_api.load_block(block1_usage_key, self.student_b)

        self.assertEqual(block1_bob.scope_ids.user_id, self.student_b.id)
        self.assertEqual(block1_bob.user_str, 'default value')
        self.assertEqual(block1_bob.uss_str, 'Alice was here (USS)')
        self.assertEqual(block1_bob.pref_str, 'default value')
        self.assertEqual(block1_bob.user_info_str, 'default value')
Example #3
0
    def test_has_score(self):
        """
        Test that the LMS-specific 'has_score' attribute is getting added to
        blocks.
        """
        unit_block_key = library_api.create_library_block(self.library.key, "unit", "u1").usage_key
        problem_block_key = library_api.create_library_block(self.library.key, "problem", "p1").usage_key
        library_api.publish_changes(self.library.key)
        unit_block = xblock_api.load_block(unit_block_key, self.student_a)
        problem_block = xblock_api.load_block(problem_block_key, self.student_a)

        self.assertFalse(hasattr(UnitBlock, 'has_score'))  # The block class doesn't declare 'has_score'
        self.assertEqual(unit_block.has_score, False)  # But it gets added by the runtime and defaults to False
        # And problems do have has_score True:
        self.assertEqual(problem_block.has_score, True)
Example #4
0
    def test_independent_instances(self):
        """
        Test that independent instances of the same block don't share field data
        until .save() and re-loading, even when they're using the same runtime.
        """
        block_metadata = library_api.create_library_block(self.library.key, UserStateTestBlock.BLOCK_TYPE, "b3")
        block_usage_key = block_metadata.usage_key
        library_api.publish_changes(self.library.key)

        block_instance1 = xblock_api.load_block(block_usage_key, self.student_a)
        block_instance2 = block_instance1.runtime.get_block(block_usage_key)

        # We could assert that both instances of the block have the same runtime
        # instance, but that's an implementation detail. The main point of this
        # test is just to make sure there's never any surprises when reading
        # field data out of an XBlock, because of other instances of the same
        # block.

        block_instance1.user_str = 'changed to this'
        self.assertNotEqual(block_instance1.user_str, block_instance2.user_str)

        block_instance1.save()
        self.assertNotEqual(block_instance1.user_str, block_instance2.user_str)

        block_instance2 = block_instance1.runtime.get_block(block_usage_key)
        # Now they should be equal, because we've saved and re-loaded instance2:
        self.assertEqual(block_instance1.user_str, block_instance2.user_str)
Example #5
0
 def test_dndv2_sets_translator(self):
     dnd_block_key = library_api.create_library_block(
         self.library.key, "drag-and-drop-v2", "dnd1").usage_key
     library_api.publish_changes(self.library.key)
     dnd_block = xblock_api.load_block(dnd_block_key, self.student_a)
     i18n_service = dnd_block.runtime.service(dnd_block, 'i18n')
     assert isinstance(i18n_service.translator, GNUTranslations)
Example #6
0
def create_library_block_child(parent_usage_key, block_type, definition_id):
    """
    Create a new XBlock definition in this library of the specified type (e.g.
    "html"), and add it as a child of the specified existing block.

    The 'definition_id' value (which should be a string like "problem1") will be
    used as both the definition_id and the usage_id of the child.
    """
    assert isinstance(parent_usage_key, LibraryUsageLocatorV2)
    # Load the parent block to make sure it exists and so we can modify its 'children' field:
    parent_block = load_block(parent_usage_key, user=None)
    if not parent_block.has_children:
        raise ValueError(
            "The specified parent XBlock does not allow child XBlocks.")
    # Create the new block in the library:
    metadata = create_library_block(parent_usage_key.context_key, block_type,
                                    definition_id)
    # Set the block as a child.
    # This will effectively "move" the newly created block from being a top-level block in the library to a child.
    include_data = XBlockInclude(link_id=None,
                                 block_type=block_type,
                                 definition_id=definition_id,
                                 usage_hint=None)
    parent_block.runtime.add_child_include(parent_block, include_data)
    parent_block.save()
    ref = ContentLibrary.objects.get_by_key(parent_usage_key.context_key)
    LIBRARY_BLOCK_UPDATED.send(sender=None,
                               library_key=ref.library_key,
                               usage_key=metadata.usage_key)
    return metadata
Example #7
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 #8
0
    def import_from_blockstore(self, dest_block, blockstore_block_id):
        """
        Imports a block from a blockstore-based learning context (usually a
        content library) into modulestore, as a new child of dest_block.
        Any existing children of dest_block are replaced.

        This is only used by LibrarySourcedBlock. It should verify first that
        the number of block IDs is reasonable.
        """
        dest_key = dest_block.scope_ids.usage_id
        if not isinstance(dest_key, BlockUsageLocator):
            raise TypeError(
                "Destination {} should be a modulestore course.".format(
                    dest_key))
        if self.user_id is None:
            raise ValueError(
                "Cannot check user permissions - LibraryTools user_id is None")

        dest_course_key = dest_key.context_key
        user = User.objects.get(id=self.user_id)
        if not has_studio_write_access(user, dest_course_key):
            raise PermissionDenied()

        # Read the source block; this will also confirm that user has permission to read it.
        orig_block = load_block(UsageKey.from_string(blockstore_block_id),
                                user)

        with self.store.bulk_operations(dest_course_key):
            new_block_id = self._import_block(orig_block, dest_key)
            # Remove any existing children that are no longer used
            for old_child_id in set(dest_block.children) - set([new_block_id]):
                self.store.delete_item(old_child_id, self.user_id)
            # If this was called from a handler, it will save dest_block at the end, so we must update
            # dest_block.children to avoid it saving the old value of children and deleting the new ones.
            dest_block.children = self.store.get_item(dest_key).children
    def test_complex_xblock_tree(self, _mock1, _mock2):
        """
        Test a Pathway with a deep XBlock tree and inter-bundle-links
        """
        # Create the following XBlock tree
        # Library 2
        #   unit "alpha"
        #     -> link to library 1's bundle ("link1")
        #       -> unit "beta" (in Library 1)
        #         -> html "gamma"
        # Then make sure we can add that whole hierarchy into a pathway.

        beta_key = library_api.create_library_block(self.lib1.key, "unit", "beta").usage_key
        gamma_key = library_api.create_library_block_child(beta_key, "html", "gamma").usage_key
        library_api.set_library_block_olx(gamma_key, '<html>This is gamma.</html>')
        library_api.publish_changes(self.lib1.key)
        library_api.create_bundle_link(self.lib2.key, 'link1', self.lib1.key)
        alpha_key = library_api.create_library_block(self.lib2.key, "unit", "alpha").usage_key
        library_api.set_library_block_olx(alpha_key, '''
            <unit display_name="alpha">
                <xblock-include source="link1" definition="unit/beta" usage_hint="beta" />
            </unit>
        ''')
        library_api.publish_changes(self.lib2.key)

        # Create a new pathway:
        response = self.client.post(URL_CREATE_PATHWAY, {
            "owner_user_id": self.user.id,
            "draft_data": {
                "title": "A Commplex Pathway",
                "description": "Complex pathway for testing",
                "data": {},
                "items": [
                    {"original_usage_id": str(alpha_key)},
                ],
            },
        }, format='json')
        self.assertEqual(response.status_code, 200)
        pathway_id = response.data["id"]
        pathway_url = URL_GET_PATHWAY.format(pathway_id=pathway_id)

        # Now publish the pathway:
        response = self.client.post(pathway_url + 'publish/')
        self.assertEqual(response.status_code, 200)
        # Get the resulting pathway:
        response = self.client.get(pathway_url)
        self.assertEqual(response.status_code, 200)

        # Get the usage ID of the root 'alpha' XBlock:
        alpha_key_pathway = UsageKey.from_string(response.data["published_data"]["items"][0]["usage_id"])
        self.assertNotEqual(alpha_key_pathway, alpha_key)

        block = xblock_api.load_block(alpha_key_pathway, user=self.other_user)
        # Render the block and all its children - make sure even the descendants are rendered:
        fragment = block.render('student_view', context={})
        self.assertIn('This is gamma.', fragment.content)
Example #10
0
    def import_from_blockstore(self, dest_block, blockstore_block_ids):
        """
        Imports a block from a blockstore-based learning context (usually a
        content library) into modulestore, as a new child of dest_block.
        Any existing children of dest_block are replaced.

        This is only used by LibrarySourcedBlock. It should verify first that
        the number of block IDs is reasonable.
        """
        dest_key = dest_block.scope_ids.usage_id
        if not isinstance(dest_key, BlockUsageLocator):
            raise TypeError(
                "Destination {} should be a modulestore course.".format(
                    dest_key))
        if self.user_id is None:
            raise ValueError(
                "Cannot check user permissions - LibraryTools user_id is None")

        if len(set(blockstore_block_ids)) != len(blockstore_block_ids):
            # We don't support importing the exact same block twice because it would break the way we generate new IDs
            # for each block and then overwrite existing copies of blocks when re-importing the same blocks.
            raise ValueError(
                "One or more library component IDs is a duplicate.")

        dest_course_key = dest_key.context_key
        user = User.objects.get(id=self.user_id)
        if not has_studio_write_access(user, dest_course_key):
            raise PermissionDenied()

        # Read the source block; this will also confirm that user has permission to read it.
        # (This could be slow and use lots of memory, except for the fact that LibrarySourcedBlock which calls this
        # should be limiting the number of blocks to a reasonable limit. We load them all now instead of one at a
        # time in order to raise any errors before we start actually copying blocks over.)
        orig_blocks = [
            load_block(UsageKey.from_string(key), user)
            for key in blockstore_block_ids
        ]

        with self.store.bulk_operations(dest_course_key):
            child_ids_updated = set()

            for block in orig_blocks:
                new_block_id = self._import_block(block, dest_key)
                child_ids_updated.add(new_block_id)

            # Remove any existing children that are no longer used
            for old_child_id in set(dest_block.children) - child_ids_updated:
                self.store.delete_item(old_child_id, self.user_id)
            # If this was called from a handler, it will save dest_block at the end, so we must update
            # dest_block.children to avoid it saving the old value of children and deleting the new ones.
            dest_block.children = self.store.get_item(dest_key).children
Example #11
0
    def test_default_values(self):
        """
        Test that a user sees the default field values at first
        """
        block_metadata = library_api.create_library_block(self.library.key, UserStateTestBlock.BLOCK_TYPE, "b1")
        block_usage_key = block_metadata.usage_key
        library_api.publish_changes(self.library.key)

        block_alice = xblock_api.load_block(block_usage_key, self.student_a)

        self.assertEqual(block_alice.scope_ids.user_id, self.student_a.id)
        self.assertEqual(block_alice.user_str, 'default value')
        self.assertEqual(block_alice.uss_str, 'default value')
        self.assertEqual(block_alice.pref_str, 'default value')
        self.assertEqual(block_alice.user_info_str, 'default value')
Example #12
0
    def test_import_from_blockstore(self):
        # Create a blockstore content library
        library = self._create_library(slug="testlib1_import", title="A Test Library", description="Testing XBlocks")
        # Create a unit block with an HTML block in it.
        unit_block_id = self._add_block_to_library(library["id"], "unit", "unit1")["id"]
        html_block_id = self._add_block_to_library(library["id"], "html", "html1", parent_block=unit_block_id)["id"]
        html_block = load_block(UsageKey.from_string(html_block_id), self.user)
        # Add assets and content to the HTML block
        self._set_library_block_asset(html_block_id, "test.txt", b"data", expect_response=200)
        self._set_library_block_olx(html_block_id, '<html><a href="/static/test.txt">Hello world</a></html>')

        # Create a modulestore course
        course = CourseFactory.create(modulestore=self.store, user_id=self.user.id)
        CourseInstructorRole(course.id).add_users(self.user)
        # Add Source from library block to the course
        sourced_block = self.make_block("library_sourced", course, user_id=self.user.id)

        # Import the unit block from the library to the course
        self.tools.import_from_blockstore(sourced_block, unit_block_id)

        # Verify imported block with its children
        self.assertEqual(len(sourced_block.children), 1)
        self.assertEqual(sourced_block.children[0].category, 'unit')

        imported_unit_block = self.store.get_item(sourced_block.children[0])
        self.assertEqual(len(imported_unit_block.children), 1)
        self.assertEqual(imported_unit_block.children[0].category, 'html')

        imported_html_block = self.store.get_item(imported_unit_block.children[0])
        self.assertIn('Hello world', imported_html_block.data)

        # Check that assets were imported and static paths were modified after importing
        assets = library_api.get_library_block_static_asset_files(html_block.scope_ids.usage_id)
        self.assertEqual(len(assets), 1)
        self.assertIn(assets[0].url, imported_html_block.data)

        # Check that reimporting updates the target block
        self._set_library_block_olx(html_block_id, '<html><a href="/static/test.txt">Foo bar</a></html>')
        self.tools.import_from_blockstore(sourced_block, unit_block_id)

        self.assertEqual(len(sourced_block.children), 1)
        imported_unit_block = self.store.get_item(sourced_block.children[0])
        self.assertEqual(len(imported_unit_block.children), 1)
        imported_html_block = self.store.get_item(imported_unit_block.children[0])
        self.assertNotIn('Hello world', imported_html_block.data)
        self.assertIn('Foo bar', imported_html_block.data)
Example #13
0
    def post(self, request):
        """
        Process LTI platform launch requests.
        """

        # Parse LTI launch message.

        try:
            self.launch_message = self.get_launch_message()
        except LtiException as exc:
            log.exception('LTI 1.3: Tool launch failed: %s', exc)
            return self._bad_request_response()

        log.info("LTI 1.3: Launch message body: %s",
                 json.dumps(self.launch_data))

        # Parse content key.

        usage_key_str = request.GET.get('id')
        if not usage_key_str:
            return self._bad_request_response()
        usage_key = LibraryUsageLocatorV2.from_string(usage_key_str)
        log.info('LTI 1.3: Launch block: id=%s', usage_key)

        # Authenticate the launch and setup LTI profiles.

        edx_user = self._authenticate_and_login(usage_key)
        if not edx_user:
            return self._bad_request_response()

        # Get the block.

        self.block = xblock_api.load_block(
            usage_key,
            user=self.request.user)

        # Handle Assignment and Grade Service request.

        self.handle_ags()

        # Render context and response.
        context = self.get_context_data()
        response = self.render_to_response(context)
        mark_user_change_as_expected(edx_user.id)
        return response
Example #14
0
 def get_block_completion_status():
     """ Get block completion status (0 to 1) """
     block = xblock_api.load_block(block_id, self.student_a)
     assert hasattr(block, 'publish_completion')
     service = block.runtime.service(block, 'completion')
     return service.get_completions([block_id])[block_id]
    def test_pathway_lifecycle(self):  # pylint: disable=too-many-statements
        """
        Test Pathway CRUD
        """
        # Create a new pathway:
        response = self.client.post(URL_CREATE_PATHWAY, self.pathway_initial_data, format='json')
        self.assertEqual(response.status_code, 200)
        pathway_id = response.data["id"]

        # Get the resulting pathway:
        pathway_url = URL_GET_PATHWAY.format(pathway_id=pathway_id)
        # Other users cannot get the new pathway:
        other_response = self.other_client.get(pathway_url)
        self.assertEqual(other_response.status_code, 403)
        # But our authorized user can:
        response = self.client.get(pathway_url)
        self.assertEqual(response.status_code, 200)
        orig_data = response.data
        self.assertEqual(orig_data["id"], pathway_id)
        self.assertEqual(orig_data["owner_user_id"], self.user.id)
        self.assertEqual(orig_data["owner_group_name"], None)
        self.assertEqual(orig_data["draft_data"]["items"][0]["original_usage_id"], str(self.problem_block1_id))
        self.assertEqual(orig_data["draft_data"]["items"][0]["data"]["notes"], "this is a test note.")
        self.assertEqual(orig_data["draft_data"]["items"][1]["original_usage_id"], str(self.html_block2_id))
        self.assertEqual(len(orig_data["draft_data"]["items"]), 2)
        self.assertEqual(orig_data["published_data"]["items"], [])
        item1_pathway_id = orig_data["draft_data"]["items"][0]["usage_id"]
        item2_pathway_id = orig_data["draft_data"]["items"][1]["usage_id"]

        # Now publish the pathway:
        response = self.client.post(pathway_url + 'publish/')
        self.assertEqual(response.status_code, 200)
        response = self.client.get(pathway_url)
        self.assertEqual(response.status_code, 200)
        # The new published data/items should equal the previous draft data:
        self.assertEqual(response.data["published_data"], orig_data["draft_data"])

        # Now we'll add a second copy of the HTML block to the pathway:
        new_draft_data = deepcopy(orig_data["draft_data"])
        new_draft_data["items"].append({"original_usage_id": str(self.html_block2_id)})
        # Other users cannot change the pathway:
        other_response = self.other_client.patch(pathway_url, {"draft_data": new_draft_data}, format="json")
        self.assertEqual(other_response.status_code, 403)
        # But our authorized user can:
        response = self.client.patch(pathway_url, {"draft_data": new_draft_data}, format="json")
        self.assertEqual(response.status_code, 200)
        # The IDs of the existing items in the pathway must not have changed:
        self.assertEqual(response.data["draft_data"]["items"][0]["usage_id"], item1_pathway_id)
        self.assertEqual(response.data["draft_data"]["items"][1]["usage_id"], item2_pathway_id)
        # And now a new item was added:
        self.assertEqual(len(response.data["draft_data"]["items"]), 3)

        # Now revert the changes, going back to two items:
        response = self.client.delete(pathway_url + 'publish/')
        self.assertEqual(response.status_code, 200)
        response = self.client.get(pathway_url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(len(response.data["draft_data"]["items"]), 2)
        self.assertEqual(len(response.data["published_data"]["items"]), 2)
        self.assertEqual(response.data["draft_data"], orig_data["draft_data"])
        self.assertEqual(response.data["published_data"], orig_data["draft_data"])

        # Now view one of the XBlocks in the pathway.
        # This checks that all of the normal XBlock runtime APIs are working with blocks in a pathway context:
        item1_pathway_key = UsageKey.from_string(item1_pathway_id)
        block = xblock_api.load_block(item1_pathway_key, user=self.other_user)
        self.assertEqual(block.attempts, 0)  # Test loading a field value

        # Now delete the pathway:
        # Other users cannot delete the pathway:
        other_response = self.other_client.delete(pathway_url)
        self.assertEqual(other_response.status_code, 403)
        # But our authorized user can:
        response = self.client.delete(pathway_url)
        self.assertEqual(response.status_code, 200)
        response = self.client.get(pathway_url)
        self.assertEqual(response.status_code, 404)  # Pathway is deleted.