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 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 — Ω ≠ π ≡ Ω ≠ π.
+ Some more Chinese 四節比分和七年前
"""
- 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