From 9f6b3a873f3426a1c6ff08e574e7b63b35deab25 Mon Sep 17 00:00:00 2001 From: Samuel Walladge Date: Mon, 2 Mar 2020 16:37:24 +1030 Subject: [PATCH] Make more metadata available via the new runtime's generic XBlock API Without this PR, there is no [reasonable] way to get the following data for any XBlocks in the new runtime; now there is :) * index_dictionary: data about the block content, for search indexing * student_view_data: data-only equivalent of student_view, for use in custom UIs/mobile * children: list of child IDs * editable_children: list of child IDs in the same bundle (use case: when showing an OLX editor you want to allow editing the OLX of children in the same bundle but not linked children) --- .../content_libraries/tests/base.py | 5 +- .../content_libraries/tests/test_runtime.py | 53 +++++++++++++++++++ openedx/core/djangoapps/xblock/api.py | 37 +++++++++++-- .../core/djangoapps/xblock/rest_api/views.py | 10 +++- 4 files changed, 99 insertions(+), 6 deletions(-) diff --git a/openedx/core/djangoapps/content_libraries/tests/base.py b/openedx/core/djangoapps/content_libraries/tests/base.py index 63d8a7fa8e..68a6ff2902 100644 --- a/openedx/core/djangoapps/content_libraries/tests/base.py +++ b/openedx/core/djangoapps/content_libraries/tests/base.py @@ -32,6 +32,7 @@ URL_LIB_BLOCK_ASSET_FILE = URL_LIB_BLOCK + 'assets/{file_name}' # Get, delete, URL_BLOCK_RENDER_VIEW = '/api/xblock/v2/xblocks/{block_key}/view/{view_name}/' URL_BLOCK_GET_HANDLER_URL = '/api/xblock/v2/xblocks/{block_key}/handler_url/{handler_name}/' +URL_BLOCK_METADATA_URL = '/api/xblock/v2/xblocks/{block_key}/' # Decorator for tests that require blockstore @@ -72,9 +73,9 @@ class ContentLibrariesRestApiTest(APITestCase): # is not yet any Studio REST API for doing so: cls.collection = blockstore_api.create_collection("Content Library Test Collection") # Create an organization - cls.organization = Organization.objects.create( - name="Content Libraries Tachyon Exploration & Survey Team", + cls.organization, _ = Organization.objects.get_or_create( short_name="CL-TEST", + defaults={"name": "Content Libraries Tachyon Exploration & Survey Team"}, ) def setUp(self): diff --git a/openedx/core/djangoapps/content_libraries/tests/test_runtime.py b/openedx/core/djangoapps/content_libraries/tests/test_runtime.py index 2dd2c3284d..4f16dfb217 100644 --- a/openedx/core/djangoapps/content_libraries/tests/test_runtime.py +++ b/openedx/core/djangoapps/content_libraries/tests/test_runtime.py @@ -16,6 +16,7 @@ from openedx.core.djangoapps.content_libraries.tests.base import ( requires_blockstore, URL_BLOCK_RENDER_VIEW, URL_BLOCK_GET_HANDLER_URL, + URL_BLOCK_METADATA_URL, ) from openedx.core.djangoapps.content_libraries.tests.user_state_block import UserStateTestBlock from openedx.core.djangoapps.xblock import api as xblock_api @@ -113,6 +114,57 @@ class ContentLibraryRuntimeTest(ContentLibraryContentTestMixin, TestCase): # And problems do have has_score True: self.assertEqual(problem_block.has_score, True) + @skip_unless_cms # creating child blocks only works properly in Studio + def test_xblock_metadata(self): + """ + Test the XBlock metadata API + """ + unit_block_key = library_api.create_library_block(self.library.key, "unit", "metadata-u1").usage_key + problem_key = library_api.create_library_block_child(unit_block_key, "problem", "metadata-p1").usage_key + new_olx = """ + + +

This is a normal capa problem. It has "maximum attempts" set to **5**.

+ + + XBlock metadata only + XBlock data/metadata and associated static asset files + Static asset files for XBlocks and courseware + XModule metadata only + +
+
+ """.strip() + library_api.set_library_block_olx(problem_key, new_olx) + library_api.publish_changes(self.library.key) + + # Now view the problem as Alice: + client = APIClient() + client.login(username=self.student_a.username, password='edx') + + # Check the metadata API for the unit: + metadata_view_result = client.get( + URL_BLOCK_METADATA_URL.format(block_key=unit_block_key), + {"include": "children,editable_children"}, + ) + self.assertEqual(metadata_view_result.data["children"], [str(problem_key)]) + self.assertEqual(metadata_view_result.data["editable_children"], [str(problem_key)]) + + # Check the metadata API for the problem: + metadata_view_result = client.get( + URL_BLOCK_METADATA_URL.format(block_key=problem_key), + {"include": "student_view_data,index_dictionary"}, + ) + self.assertEqual(metadata_view_result.data["block_id"], str(problem_key)) + self.assertEqual(metadata_view_result.data["display_name"], "New Multi Choice Question") + self.assertNotIn("children", metadata_view_result.data) + self.assertNotIn("editable_children", metadata_view_result.data) + self.assertDictContainsSubset({ + "content_type": "CAPA", + "problem_types": ["multiplechoiceresponse"], + }, metadata_view_result.data["index_dictionary"]) + self.assertEqual(metadata_view_result.data["student_view_data"], None) # Capa doesn't provide student_view_data + @requires_blockstore # We can remove the line below to enable this in Studio once we implement a session-backed @@ -339,6 +391,7 @@ class ContentLibraryXBlockUserStateTest(ContentLibraryContentTestMixin, TestCase student_view_result = client.get(URL_BLOCK_RENDER_VIEW.format(block_key=block_id, view_name='student_view')) problem_key = "input_{}_2_1".format(block_id) self.assertIn(problem_key, student_view_result.data["content"]) + # And submit a wrong answer: result = client.get(URL_BLOCK_GET_HANDLER_URL.format(block_key=block_id, handler_name='xmodule_handler')) problem_check_url = result.data["handler_url"] + 'problem_check' diff --git a/openedx/core/djangoapps/xblock/api.py b/openedx/core/djangoapps/xblock/api.py index a3449a198f..cef4fff332 100644 --- a/openedx/core/djangoapps/xblock/api.py +++ b/openedx/core/djangoapps/xblock/api.py @@ -79,16 +79,47 @@ def load_block(usage_key, user): return runtime.get_block(usage_key) -def get_block_metadata(block): +def get_block_metadata(block, includes=()): """ - Get metadata about the specified XBlock + Get metadata about the specified XBlock. + + This metadata is the same for all users. Any data which varies per-user must + be served from a different API. + + Optionally provide a list or set of metadata keys to include. Valid keys are: + index_dictionary: a dictionary of data used to add this XBlock's content + to a search index. + student_view_data: data needed to render the XBlock on mobile or in + custom frontends. + children: list of usage keys of the XBlock's children + editable_children: children in the same bundle, as opposed to linked + children in other bundles. """ - return { + data = { "block_id": six.text_type(block.scope_ids.usage_id), "block_type": block.scope_ids.block_type, "display_name": get_block_display_name(block), } + if "index_dictionary" in includes: + data["index_dictionary"] = block.index_dictionary() + + if "student_view_data" in includes: + data["student_view_data"] = block.student_view_data() if hasattr(block, 'student_view_data') else None + + if "children" in includes: + data["children"] = block.children if hasattr(block, 'children') else [] # List of usage keys of children + + if "editable_children" in includes: + # "Editable children" means children in the same bundle, as opposed to linked children in other bundles. + data["editable_children"] = [] + child_includes = block.runtime.child_includes_of(block) + for idx, include in enumerate(child_includes): + if include.link_id is None: + data["editable_children"].append(block.children[idx]) + + return data + def resolve_definition(block_or_key): """ diff --git a/openedx/core/djangoapps/xblock/rest_api/views.py b/openedx/core/djangoapps/xblock/rest_api/views.py index 939c663b7b..fbabcec2a1 100644 --- a/openedx/core/djangoapps/xblock/rest_api/views.py +++ b/openedx/core/djangoapps/xblock/rest_api/views.py @@ -34,10 +34,18 @@ User = get_user_model() def block_metadata(request, usage_key_str): """ Get metadata about the specified block. + + Accepts an "include" query parameter which must be a comma separated list of keys to include. Valid keys are + "index_dictionary" and "student_view_data". """ usage_key = UsageKey.from_string(usage_key_str) block = load_block(usage_key, request.user) - metadata_dict = get_block_metadata(block) + includes = request.GET.get("include", "").split(",") + metadata_dict = get_block_metadata(block, includes=includes) + if 'children' in metadata_dict: + metadata_dict['children'] = [str(key) for key in metadata_dict['children']] + if 'editable_children' in metadata_dict: + metadata_dict['editable_children'] = [str(key) for key in metadata_dict['editable_children']] return Response(metadata_dict)