|
32 | 32 | ("block", "http://code.edx.org/xblock/block"),
|
33 | 33 | ])
|
34 | 34 |
|
| 35 | +XML_PARSER = etree.XMLParser(dtd_validation=False, load_dtd=False, remove_blank_text=True, encoding='utf-8') |
| 36 | + |
35 | 37 | # __all__ controls what classes end up in the docs.
|
36 | 38 | __all__ = ['XBlock', 'XBlockAside']
|
37 | 39 |
|
38 | 40 | UNSET = object()
|
39 | 41 |
|
40 | 42 |
|
| 43 | +def name_to_pathname(name): |
| 44 | + """ |
| 45 | + Convert a location name for use in a path: replace ':' with '/'. |
| 46 | + This allows users of the xml format to organize content into directories |
| 47 | + """ |
| 48 | + return name.replace(':', '/') |
| 49 | + |
| 50 | + |
| 51 | +def is_pointer_tag(xml_obj): |
| 52 | + """ |
| 53 | + Check if xml_obj is a pointer tag: <blah url_name="something" />. |
| 54 | + No children, one attribute named url_name, no text. |
| 55 | +
|
| 56 | + Special case for course roots: the pointer is |
| 57 | + <course url_name="something" org="myorg" course="course"> |
| 58 | +
|
| 59 | + xml_obj: an etree Element |
| 60 | +
|
| 61 | + Returns a bool. |
| 62 | + """ |
| 63 | + if xml_obj.tag != "course": |
| 64 | + expected_attr = {'url_name'} |
| 65 | + else: |
| 66 | + expected_attr = {'url_name', 'course', 'org'} |
| 67 | + |
| 68 | + actual_attr = set(xml_obj.attrib.keys()) |
| 69 | + |
| 70 | + has_text = xml_obj.text is not None and len(xml_obj.text.strip()) > 0 |
| 71 | + |
| 72 | + return len(xml_obj) == 0 and actual_attr == expected_attr and not has_text |
| 73 | + |
| 74 | + |
| 75 | +def load_definition_xml(node, runtime, def_id): |
| 76 | + """ |
| 77 | + Loads definition_xml stored in a dedicated file |
| 78 | + """ |
| 79 | + url_name = node.get('url_name') |
| 80 | + filepath = format_filepath(node.tag, name_to_pathname(url_name)) |
| 81 | + definition_xml = load_file(filepath, runtime.resources_fs, def_id) |
| 82 | + return definition_xml, filepath |
| 83 | + |
| 84 | + |
| 85 | +def format_filepath(category, name): |
| 86 | + return f'{category}/{name}.xml' |
| 87 | + |
| 88 | + |
| 89 | +def load_file(filepath, fs, def_id): # pylint: disable=invalid-name |
| 90 | + """ |
| 91 | + Open the specified file in fs, and call cls.file_to_xml on it, |
| 92 | + returning the lxml object. |
| 93 | +
|
| 94 | + Add details and reraise on error. |
| 95 | + """ |
| 96 | + try: |
| 97 | + with fs.open(filepath) as xml_file: |
| 98 | + return file_to_xml(xml_file) |
| 99 | + except Exception as err: # lint-amnesty, pylint: disable=broad-except |
| 100 | + # Add info about where we are, but keep the traceback |
| 101 | + raise Exception(f'Unable to load file contents at path {filepath} for item {def_id}: {err}') from err |
| 102 | + |
| 103 | + |
| 104 | +def file_to_xml(file_object): |
| 105 | + """ |
| 106 | + Used when this module wants to parse a file object to xml |
| 107 | + that will be converted to the definition. |
| 108 | +
|
| 109 | + Returns an lxml Element |
| 110 | + """ |
| 111 | + return etree.parse(file_object, parser=XML_PARSER).getroot() |
| 112 | + |
| 113 | + |
41 | 114 | class _AutoNamedFieldsMetaclass(type):
|
42 | 115 | """
|
43 | 116 | Builds classes such that their Field attributes know their own names.
|
@@ -746,6 +819,11 @@ def parse_xml(cls, node, runtime, keys):
|
746 | 819 | keys (:class:`.ScopeIds`): The keys identifying where this block
|
747 | 820 | will store its data.
|
748 | 821 | """
|
| 822 | + if is_pointer_tag(node): |
| 823 | + # new style: |
| 824 | + # read the actual definition file--named using url_name.replace(':','/') |
| 825 | + node, _ = load_definition_xml(node, runtime, keys.def_id) |
| 826 | + |
749 | 827 | block = runtime.construct_xblock_from_class(cls, keys)
|
750 | 828 |
|
751 | 829 | # The base implementation: child nodes become child blocks.
|
|
0 commit comments