Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions xblock/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from xblock.internal import class_lazy
from xblock.plugin import Plugin
from xblock.validation import Validation
from xblock.xml import is_pointer_tag

# OrderedDict is used so that namespace attributes are put in predictable order
# This allows for simple string equality assertions in tests and have no other effects
Expand All @@ -32,6 +33,7 @@
("block", "http://code.edx.org/xblock/block"),
])


# __all__ controls what classes end up in the docs.
__all__ = ['XBlock', 'XBlockAside']

Expand Down Expand Up @@ -746,6 +748,9 @@
keys (:class:`.ScopeIds`): The keys identifying where this block
will store its data.
"""
if is_pointer_tag(node):
node, _ = runtime.load_definition_xml(node, keys.def_id)

Check warning on line 752 in xblock/core.py

View check run for this annotation

Codecov / codecov/patch

xblock/core.py#L752

Added line #L752 was not covered by tests

block = runtime.construct_xblock_from_class(cls, keys)

# The base implementation: child nodes become child blocks.
Expand Down
19 changes: 19 additions & 0 deletions xblock/runtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
FieldDataDeprecationWarning,
UserIdDeprecationWarning,
)
from xblock.xml import format_filepath, load_file, name_to_pathname

log = logging.getLogger(__name__)

Expand Down Expand Up @@ -792,6 +793,24 @@
child = etree.SubElement(node, "unknown")
block.add_xml_to_node(child)

def load_definition_xml(self, node, def_id):
"""
Load an XML definition for a block.
This method can be overridden in different runtime environments.

Args:
node: XML element containing attributes for definition loading
def_id: Unique identifier for the definition being loaded

Returns:
tuple: A tuple containing the loaded XML definition and the
corresponding file path or identifier
"""
url_name = node.get('url_name')
filepath = format_filepath(node.tag, name_to_pathname(url_name))
definition_xml = load_file(filepath, self.resources_fs, def_id) # pylint: disable=no-member
return definition_xml, filepath

Check warning on line 812 in xblock/runtime.py

View check run for this annotation

Codecov / codecov/patch

xblock/runtime.py#L809-L812

Added lines #L809 - L812 were not covered by tests

# Rendering

def render(self, block, view_name, context=None):
Expand Down
63 changes: 63 additions & 0 deletions xblock/test/test_xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""
Tests for xblock/xml.py
"""

import unittest
from io import BytesIO
from lxml import etree

from xblock.xml import (
name_to_pathname,
is_pointer_tag,
format_filepath,
file_to_xml,
)


class TestPointerTagParsing(unittest.TestCase):
"""
Tests for core functions in XBlock.
"""
def test_name_to_pathname(self):
self.assertEqual(name_to_pathname("course:subcourse"), "course/subcourse")
self.assertEqual(name_to_pathname("module:lesson:part"), "module/lesson/part")
self.assertEqual(name_to_pathname("no_colon"), "no_colon")

def test_is_pointer_tag(self):
# Case 1: Valid pointer tag
xml_obj = etree.Element("some_tag", url_name="test_url")
self.assertTrue(is_pointer_tag(xml_obj))

# Case 2: Valid course pointer tag
xml_obj = etree.Element("course", url_name="test_url", course="test_course", org="test_org")
self.assertTrue(is_pointer_tag(xml_obj))

# Case 3: Invalid case - extra attribute
xml_obj = etree.Element("some_tag", url_name="test_url", extra_attr="invalid")
self.assertFalse(is_pointer_tag(xml_obj))

# Case 4: Invalid case - has text
xml_obj = etree.Element("some_tag", url_name="test_url")
xml_obj.text = "invalid_text"
self.assertFalse(is_pointer_tag(xml_obj))

# Case 5: Invalid case - has children
xml_obj = etree.Element("some_tag", url_name="test_url")
_ = etree.SubElement(xml_obj, "child")
self.assertFalse(is_pointer_tag(xml_obj))

def test_format_filepath(self):
self.assertEqual(format_filepath("course", "test_url"), "course/test_url.xml")

def test_file_to_xml(self):
"""Test that `file_to_xml` correctly parses XML from a file object."""
# Create a BytesIO object
file_obj = BytesIO(b"<root><child>Value</child></root>")

# Parse the XML
result = file_to_xml(file_obj)

# Verify the result
self.assertEqual(result.tag, 'root')
self.assertEqual(result[0].tag, 'child')
self.assertEqual(result[0].text, 'Value')
71 changes: 71 additions & 0 deletions xblock/xml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""
XML utilities for XBlock, including parsing and loading XML definitions.
"""
from lxml import etree


XML_PARSER = etree.XMLParser(dtd_validation=False, load_dtd=False, remove_blank_text=True, encoding='utf-8')


def name_to_pathname(name):
"""
Convert a location name for use in a path: replace ':' with '/'.
This allows users of the xml format to organize content into directories
"""
return name.replace(':', '/')


def is_pointer_tag(xml_obj):
"""
Check if xml_obj is a pointer tag: <blah url_name="something" />.
No children, one attribute named url_name, no text.

Special case for course roots: the pointer is
<course url_name="something" org="myorg" course="course">

xml_obj: an etree Element

Returns a bool.
"""
if xml_obj.tag != "course":
expected_attr = {'url_name'}
else:
expected_attr = {'url_name', 'course', 'org'}

actual_attr = set(xml_obj.attrib.keys())

has_text = xml_obj.text is not None and len(xml_obj.text.strip()) > 0

return len(xml_obj) == 0 and actual_attr == expected_attr and not has_text


def format_filepath(category, name):
"""
Construct a formatted filepath string based on the given category and name.
"""
return f'{category}/{name}.xml'


def load_file(filepath, fs, def_id): # pylint: disable=invalid-name
"""
Open the specified file in fs, and call cls.file_to_xml on it,
returning the lxml object.

Add details and reraise on error.
"""
try:
with fs.open(filepath) as xml_file:
return file_to_xml(xml_file)
except Exception as err:

Check warning on line 59 in xblock/xml.py

View check run for this annotation

Codecov / codecov/patch

xblock/xml.py#L56-L59

Added lines #L56 - L59 were not covered by tests
# Add info about where we are, but keep the traceback
raise Exception(f'Unable to load file contents at path {filepath} for item {def_id}: {err}') from err

Check warning on line 61 in xblock/xml.py

View check run for this annotation

Codecov / codecov/patch

xblock/xml.py#L61

Added line #L61 was not covered by tests


def file_to_xml(file_object):
"""
Used when this module wants to parse a file object to xml
that will be converted to the definition.

Returns an lxml Element
"""
return etree.parse(file_object, parser=XML_PARSER).getroot()