Merge pull request #29517 from open-craft/jill/lx-2365

[LX-2365] allows XBlock API users to optionally use LabXchange block types
This commit is contained in:
Braden MacDonald
2022-01-20 09:41:52 -08:00
committed by GitHub
8 changed files with 472 additions and 12 deletions

View File

@@ -267,6 +267,12 @@ TEST_ELASTICSEARCH_USE_SSL = os.environ.get(
TEST_ELASTICSEARCH_HOST = os.environ.get('EDXAPP_TEST_ELASTICSEARCH_HOST', 'edx.devstack.elasticsearch710')
TEST_ELASTICSEARCH_PORT = int(os.environ.get('EDXAPP_TEST_ELASTICSEARCH_PORT', '9200'))
############################# TEMPLATE CONFIGURATION #############################
# Adds mako template dirs for content_libraries tests
MAKO_TEMPLATE_DIRS_BASE.append(
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates'
)
########################## AUTHOR PERMISSION #######################
FEATURES['ENABLE_CREATOR_GROUP'] = False

96
common/test/problem.html Normal file
View File

@@ -0,0 +1,96 @@
<!--
common/test/problem.html
Placeholder template for openedx content_library tests -->
<%page expression_filter="h"/>
<%!
from django.utils.translation import ngettext, gettext as _
from openedx.core.djangolib.markup import HTML
%>
<%namespace name='static' file='static_content.html'/>
<h3 class="hd hd-3 problem-header" id="${ short_id }-problem-title" aria-describedby="${ id }-problem-progress" tabindex="-1">
${ problem['name'] }
</h3>
<div class="problem-progress" id="${ id }-problem-progress"></div>
<div class="problem">
${ HTML(problem['html']) }
<div class="action">
<input type="hidden" name="problem_id" value="${ problem['name'] }" />
<div class="problem-action-buttons-wrapper">
% if demand_hint_possible:
<span class="problem-action-button-wrapper">
<button type="button" class="hint-button problem-action-btn btn-link btn-small" data-value="${_('Hint')}" ${'' if should_enable_next_hint else 'disabled'}>${_('Hint')}</button>
</span>
% endif
% if save_button:
<span class="problem-action-button-wrapper">
<button type="button" class="save problem-action-btn btn-link btn-small" data-value="${_('Save')}">
<span aria-hidden="true">${_('Save')}</span>
<span class="sr">${_("Save your answer")}</span>
</button>
</span>
% endif
% if reset_button:
<span class="problem-action-button-wrapper">
<button type="button" class="reset problem-action-btn btn-link btn-small" data-value="${_('Reset')}"><span aria-hidden="true">${_('Reset')}</span><span class="sr">${_("Reset your answer")}</span></button>
</span>
% endif
% if answer_available:
<span class="problem-action-button-wrapper">
<button type="button" class="show problem-action-btn btn-link btn-small" aria-describedby="${ short_id }-problem-title"><span class="show-label">${_('Show answer')}</span></button>
</span>
% endif
</div>
<div class="submit-attempt-container">
<button type="button" class="submit btn-brand" data-submitting="${ submit_button_submitting }" data-value="${ submit_button }" data-should-enable-submit-button="${ should_enable_submit_button }" aria-describedby="submission_feedback_${short_id}" ${'' if should_enable_submit_button else 'disabled'}>
<span class="submit-label">${ submit_button }</span>
</button>
% if submit_disabled_cta:
% if submit_disabled_cta.get('event_data'):
<button class="submit-cta-link-button btn-link btn-small" onclick="emit_event(${submit_disabled_cta['event_data']})">
${submit_disabled_cta['link_name']}
</button>
<span class="submit-cta-description" tabindex="0" role="note" aria-label="description">
<span data-tooltip="${submit_disabled_cta['description']}" data-tooltip-show-on-click="true"
class="fa fa-info-circle fa-lg" aria-hidden="true">
</span>
</span>
<span class="sr">(${submit_disabled_cta['description']})</span>
% else:
<form class="submit-cta" method="post" action="${submit_disabled_cta['link']}">
<input type="hidden" id="csrf_token" name="csrfmiddlewaretoken" value="${csrf_token}">
% for form_name, form_value in submit_disabled_cta['form_values'].items():
<input type="hidden" name="${form_name}" value="${form_value}">
% endfor
<button class="submit-cta-link-button btn-link btn-small">
${submit_disabled_cta['link_name']}
</button>
<span class="submit-cta-description" tabindex="0" role="note" aria-label="description">
<span data-tooltip="${submit_disabled_cta['description']}" data-tooltip-show-on-click="true"
class="fa fa-info-circle fa-lg" aria-hidden="true">
</span>
</span>
<span class="sr">(${submit_disabled_cta['description']})</span>
</form>
% endif
% endif
<div class="submission-feedback ${'cta-enabled' if submit_disabled_cta else ''}" id="submission_feedback_${short_id}">
## When attempts are not 0, the CTA above will contain a message about the number of used attempts
% if attempts_allowed and (not submit_disabled_cta or attempts_used == 0):
${ngettext("You have used {num_used} of {num_total} attempt", "You have used {num_used} of {num_total} attempts", attempts_allowed).format(num_used=attempts_used, num_total=attempts_allowed)}
% endif
<span class="sr">${_("Some problems have options such as save, reset, hints, or show answer. These options follow the Submit button.")}</span>
</div>
</div>
</div>
</div>
<script>
function emit_event(message) {
parent.postMessage(message, '*');
}
</script>

View File

@@ -0,0 +1,16 @@
<!--
common/tests/problem_ajax.html
Placeholder template for openedx content_library tests -->
<div id="problem_${element_id}" class="problems-wrapper" role="group"
aria-labelledby="${element_id}-problem-title"
data-problem-id="${id}" data-url="${ajax_url}"
data-problem-score="${current_score}"
data-problem-total-possible="${total_possible}"
data-attempts-used="${attempts_used}"
data-content="${content | h}"
data-graded="${graded}">
<p class="loading-spinner">
<i class="fa fa-spinner fa-pulse fa-2x fa-fw"></i>
<span class="sr">Loading&hellip;</span>
</p>
</div>

122
common/test/video.html Normal file
View File

@@ -0,0 +1,122 @@
<!--
common/test/video.html
Placeholder template for openedx content_library tests -->
<%page expression_filter="h"/>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.js_utils import (
dump_js_escaped_json, js_escaped_string
)
%>
% if display_name is not UNDEFINED and display_name is not None:
<h3 class="hd hd-2">${display_name}</h3>
% endif
<div
id="video_${id}"
class="video closed"
data-metadata='${metadata}'
data-bumper-metadata='${bumper_metadata}'
data-autoadvance-enabled="${autoadvance_enabled}"
data-poster='${poster}'
tabindex="-1"
>
<div class="focus_grabber first"></div>
<div class="tc-wrapper">
<div class="video-wrapper">
<span tabindex="0" class="spinner" aria-hidden="false" aria-label="${_('Loading video player')}"></span>
<span tabindex="-1" class="btn-play fa fa-youtube-play fa-2x is-hidden" aria-hidden="true" aria-label="${_('Play video')}"></span>
<div class="video-player-pre"></div>
<div class="video-player">
<div id="${id}"></div>
<h4 class="hd hd-4 video-error is-hidden">${_('No playable video sources found.')}</h4>
<h4 class="hd hd-4 video-hls-error is-hidden">
${_('Your browser does not support this video format. Try using a different browser.')}
</h4>
</div>
<div class="video-player-post"></div>
<div class="closed-captions"></div>
<div class="video-controls is-hidden">
<div>
<div class="vcr"><div class="vidtime">0:00 / 0:00</div></div>
<div class="secondary-controls"></div>
</div>
</div>
</div>
</div>
<div class="focus_grabber last"></div>
% if download_video_link or track or handout or branding_info:
<h3 class="hd hd-4 downloads-heading sr" id="video-download-transcripts_${id}">${_('Downloads and transcripts')}</h3>
<div class="wrapper-downloads" role="region" aria-labelledby="video-download-transcripts_${id}">
% if download_video_link:
<div class="wrapper-download-video">
<h4 class="hd hd-5">${_('Video')}</h4>
<a class="btn-link video-sources video-download-button" href="${download_video_link}">
${_('Download video file')}
</a>
</div>
% endif
% if track:
<div class="wrapper-download-transcripts">
<h4 class="hd hd-5">${_('Transcripts')}</h4>
% if transcript_download_format:
<ul class="list-download-transcripts">
% for item in transcript_download_formats_list:
<li class="transcript-option">
<% dname = _("Download {file}").format(file=item['display_name']) %>
<a class="btn btn-link" href="${track}" data-value="${item['value']}">${dname}</a>
</li>
% endfor
</ul>
% else:
<a class="btn-link external-track" href="${track}">${_('Download transcript')}</a>
% endif
</div>
% endif
% if handout:
<div class="wrapper-handouts">
<h4 class="hd hd-5">${_('Handouts')}</h4>
<a class="btn-link" href="${handout}">${_('Download Handout')}</a>
</div>
% endif
% if branding_info:
<div class="branding">
<span class="host-tag">${branding_info['logo_tag']}</span>
<a href="${branding_info['url']}"><img class="brand-logo" src="${branding_info['logo_src']}" alt="${branding_info['logo_tag']}" /></a>
</div>
% endif
</div>
% endif
</div>
% if cdn_eval:
<script>
//TODO: refactor this js into a separate file.
function sendPerformanceBeacon(id, expgroup, value, event_name) {
var data = {event: event_name, id: id, expgroup: expgroup, value: value, page: "html5vid"};
$.ajax({method: "POST", url: "/performance", data: data});
}
var cdnStartTime;
var salt = Math.floor((1 + Math.random()) * 0x100000).toString(36);
var id = "${id | n, js_escaped_string}";
function initializeCDNExperiment() {
sendPerformanceBeacon(id + "_" + salt, ${cdn_exp_group | n, dump_js_escaped_json}, "", "load");
cdnStartTime = Date.now();
$.each(['loadstart', 'abort', 'error', 'stalled', 'loadedmetadata',
'loadeddata', 'canplay', 'canplaythrough', 'seeked'],
function(index, eventName) {
$("#video_" + id).bind("html5:" + eventName, null, function() {
timeElapsed = Date.now() - cdnStartTime;
sendPerformanceBeacon(id + "_" + salt, ${cdn_exp_group | n, dump_js_escaped_json}, timeElapsed, eventName);
});
});
}
$("#video_" + id).bind("initialize", null, initializeCDNExperiment);
if ($("#video_" + id).hasClass("is-initialized")) {
initializeCDNExperiment();
}
</script>
% endif;

View File

@@ -3,7 +3,9 @@ Tests for Blockstore-based Content Libraries
"""
from uuid import UUID
from unittest.mock import patch
from urllib.parse import urlparse, parse_qsl
import json
import ddt
from django.conf import settings
from django.contrib.auth.models import Group
@@ -11,6 +13,9 @@ from django.test.client import Client
from django.test.utils import override_settings
from organizations.models import Organization
from rest_framework.test import APITestCase
from web_fragments.fragment import Fragment
from webob import Response
from xblock.core import XBlock
from openedx.core.djangoapps.content_libraries.libraries_index import LibraryBlockIndexer, ContentLibraryIndexer
from openedx.core.djangoapps.content_libraries.tests.base import (
@@ -20,6 +25,7 @@ from openedx.core.djangoapps.content_libraries.tests.base import (
URL_BLOCK_RENDER_VIEW,
URL_BLOCK_GET_HANDLER_URL,
URL_BLOCK_XBLOCK_HANDLER,
URL_LIB_BLOCK_OLX,
)
from openedx.core.djangoapps.content_libraries.constants import VIDEO, COMPLEX, PROBLEM, CC_4_BY, ALL_RIGHTS_RESERVED
from openedx.core.djangolib.blockstore_cache import cache
@@ -903,3 +909,164 @@ class ContentLibraryXBlockValidationTest(APITestCase):
self.assertEqual(response.json(), {
'detail': f"XBlock {valid_not_found_key} does not exist, or you don't have permission to view it.",
})
class AltBlock(XBlock):
"""Class for testing LabXchange XBlock type overrides."""
@XBlock.handler
def student_view_user_state(self, request, suffix=""):
"""
Returns a JSON response for testing.
"""
view_state = {
"id": str(self.location),
"block_type": str(self.location.block_type),
"override_type": str(self.__class__),
}
return Response(
json.dumps(view_state),
content_type='application/json',
charset='UTF-8',
)
def student_view(self, context=None):
"""
Returns an HTML fragment for testing.
"""
return Fragment(f"<div data-usage='{self.location}' data-block-type='{self.location.block_type}'>"
"<div class='AltBlock-wrapper'/></div>")
@ddt.ddt
@elasticsearch_test
class ContentLibrariesXBlockTypeOverrideTest(ContentLibrariesRestApiTest):
"""
Tests for Blockstore-based Content Libraries XBlock API,
where the expected XBlock type returned is overridden in the request.
"""
BLOCK_DATA = (
('block-wo-override', {}, 'video'),
('block-w-override', {'lx_block_types': '1'}, 'alt-block'),
)
def setUp(self):
super().setUp()
if settings.ENABLE_ELASTICSEARCH_FOR_TESTS:
ContentLibraryIndexer.remove_all_items()
LibraryBlockIndexer.remove_all_items()
self.olx = """
<video display_name="Test Block Type Overrides"
youtube_id_1_0="rE42zZ-3wNo"
transcripts="{&quot;en&quot;: &quot;transcript.srt&quot;}" />
""".strip()
def create_block(self, slug, block_type='video'):
"""
Add a new library containing a block, using the given slug to keep them unique.
"""
lib = self._create_library(slug=slug, title='Test Block Type Overrides', library_type=COMPLEX)
block = self._add_block_to_library(lib['id'], block_type, slug)
self._set_library_block_olx(block['id'], self.olx)
self._commit_library_changes(lib['id'])
return block['id']
@ddt.data(*BLOCK_DATA)
@ddt.unpack
@patch("openedx.core.djangoapps.xblock.rest_api.views.LX_BLOCK_TYPES_OVERRIDE", {'video': 'alt-block'})
@XBlock.register_temp_plugin(AltBlock, 'alt-block')
def test_block_type_metadata(self, slug, api_args, expected_type):
"""
Check that the metadata API returns the overridden block type.
"""
block_key = self.create_block(f"metadata-{slug}")
response = self.client.get(
URL_BLOCK_METADATA_URL.format(block_key=block_key),
api_args,
)
assert response.data['block_id'] == str(block_key)
assert response.data['block_type'] == expected_type
assert response.data['display_name'] == 'Test Block Type Overrides'
@ddt.data(*BLOCK_DATA)
@ddt.unpack
@patch("openedx.core.djangoapps.xblock.rest_api.views.LX_BLOCK_TYPES_OVERRIDE", {'video': 'alt-block'})
@XBlock.register_temp_plugin(AltBlock, 'alt-block')
def test_block_type_olx(self, slug, api_args, expected_type):
"""
Check that the OLX API is unchanged when overriding the block type.
"""
block_key = self.create_block(f"olx-{slug}")
response = self.client.get(
URL_LIB_BLOCK_OLX.format(block_key=block_key),
api_args,
)
assert response.data['olx'] == self.olx
@ddt.data(*BLOCK_DATA)
@ddt.unpack
@patch("openedx.core.djangoapps.xblock.rest_api.views.LX_BLOCK_TYPES_OVERRIDE", {'video': 'alt-block'})
@XBlock.register_temp_plugin(AltBlock, 'alt-block')
def test_block_type_render(self, slug, api_args, expected_type):
"""
Check that the rendered block HTML uses the overridden block type.
"""
block_key = self.create_block(f"render-{slug}")
response = self.client.get(
URL_BLOCK_RENDER_VIEW.format(block_key=block_key, view_name='student_view'),
api_args,
)
assert response.data['block_id'] == str(block_key)
assert response.data['block_type'] == expected_type
assert response.data['display_name'] == 'Test Block Type Overrides'
assert f"data-usage='{block_key}'" in response.data['content']
assert f"data-block-type='{expected_type}'" in response.data['content']
if expected_type == 'video':
assert 'class="video-wrapper"' in response.data['content']
else:
assert "class='AltBlock-wrapper'" in response.data['content']
@ddt.data(*BLOCK_DATA)
@ddt.unpack
@patch("openedx.core.djangoapps.xblock.rest_api.views.LX_BLOCK_TYPES_OVERRIDE", {'video': 'alt-block'})
@XBlock.register_temp_plugin(AltBlock, 'alt-block')
def test_block_type_handler(self, slug, api_args, expected_type):
# Check that the handler_url contains the block type override params
block_key = self.create_block(f"handler-{slug}")
response = self.client.get(
URL_BLOCK_GET_HANDLER_URL.format(block_key=block_key, handler_name='student_view_user_state'),
api_args,
)
handler_url = response.data['handler_url']
parsed_url = urlparse(handler_url)
parsed_qs = dict(parse_qsl(parsed_url.query))
assert parsed_qs == api_args
# Ensure the invoked handler hits the expected Block
if expected_type == 'video':
expected_response = {
'all_sources': [],
'duration': None,
'encoded_videos': {
'youtube': {
'file_size': 0,
'url': 'https://www.youtube.com/watch?v=rE42zZ-3wNo',
}
},
'only_on_web': False,
'saved_video_position': 0.0,
'speed': None,
'transcripts': {},
}
else:
expected_response = {
"id": f'lb:CL-TEST:handler-{slug}:video:handler-{slug}',
"block_type": 'video',
"override_type": "<class 'xblock.internal.AltBlockWithMixins'>",
}
response = self.client.post(handler_url).json()
# Can't match the Transcripts download URL exactly, but we can check that it's there and roughly correct
if 'transcripts' in response:
assert f"lb:CL-TEST:handler-{slug}:video:handler-{slug}" in response['transcripts']['en']
del response['transcripts']['en']
assert response == expected_response

View File

@@ -10,6 +10,7 @@ Studio APIs cover use cases like adding/deleting/editing blocks.
import logging
import threading
from urllib.parse import urlencode
from django.urls import reverse
from django.utils.translation import gettext as _
@@ -58,7 +59,7 @@ def get_runtime_system():
return getattr(get_runtime_system, cache_name)
def load_block(usage_key, user):
def load_block(usage_key, user, block_type_overrides=None):
"""
Load the specified XBlock for the given user.
@@ -67,6 +68,12 @@ def load_block(usage_key, user):
Exceptions:
NotFound - if the XBlock doesn't exist or if the user doesn't have the
necessary permissions
Args:
usage_key(OpaqueKey): block identifier
user(User): user requesting the block
block_type_overrides(dict): optional dict of block types to override in returned block metadata:
{'from_block_type': 'to_block_type'}
"""
# Is this block part of a course, a library, or what?
# Get the Learning Context Implementation based on the usage key
@@ -85,7 +92,7 @@ def load_block(usage_key, user):
runtime = get_runtime_system().get_runtime(user=user)
return runtime.get_block(usage_key)
return runtime.get_block(usage_key, block_type_overrides=block_type_overrides)
def get_block_metadata(block, includes=()):
@@ -215,7 +222,7 @@ def render_block_view(block, view_name, user): # pylint: disable=unused-argumen
return fragment
def get_handler_url(usage_key, handler_name, user):
def get_handler_url(usage_key, handler_name, user, extra_params=None):
"""
A method for getting the URL to any XBlock handler. The URL must be usable
without any authentication (no cookie, no OAuth/JWT), and may expire. (So
@@ -232,6 +239,7 @@ def get_handler_url(usage_key, handler_name, user):
usage_key - Usage Key (Opaque Key object or string)
handler_name - Name of the handler or a dummy name like 'any_handler'
user - Django User (registered or anonymous)
extra_params - Optional extra params to append to the handler_url (dict)
This view does not check/care if the XBlock actually exists.
"""
@@ -255,8 +263,11 @@ def get_handler_url(usage_key, handler_name, user):
'secure_token': secure_token,
'handler_name': handler_name,
})
qstring = urlencode(extra_params) if extra_params else ''
if qstring:
qstring = '?' + qstring
# We must return an absolute URL. We can't just use
# rest_framework.reverse.reverse to get the absolute URL because this method
# can be called by the XBlock from python as well and in that case we don't
# have access to the request.
return site_root_url + path
return site_root_url + path + qstring

View File

@@ -29,11 +29,31 @@ from ..utils import validate_secure_token_for_xblock_handler
User = get_user_model()
LX_BLOCK_TYPES_OVERRIDE = {
'problem': 'lx_question',
'video': 'lx_video',
'html': 'lx_html',
}
class InvalidNotFound(NotFound):
default_detail = "Invalid XBlock key"
def _block_type_overrides(request_args):
"""
If the request contains the argument `lx_block_types=1`, then
returns a dict of LabXchange block types, which override the default block types.
Otherwise, returns None.
FYI: This is a temporary change, added to assist LabXchange with the transition to using their custom runtime.
"""
if request_args.get('lx_block_types'):
return LX_BLOCK_TYPES_OVERRIDE
return None
@api_view(['GET'])
@view_auth_classes(is_authenticated=False)
@permission_classes((permissions.AllowAny, )) # Permissions are handled at a lower level, by the learning context
@@ -41,15 +61,19 @@ 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".
Accepts the following query parameters:
* "include": a comma-separated list of keys to include.
Valid keys are "index_dictionary" and "student_view_data".
* "lx_block_types": optional boolean; set to use the LabXchange XBlock classes to load the requested block.
The block ID and OLX remain unchanged; they will use the original block type.
"""
try:
usage_key = UsageKey.from_string(usage_key_str)
except InvalidKeyError as e:
raise InvalidNotFound from e
block = load_block(usage_key, request.user)
block = load_block(usage_key, request.user, block_type_overrides=_block_type_overrides(request.GET))
includes = request.GET.get("include", "").split(",")
metadata_dict = get_block_metadata(block, includes=includes)
if 'children' in metadata_dict:
@@ -65,13 +89,17 @@ def block_metadata(request, usage_key_str):
def render_block_view(request, usage_key_str, view_name):
"""
Get the HTML, JS, and CSS needed to render the given XBlock.
Accepts the following query parameters:
* "lx_block_types": optional boolean; set to use the LabXchange XBlock classes to load the requested block.
The block ID and OLX remain unchanged; they will use the original block type.
"""
try:
usage_key = UsageKey.from_string(usage_key_str)
except InvalidKeyError as e:
raise InvalidNotFound from e
block = load_block(usage_key, request.user)
block = load_block(usage_key, request.user, block_type_overrides=_block_type_overrides(request.GET))
fragment = _render_block_view(block, view_name, request.user)
response_data = get_block_metadata(block)
response_data.update(fragment.to_dict())
@@ -86,13 +114,17 @@ def get_handler_url(request, usage_key_str, handler_name):
the given XBlock handler.
The URL will expire but is guaranteed to be valid for a minimum of 2 days.
The following query parameters will be appended to the returned handler_url:
* "lx_block_types": optional boolean; set to use the LabXchange XBlock classes to load the requested block.
The block ID and OLX remain unchanged; they will use the original block type.
"""
try:
usage_key = UsageKey.from_string(usage_key_str)
except InvalidKeyError as e:
raise InvalidNotFound from e
handler_url = _get_handler_url(usage_key, handler_name, request.user)
handler_url = _get_handler_url(usage_key, handler_name, request.user, request.GET)
return Response({"handler_url": handler_url})
@@ -109,6 +141,11 @@ def xblock_handler(request, user_id, secure_token, usage_key_str, handler_name,
This endpoint has a unique authentication scheme that involves a temporary
auth token included in the URL (see below). As a result it can be exempt
from CSRF, session auth, and JWT/OAuth.
Accepts the following query parameters (in addition to those passed to the handler):
* "lx_block_types": optional boolean; set to use the LabXchange XBlock classes to load the requested block.
The block ID and OLX remain unchanged; they will use the original block type.
"""
try:
usage_key = UsageKey.from_string(usage_key_str)
@@ -149,7 +186,7 @@ def xblock_handler(request, user_id, secure_token, usage_key_str, handler_name,
raise AuthenticationFailed("Invalid user ID format.")
request_webob = DjangoWebobRequest(request) # Convert from django request to the webob format that XBlocks expect
block = load_block(usage_key, user)
block = load_block(usage_key, user, block_type_overrides=_block_type_overrides(request.GET))
# Run the handler, and save any resulting XBlock field value changes:
response_webob = block.handle(handler_name, request_webob, suffix)
response = webob_to_django_response(response_webob)

View File

@@ -33,11 +33,14 @@ class BlockstoreXBlockRuntime(XBlockRuntime):
def parse_xml_file(self, fileobj, id_generator=None):
raise NotImplementedError("Use parse_olx_file() instead")
def get_block(self, usage_id, for_parent=None):
def get_block(self, usage_id, for_parent=None, block_type_overrides=None): # pylint: disable=arguments-differ
"""
Create an XBlock instance in this runtime.
The `usage_id` is used to find the XBlock class and data.
Args:
usage_key(OpaqueKey): identifier used to find the XBlock class and data.
block_type_overrides(dict): optional dict of block types to override in returned block metadata:
{'from_block_type': 'to_block_type'}
"""
def_id = self.id_reader.get_definition_id(usage_id)
if def_id is None:
@@ -46,6 +49,8 @@ class BlockstoreXBlockRuntime(XBlockRuntime):
raise TypeError("This runtime can only load blocks stored in Blockstore bundles.")
try:
block_type = self.id_reader.get_block_type(def_id)
if block_type_overrides and block_type in block_type_overrides:
block_type = block_type_overrides[block_type]
except NoSuchDefinition:
raise NoSuchUsage(repr(usage_id)) # lint-amnesty, pylint: disable=raise-missing-from
keys = ScopeIds(self.user_id, block_type, def_id, usage_id)