build!: Switch to openedx-core (renamed from openedx-learning) Instead of installing openedx-learning==0.32.0, we install openedx-core==0.34.1. We update various class names, function names, docstrings, and comments to represent the rename: * We say "openedx-core" when referring to the whole repo or PyPI project * or occasionally "Open edX Core" if we want it to look nice in the docs. * We say "openedx_content" to refer to the Content API within openedx-core, which is actually the thing we have been calling "Learning Core" all along. * In snake-case code, it's `*_openedx_content_*`. * In camel-case code, it's `*OpenedXContent*` For consistency's sake we avoid anything else like oex_core, OeXCore, OpenEdXCore, OexContent, openedx-content, OpenEdxContent, etc. There should be no more references to learning_core, learning-core, Learning Core, Learning-Core, LC, openedx-learning, openedx_learning, etc. BREAKING CHANGE: for openedx-learning/openedx-core developers: You may need to uninstall openedx-learning and re-install openedx-core from your venv. If running tutor, you may need to un-mount openedx-learning, rename the directory to openedx-core, re-mount it, and re-build. The code APIs themselves are fully backwards-compatible. Part of: https://github.com/openedx/openedx-core/issues/470
107 lines
4.1 KiB
Python
107 lines
4.1 KiB
Python
"""Mixin classes for handling raw XML data in XBlocks."""
|
|
|
|
import logging
|
|
import re
|
|
|
|
from lxml import etree
|
|
from xblock.fields import Scope, String
|
|
|
|
from .exceptions import SerializationError
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
PRE_TAG_REGEX = re.compile(r"<pre\b[^>]*>(?:(?=([^<]+))\1|<(?!pre\b[^>]*>))*?</pre>")
|
|
|
|
|
|
class RawMixin:
|
|
"""
|
|
Common code between RawDescriptor and XBlocks converted from XModules.
|
|
"""
|
|
|
|
resources_dir = None
|
|
|
|
data = String(help="XML data for the block", default="", scope=Scope.content)
|
|
|
|
@classmethod
|
|
def definition_from_xml(cls, xml_object, system): # pylint: disable=unused-argument
|
|
"""Convert XML node into a dictionary with 'data' key for XBlock."""
|
|
return {"data": etree.tostring(xml_object, pretty_print=True, encoding="unicode")}, []
|
|
|
|
def definition_to_xml(self, resource_fs): # pylint: disable=unused-argument
|
|
"""
|
|
Return an Element if we've kept the import OLX, or None otherwise.
|
|
"""
|
|
# If there's no self.data, it means that an XBlock/XModule originally
|
|
# existed for this data at the time of import/editing, but was later
|
|
# uninstalled. RawDescriptor therefore never got to preserve the
|
|
# original OLX that came in, and we have no idea how it should be
|
|
# serialized for export. It's possible that we could do some smarter
|
|
# fallback here and attempt to extract the data, but it's reasonable
|
|
# and simpler to just skip this node altogether.
|
|
if not self.data:
|
|
log.warning(
|
|
"Could not serialize %s: No XBlock installed for '%s' tag.",
|
|
self.location,
|
|
self.location.block_type,
|
|
)
|
|
return None
|
|
|
|
# Normal case: Just echo back the original OLX we saved.
|
|
try:
|
|
return etree.fromstring(self.data)
|
|
except etree.XMLSyntaxError as err:
|
|
# Can't recover here, so just add some info and
|
|
# re-raise
|
|
lines = self.data.split("\n")
|
|
line, offset = err.position
|
|
msg = (
|
|
f"Unable to create xml for block {self.location}. "
|
|
f"Context: '{lines[line - 1][offset - 40 : offset + 40]}'"
|
|
)
|
|
raise SerializationError(self.location, msg) from err
|
|
|
|
@classmethod
|
|
def parse_xml_new_runtime(cls, node, runtime, keys):
|
|
"""
|
|
Interpret the parsed XML in `node`, creating a new instance of this
|
|
module.
|
|
"""
|
|
# In the new/openedx_content-based runtime, XModule parsing (from
|
|
# XmlMixin) is disabled, so definition_from_xml will not be
|
|
# called, and instead the "normal" XBlock parse_xml will be used.
|
|
# However, it's not compatible with RawMixin, so we implement
|
|
# support here.
|
|
data_field_value = cls.definition_from_xml(node, None)[0]["data"]
|
|
for child in node.getchildren():
|
|
node.remove(child)
|
|
# Get attributes, if any, via normal parse_xml.
|
|
try:
|
|
block = super().parse_xml_new_runtime(node, runtime, keys)
|
|
except AttributeError:
|
|
block = super().parse_xml(node, runtime, keys)
|
|
block.data = data_field_value
|
|
return block
|
|
|
|
|
|
class EmptyDataRawMixin:
|
|
"""
|
|
Common code between EmptyDataRawDescriptor and XBlocks converted from XModules.
|
|
"""
|
|
|
|
resources_dir = None
|
|
|
|
data = String(default="", scope=Scope.content)
|
|
|
|
@classmethod
|
|
def definition_from_xml(cls, xml_object, system): # pylint: disable=unused-argument
|
|
"""Convert XML node to dictionary with 'data', handling empty nodes specially."""
|
|
if len(xml_object) == 0 and len(list(xml_object.items())) == 0:
|
|
return {"data": ""}, []
|
|
return {"data": etree.tostring(xml_object, pretty_print=True, encoding="unicode")}, []
|
|
|
|
def definition_to_xml(self, resource_fs): # pylint: disable=unused-argument
|
|
"""Return an XML Element from stored data, or an empty element if data is empty."""
|
|
if self.data:
|
|
return etree.fromstring(self.data)
|
|
return etree.Element(self.category)
|