From 6d01fbc4188bfe07acfe21669c921ad2c4a3588b Mon Sep 17 00:00:00 2001 From: Braden MacDonald Date: Thu, 20 Apr 2023 17:01:06 -0700 Subject: [PATCH] chore: consolidate two different implementations for serializing XBlocks --- .../core/djangoapps/content_libraries/api.py | 2 +- .../content_staging/block_serializer.py | 99 ----------- .../content_staging/tests/test_clipboard.py | 2 +- .../core/djangoapps/content_staging/views.py | 4 +- openedx/core/djangoapps/olx_rest_api/api.py | 21 +-- .../djangoapps/olx_rest_api/test_views.py | 6 +- openedx/core/djangoapps/olx_rest_api/views.py | 12 +- .../core/lib/xblock_serializer/__init__.py | 0 openedx/core/lib/xblock_serializer/api.py | 27 +++ .../xblock_serializer}/block_serializer.py | 155 ++++++++-------- .../core/lib/xblock_serializer/test_api.py | 167 ++++++++++++++++++ .../xblock_serializer/test_utils.py} | 10 +- .../xblock_serializer/utils.py} | 30 +++- setup.cfg | 1 + 14 files changed, 321 insertions(+), 215 deletions(-) delete mode 100644 openedx/core/djangoapps/content_staging/block_serializer.py create mode 100644 openedx/core/lib/xblock_serializer/__init__.py create mode 100644 openedx/core/lib/xblock_serializer/api.py rename openedx/core/{djangoapps/olx_rest_api => lib/xblock_serializer}/block_serializer.py (52%) create mode 100644 openedx/core/lib/xblock_serializer/test_api.py rename openedx/core/{djangoapps/olx_rest_api/test_adapters.py => lib/xblock_serializer/test_utils.py} (87%) rename openedx/core/{djangoapps/olx_rest_api/adapters.py => lib/xblock_serializer/utils.py} (85%) diff --git a/openedx/core/djangoapps/content_libraries/api.py b/openedx/core/djangoapps/content_libraries/api.py index ecb18d681729..e796beef1201 100644 --- a/openedx/core/djangoapps/content_libraries/api.py +++ b/openedx/core/djangoapps/content_libraries/api.py @@ -92,13 +92,13 @@ LIBRARY_BLOCK_UPDATED, LIBRARY_BLOCK_DELETED, ) -from openedx.core.djangoapps.olx_rest_api.api import serialize_modulestore_block_for_blockstore from openedx.core.djangoapps.xblock.api import ( get_block_display_name, get_learning_context_impl, load_block, XBlockInclude, ) +from openedx.core.lib.xblock_serializer.api import serialize_modulestore_block_for_blockstore from openedx.core.lib.blockstore_api import ( get_bundle, get_bundles, diff --git a/openedx/core/djangoapps/content_staging/block_serializer.py b/openedx/core/djangoapps/content_staging/block_serializer.py deleted file mode 100644 index e1d6d353df36..000000000000 --- a/openedx/core/djangoapps/content_staging/block_serializer.py +++ /dev/null @@ -1,99 +0,0 @@ -""" -Code for serializing a modulestore XBlock to OLX suitable for import into -Blockstore. -""" -import logging -import os -from collections import namedtuple - -from lxml import etree - -from openedx.core.djangoapps.olx_rest_api.api import adapters - -log = logging.getLogger(__name__) - -# A static file required by an XBlock -StaticFile = namedtuple('StaticFile', ['name', 'url', 'data']) - - -class XBlockSerializer: - """ - A class that can serializer an XBlock to OLX - """ - # TEMP: this needs to be consolidated with the XBlockSerializer in olx_rest_api. - # i.e. have one base serializer, and a derived blockstore serializer - - def __init__(self, block): - """ - Serialize an XBlock to an OLX string + supporting files, and store the - resulting data in this object. - """ - self.orig_block_key = block.scope_ids.usage_id - self.static_files = [] - olx_node = self.serialize_block(block) - self.olx_str = etree.tostring(olx_node, encoding="unicode", pretty_print=True) - - course_key = self.orig_block_key.course_key - # Search the OLX for references to files stored in the course's - # "Files & Uploads" (contentstore): - self.olx_str = adapters.rewrite_absolute_static_urls(self.olx_str, course_key) - for asset in adapters.collect_assets_from_text(self.olx_str, course_key): - path = asset['path'] - if path not in [sf.name for sf in self.static_files]: - self.static_files.append(StaticFile(name=path, url=asset['url'], data=None)) - - def serialize_block(self, block) -> etree.Element: - if self.orig_block_key.block_type == 'html': - return self.serialize_html_block(block) - else: - return self.serialize_normal_block(block) - - def serialize_normal_block(self, block) -> etree.Element: - """ - Serialize an XBlock to XML. - - This method is used for every block type except HTML, which uses - serialize_html_block() instead. - """ - # Create an XML node to hold the exported data - olx_node = etree.Element("root") # The node name doesn't matter: add_xml_to_node will change it - # ^ Note: We could pass nsmap=xblock.core.XML_NAMESPACES here, but the - # resulting XML namespace attributes don't seem that useful? - with adapters.override_export_fs(block) as filesystem: # Needed for XBlocks that inherit XModuleDescriptor - # Tell the block to serialize itself as XML/OLX: - if not block.has_children: - block.add_xml_to_node(olx_node) - else: - # We don't want the children serialized at this time, because - # otherwise we can't tell which files in 'filesystem' belong to - # this block and which belong to its children. So, temporarily - # disable any children: - children = block.children - block.children = [] - block.add_xml_to_node(olx_node) - block.children = children - - # Now the block may have exported addtional data as files in - # 'filesystem'. If so, store them: - for item in filesystem.walk(): # pylint: disable=not-callable - for unit_file in item.files: - file_path = os.path.join(item.path, unit_file.name) - with filesystem.open(file_path, 'rb') as fh: - data = fh.read() - self.static_files.append(StaticFile(name=unit_file.name, data=data, url=None)) - # Recursively serialize the children: - if block.has_children: - for child in block.get_children(): - child_node = self.serialize_block(child) - olx_node.append(child_node) - return olx_node - - def serialize_html_block(self, block) -> etree.Element: - """ - Special case handling for HTML blocks - """ - olx_node = etree.Element("html") - if block.display_name: - olx_node.attrib["display_name"] = block.display_name - olx_node.text = etree.CDATA("\n" + block.data + "\n") - return olx_node diff --git a/openedx/core/djangoapps/content_staging/tests/test_clipboard.py b/openedx/core/djangoapps/content_staging/tests/test_clipboard.py index 4e042199a692..1313f05d7ce6 100644 --- a/openedx/core/djangoapps/content_staging/tests/test_clipboard.py +++ b/openedx/core/djangoapps/content_staging/tests/test_clipboard.py @@ -122,7 +122,7 @@ def test_copy_html(self): # For HTML, we really want to be sure that the OLX is serialized in this exact format (using CDATA), so we check # the actual string directly rather than using assertXmlEqual(): self.assertEqual(olx_response.content.decode(), dedent(""" - Sample ]]> """).lstrip()) diff --git a/openedx/core/djangoapps/content_staging/views.py b/openedx/core/djangoapps/content_staging/views.py index 96eae3db7156..3afed9352ba0 100644 --- a/openedx/core/djangoapps/content_staging/views.py +++ b/openedx/core/djangoapps/content_staging/views.py @@ -17,11 +17,11 @@ from common.djangoapps.student.auth import has_studio_read_access from openedx.core.lib.api.view_utils import view_auth_classes +from openedx.core.lib.xblock_serializer.api import serialize_xblock_to_olx from xmodule import block_metadata_utils from xmodule.modulestore.django import modulestore from xmodule.modulestore.exceptions import ItemNotFoundError -from .block_serializer import XBlockSerializer from .models import StagedContent, UserClipboard from .serializers import UserClipboardSerializer, PostToClipboardSerializer from .tasks import delete_expired_clipboards @@ -110,7 +110,7 @@ def post(self, request): block = modulestore().get_item(usage_key) except ItemNotFoundError as exc: raise NotFound("The requested usage key does not exist.") from exc - block_data = XBlockSerializer(block) + block_data = serialize_xblock_to_olx(block) expired_ids = [] with transaction.atomic(): diff --git a/openedx/core/djangoapps/olx_rest_api/api.py b/openedx/core/djangoapps/olx_rest_api/api.py index 752e1e9ccab8..0703dfab3cac 100644 --- a/openedx/core/djangoapps/olx_rest_api/api.py +++ b/openedx/core/djangoapps/olx_rest_api/api.py @@ -1,22 +1,5 @@ """ Public Python API for the OLX REST API app """ -from .block_serializer import XBlockSerializer as _XBlockSerializer -# pylint: disable=unused-import -# 'adapters' are _temporarily_ part of the public API to keep the code DRY until -# we can consolidate the two different block_serializer implementations in -# content_staging and olx_rest_api. -from . import adapters - - -def serialize_modulestore_block_for_blockstore(block): - """ - Given a modulestore XBlock (e.g. loaded using - modulestore.get_item(block_key) - ), produce: - (1) A new definition ID for use in Blockstore - (2) an XML string defining the XBlock and referencing the IDs of its - children (but not containing the actual XML of its children) - (3) a list of any static files required by the XBlock and their URL - """ - return _XBlockSerializer(block) +# Currently there is no python API here. See openedx.core.lib.xblock_serializer +# for a python API to serialize XBlocks to OLX. diff --git a/openedx/core/djangoapps/olx_rest_api/test_views.py b/openedx/core/djangoapps/olx_rest_api/test_views.py index ba790f9d5c87..24b318ca1229 100644 --- a/openedx/core/djangoapps/olx_rest_api/test_views.py +++ b/openedx/core/djangoapps/olx_rest_api/test_views.py @@ -7,9 +7,9 @@ from openedx.core.djangolib.testing.utils import skip_unless_cms from common.djangoapps.student.roles import CourseStaffRole from common.djangoapps.student.tests.factories import CourseEnrollmentFactory, UserFactory -from xmodule.modulestore import ModuleStoreEnum # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order -from xmodule.modulestore.tests.factories import ToyCourseFactory # lint-amnesty, pylint: disable=wrong-import-order +from xmodule.modulestore import ModuleStoreEnum +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import ToyCourseFactory @skip_unless_cms diff --git a/openedx/core/djangoapps/olx_rest_api/views.py b/openedx/core/djangoapps/olx_rest_api/views.py index bb1caa306194..8977b131530b 100644 --- a/openedx/core/djangoapps/olx_rest_api/views.py +++ b/openedx/core/djangoapps/olx_rest_api/views.py @@ -11,9 +11,9 @@ from common.djangoapps.student.auth import has_studio_read_access from openedx.core.lib.api.view_utils import view_auth_classes +from xmodule.modulestore.django import modulestore -from . import adapters -from .block_serializer import XBlockSerializer +from openedx.core.lib.xblock_serializer.api import serialize_modulestore_block_for_blockstore @api_view(['GET']) @@ -47,8 +47,8 @@ def serialize_block(block_key): if block_key in serialized_blocks: return - block = adapters.get_block(block_key) - serialized_blocks[block_key] = XBlockSerializer(block) + block = modulestore().get_item(block_key) + serialized_blocks[block_key] = serialize_modulestore_block_for_blockstore(block) if block.has_children: for child_id in block.children: @@ -102,8 +102,8 @@ def get_block_exportfs_file(request, usage_key_str, path): if not has_studio_read_access(request.user, course_key): raise PermissionDenied("You must be a member of the course team in Studio to export OLX using this API.") - block = adapters.get_block(usage_key) - serialized = XBlockSerializer(block) + block = modulestore().get_item(usage_key) + serialized = serialize_modulestore_block_for_blockstore(block) static_file = None for f in serialized.static_files: if f.name == path: diff --git a/openedx/core/lib/xblock_serializer/__init__.py b/openedx/core/lib/xblock_serializer/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/openedx/core/lib/xblock_serializer/api.py b/openedx/core/lib/xblock_serializer/api.py new file mode 100644 index 000000000000..97d04580f24b --- /dev/null +++ b/openedx/core/lib/xblock_serializer/api.py @@ -0,0 +1,27 @@ +""" +Public python API for serializing XBlocks to OLX +""" +# pylint: disable=unused-import +from .block_serializer import StaticFile, XBlockSerializer, XBlockSerializerForBlockstore + + +def serialize_xblock_to_olx(block): + """ + This class will serialize an XBlock, producing: + (1) an XML string defining the XBlock and all of its children (inline) + (2) a list of any static files required by the XBlock and their URL + """ + return XBlockSerializer(block) + + +def serialize_modulestore_block_for_blockstore(block): + """ + This class will serialize an XBlock, producing: + (1) A new definition ID for use in Blockstore + (2) an XML string defining the XBlock and referencing the IDs of its + children using syntax (which doesn't actually + contain the OLX of its children, just refers to them, so you have to + separately serialize them.) + (3) a list of any static files required by the XBlock and their URL + """ + return XBlockSerializerForBlockstore(block) diff --git a/openedx/core/djangoapps/olx_rest_api/block_serializer.py b/openedx/core/lib/xblock_serializer/block_serializer.py similarity index 52% rename from openedx/core/djangoapps/olx_rest_api/block_serializer.py rename to openedx/core/lib/xblock_serializer/block_serializer.py index 4e063601aebe..92bb7475c3d6 100644 --- a/openedx/core/djangoapps/olx_rest_api/block_serializer.py +++ b/openedx/core/lib/xblock_serializer/block_serializer.py @@ -1,6 +1,5 @@ """ -Code for serializing a modulestore XBlock to OLX suitable for import into -Blockstore. +Code for serializing a modulestore XBlock to OLX. """ import logging import os @@ -8,7 +7,7 @@ from lxml import etree -from . import adapters +from . import utils log = logging.getLogger(__name__) @@ -16,33 +15,9 @@ StaticFile = namedtuple('StaticFile', ['name', 'url', 'data']) -def blockstore_def_key_from_modulestore_usage_key(usage_key): - """ - In modulestore, the "definition key" is a MongoDB ObjectID kept in split's - definitions table, which theoretically allows the same block to be used in - many places (each with a unique usage key). However, that functionality is - not exposed in Studio (other than via content libraries). So when we import - into Blockstore, we assume that each usage is unique, don't generate a usage - key, and create a new "definition key" from the original usage key. - So modulestore usage key - block-v1:A+B+C+type@html+block@introduction - will become Blockstore definition key - html/introduction - """ - block_type = usage_key.block_type - if block_type == 'vertical': - # We transform to - block_type = "unit" - return block_type + "/" + usage_key.block_id - - class XBlockSerializer: """ - This class will serialize an XBlock, producing: - (1) A new definition ID for use in Blockstore - (2) an XML string defining the XBlock and referencing the IDs of its - children (but not containing the actual XML of its children) - (3) a list of any static files required by the XBlock and their URL + A class that can serialize an XBlock to OLX. """ def __init__(self, block): @@ -52,24 +27,26 @@ def __init__(self, block): """ self.orig_block_key = block.scope_ids.usage_id self.static_files = [] - self.def_id = blockstore_def_key_from_modulestore_usage_key(self.orig_block_key) - - # Special cases: - if self.orig_block_key.block_type == 'html': - self.serialize_html_block(block) - else: - self.serialize_normal_block(block) + olx_node = self._serialize_block(block) + self.olx_str = etree.tostring(olx_node, encoding="unicode", pretty_print=True) course_key = self.orig_block_key.course_key # Search the OLX for references to files stored in the course's # "Files & Uploads" (contentstore): - self.olx_str = adapters.rewrite_absolute_static_urls(self.olx_str, course_key) - for asset in adapters.collect_assets_from_text(self.olx_str, course_key): + self.olx_str = utils.rewrite_absolute_static_urls(self.olx_str, course_key) + for asset in utils.collect_assets_from_text(self.olx_str, course_key): path = asset['path'] if path not in [sf.name for sf in self.static_files]: self.static_files.append(StaticFile(name=path, url=asset['url'], data=None)) - def serialize_normal_block(self, block): + def _serialize_block(self, block) -> etree.Element: + """ Serialize an XBlock to OLX/XML. """ + if block.scope_ids.usage_id.block_type == 'html': + return self._serialize_html_block(block) + else: + return self._serialize_normal_block(block) + + def _serialize_normal_block(self, block) -> etree.Element: """ Serialize an XBlock to XML. @@ -80,7 +57,7 @@ def serialize_normal_block(self, block): olx_node = etree.Element("root") # The node name doesn't matter: add_xml_to_node will change it # ^ Note: We could pass nsmap=xblock.core.XML_NAMESPACES here, but the # resulting XML namespace attributes don't seem that useful? - with adapters.override_export_fs(block) as filesystem: # Needed for XBlocks that inherit XModuleDescriptor + with utils.override_export_fs(block) as filesystem: # Needed for XBlocks that inherit XModuleDescriptor # Tell the block to serialize itself as XML/OLX: if not block.has_children: block.add_xml_to_node(olx_node) @@ -102,47 +79,85 @@ def serialize_normal_block(self, block): with filesystem.open(file_path, 'rb') as fh: data = fh.read() self.static_files.append(StaticFile(name=unit_file.name, data=data, url=None)) - # Apply some transformations to the OLX: - self.transform_olx(olx_node, usage_id=block.scope_ids.usage_id) - # Add tags for each child (XBlock XML export - # normally puts children inline as e.g. tags, but we want - # references to them only.) if block.has_children: - for child_id in block.children: - # In modulestore, the "definition key" is a MongoDB ObjectID - # kept in split's definitions table, which theoretically allows - # the same block to be used in many places (each with a unique - # usage key). However, that functionality is not exposed in - # Studio (other than via content libraries). So when we import - # into Blockstore, we assume that each usage is unique, don't - # generate a usage key, and create a new "definition key" from - # the original usage key. - # So modulestore usage key - # block-v1:A+B+C+type@html+block@introduction - # will become Blockstore definition key - # html+introduction - # - # If we needed the real definition key, we could get it via - # child = block.runtime.get_block(child_id) - # child_def_id = str(child.scope_ids.def_id) - # and then use - # - def_id = blockstore_def_key_from_modulestore_usage_key(child_id) - olx_node.append(olx_node.makeelement("xblock-include", {"definition": def_id})) - # Store the resulting XML as a string: - self.olx_str = etree.tostring(olx_node, encoding="unicode", pretty_print=True) + self._serialize_children(block, olx_node) + return olx_node - def serialize_html_block(self, block): + def _serialize_children(self, block, parent_olx_node): + """ + Recursively serialize the children of XBlock 'block'. + Subclasses may override this. + """ + for child in block.get_children(): + child_node = self._serialize_block(child) + parent_olx_node.append(child_node) + + def _serialize_html_block(self, block) -> etree.Element: """ Special case handling for HTML blocks """ olx_node = etree.Element("html") + olx_node.attrib["url_name"] = block.scope_ids.usage_id.block_id if block.display_name: olx_node.attrib["display_name"] = block.display_name olx_node.text = etree.CDATA("\n" + block.data + "\n") - self.olx_str = etree.tostring(olx_node, encoding="unicode", pretty_print=True) + return olx_node - def transform_olx(self, olx_node, usage_id): + +class XBlockSerializerForBlockstore(XBlockSerializer): + """ + This class will serialize an XBlock, producing: + (1) A new definition ID for use in Blockstore + (2) an XML string defining the XBlock and referencing the IDs of its + children using syntax (which doesn't actually + contain the OLX of its children, just refers to them, so you have to + separately serialize them.) + (3) a list of any static files required by the XBlock and their URL + """ + + def __init__(self, block): + """ + Serialize an XBlock to an OLX string + supporting files, and store the + resulting data in this object. + """ + super().__init__(block) + self.def_id = utils.blockstore_def_key_from_modulestore_usage_key(self.orig_block_key) + + def _serialize_block(self, block) -> etree.Element: + """ Serialize an XBlock to OLX/XML. """ + olx_node = super()._serialize_block(block) + # Apply some transformations to the OLX: + self._transform_olx(olx_node, usage_id=block.scope_ids.usage_id) + return olx_node + + def _serialize_children(self, block, parent_olx_node): + """ + Recursively serialize the children of XBlock 'block'. + Subclasses may override this. + """ + for child_id in block.children: + # In modulestore, the "definition key" is a MongoDB ObjectID + # kept in split's definitions table, which theoretically allows + # the same block to be used in many places (each with a unique + # usage key). However, that functionality is not exposed in + # Studio (other than via content libraries). So when we import + # into Blockstore, we assume that each usage is unique, don't + # generate a usage key, and create a new "definition key" from + # the original usage key. + # So modulestore usage key + # block-v1:A+B+C+type@html+block@introduction + # will become Blockstore definition key + # html+introduction + # + # If we needed the real definition key, we could get it via + # child = block.runtime.get_block(child_id) + # child_def_id = str(child.scope_ids.def_id) + # and then use + # + def_id = utils.blockstore_def_key_from_modulestore_usage_key(child_id) + parent_olx_node.append(parent_olx_node.makeelement("xblock-include", {"definition": def_id})) + + def _transform_olx(self, olx_node, usage_id): """ Apply transformations to the given OLX etree Node. """ diff --git a/openedx/core/lib/xblock_serializer/test_api.py b/openedx/core/lib/xblock_serializer/test_api.py new file mode 100644 index 000000000000..f9258f18551a --- /dev/null +++ b/openedx/core/lib/xblock_serializer/test_api.py @@ -0,0 +1,167 @@ +""" +Test for the XBlock serialization lib's API +""" +from xml.etree import ElementTree + +from openedx.core.djangolib.testing.utils import skip_unless_cms +from xmodule.modulestore.django import modulestore +from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase +from xmodule.modulestore.tests.factories import ToyCourseFactory + +from . import api + + +# The expected OLX string for the 'Toy_Videos' sequential in the toy course +EXPECTED_SEQUENTIAL_OLX = """ + + Lab 2A: Superposition Experiment + + +

Isn't the toy course great?

+ +

Let's add some markup that uses non-ascii characters. +'For example, we should be able to write words like encyclopædia, or foreign words like français. +Looking beyond latin-1, we should handle math symbols: πr² ≤ ∞. +And it shouldn't matter if we use entities or numeric codes — Ω ≠ π ≡ Ω ≠ π. +

+ + +]]> + This is a link to another page and some Chinese 四節比分和七年前

Some more Chinese 四節比分和七年前

+ +]]> + Sample +]]> + link + +]]> + link + + +]]> + + +]]> + Red text here

+]]> + +]]> +
+""" + + +@skip_unless_cms +class XBlockSerializationTestCase(SharedModuleStoreTestCase): + """ + Test for the XBlock serialization library's python API + """ + + @classmethod + def setUpClass(cls): + """ + Set up a course for use in these tests + """ + super().setUpClass() + cls.course = ToyCourseFactory.create() + + def assertXmlEqual(self, xml_str_a: str, xml_str_b: str) -> bool: + """ Assert that the given XML strings are equal, ignoring attribute order and some whitespace variations. """ + self.assertEqual( + ElementTree.canonicalize(xml_str_a, strip_text=True), + ElementTree.canonicalize(xml_str_b, strip_text=True), + ) + + def test_html_with_static_asset(self): + """ + Test that HTML gets converted to use CDATA and static assets are + handled. + """ + block_id = self.course.id.make_usage_key('html', 'just_img') # see sample_courses.py + html_block = modulestore().get_item(block_id) + serialized = api.serialize_xblock_to_olx(html_block) + + self.assertXmlEqual( + serialized.olx_str, + """ + + ]]> + """ + ) + self.assertIn("CDATA", serialized.olx_str) + self.assertEqual(serialized.static_files, [ + api.StaticFile( + name="foo_bar.jpg", + url="/asset-v1:edX+toy+2012_Fall+type@asset+block@foo_bar.jpg", + data=None, + ), + ]) + + def test_html_with_static_asset_blockstore(self): + """ + Test the blockstore-specific serialization of an HTML block + """ + block_id = self.course.id.make_usage_key('html', 'just_img') # see sample_courses.py + html_block = modulestore().get_item(block_id) + serialized = api.serialize_xblock_to_olx(html_block) + serialized_blockstore = api.serialize_modulestore_block_for_blockstore(html_block) + self.assertXmlEqual( + serialized_blockstore.olx_str, + # For blockstore, OLX should never contain "url_name" as that ID is specified by the filename: + """ + + ]]> + """ + ) + self.assertIn("CDATA", serialized.olx_str) + # Static files should be identical: + self.assertEqual(serialized.static_files, serialized_blockstore.static_files) + # This is the only other difference - an extra field with the blockstore-specific definition ID: + self.assertEqual(serialized_blockstore.def_id, "html/just_img") + + def test_export_sequential(self): + """ + Export a sequential from the toy course, including all of its children. + """ + sequential_id = self.course.id.make_usage_key('sequential', 'Toy_Videos') # see sample_courses.py + sequential = modulestore().get_item(sequential_id) + serialized = api.serialize_xblock_to_olx(sequential) + + self.assertXmlEqual(serialized.olx_str, EXPECTED_SEQUENTIAL_OLX) + + def test_export_sequential_blockstore(self): + """ + Export a sequential from the toy course, formatted for blockstore. + """ + sequential_id = self.course.id.make_usage_key('sequential', 'Toy_Videos') # see sample_courses.py + sequential = modulestore().get_item(sequential_id) + serialized = api.serialize_modulestore_block_for_blockstore(sequential) + + self.assertXmlEqual(serialized.olx_str, """ + + + + + + + + + + + + """) diff --git a/openedx/core/djangoapps/olx_rest_api/test_adapters.py b/openedx/core/lib/xblock_serializer/test_utils.py similarity index 87% rename from openedx/core/djangoapps/olx_rest_api/test_adapters.py rename to openedx/core/lib/xblock_serializer/test_utils.py index ec029758c3c2..5517a692732b 100644 --- a/openedx/core/djangoapps/olx_rest_api/test_adapters.py +++ b/openedx/core/lib/xblock_serializer/test_utils.py @@ -1,16 +1,16 @@ """ -Test the OLX REST API adapters code +Test the OLX serialization utils """ import unittest from opaque_keys.edx.keys import CourseKey -from openedx.core.djangoapps.olx_rest_api import adapters +from . import utils -class TestAdapters(unittest.TestCase): +class TestUtils(unittest.TestCase): """ - Test the OLX REST API adapters code + Test the OLX serialization utils """ def test_rewrite_absolute_static_urls(self): @@ -45,5 +45,5 @@ def test_rewrite_absolute_static_urls(self): """ - olx_out = adapters.rewrite_absolute_static_urls(olx_in, course_id) + olx_out = utils.rewrite_absolute_static_urls(olx_in, course_id) assert olx_out == olx_expected diff --git a/openedx/core/djangoapps/olx_rest_api/adapters.py b/openedx/core/lib/xblock_serializer/utils.py similarity index 85% rename from openedx/core/djangoapps/olx_rest_api/adapters.py rename to openedx/core/lib/xblock_serializer/utils.py index cc5c59f896a4..fd2253ccad44 100644 --- a/openedx/core/djangoapps/olx_rest_api/adapters.py +++ b/openedx/core/lib/xblock_serializer/utils.py @@ -1,5 +1,5 @@ """ -Helpers required to adapt to differing APIs +Helper functions for XBlock serialization """ import logging import re @@ -12,7 +12,6 @@ from xmodule.assetstore.assetmgr import AssetManager from xmodule.contentstore.content import StaticContent from xmodule.exceptions import NotFoundError -from xmodule.modulestore.django import modulestore as store from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.xml_block import XmlMixin @@ -21,13 +20,6 @@ log = logging.getLogger(__name__) -def get_block(usage_key): - """ - Return an XBlock from modulestore. - """ - return store().get_item(usage_key) - - def get_asset_content_from_path(course_key, asset_path): """ Locate the given asset content, load it into memory, and return it. @@ -137,3 +129,23 @@ def override_export_fs(block): if hasattr(block, 'export_to_file'): block.export_to_file = old_export_to_file XmlMixin.export_to_file = old_global_export_to_file + + +def blockstore_def_key_from_modulestore_usage_key(usage_key): + """ + In modulestore, the "definition key" is a MongoDB ObjectID kept in split's + definitions table, which theoretically allows the same block to be used in + many places (each with a unique usage key). However, that functionality is + not exposed in Studio (other than via content libraries). So when we import + into Blockstore, we assume that each usage is unique, don't generate a usage + key, and create a new "definition key" from the original usage key. + So modulestore usage key + block-v1:A+B+C+type@html+block@introduction + will become Blockstore definition key + html/introduction + """ + block_type = usage_key.block_type + if block_type == 'vertical': + # We transform to + block_type = "unit" + return block_type + "/" + usage_key.block_id diff --git a/setup.cfg b/setup.cfg index 7fcb03ff1530..e588252266cb 100644 --- a/setup.cfg +++ b/setup.cfg @@ -164,6 +164,7 @@ isolated_apps = openedx.core.djangoapps.content_libraries openedx.core.djangoapps.olx_rest_api openedx.core.djangoapps.xblock + openedx.core.lib.xblock_serializer allowed_modules = # Only imports from api.py are allowed elsewhere in the code # See https://open-edx-proposals.readthedocs.io/en/latest/best-practices/oep-0049-django-app-patterns.html#api-py