diff --git a/common/lib/xmodule/tests/test_import.py b/common/lib/xmodule/tests/test_import.py new file mode 100644 index 0000000000..c0bd9af3d0 --- /dev/null +++ b/common/lib/xmodule/tests/test_import.py @@ -0,0 +1,38 @@ +from path import path + +import unittest + +from xmodule.x_module import XMLParsingSystem, XModuleDescriptor +from xmodule.errorhandlers import ignore_errors_handler +from xmodule.modulestore import Location + +class ImportTestCase(unittest.TestCase): + '''Make sure module imports work properly, including for malformed inputs''' + + def test_fallback(self): + '''Make sure that malformed xml loads as a MalformedDescriptorb.''' + + bad_xml = '''''' + + # Shouldn't need any system params, because the initial parse should fail + def load_item(loc): + raise Exception("Shouldn't be called") + + resources_fs = None + + def process_xml(xml): + raise Exception("Shouldn't be called") + + + def render_template(template, context): + raise Exception("Shouldn't be called") + + system = XMLParsingSystem(load_item, resources_fs, + ignore_errors_handler, process_xml) + system.render_template = render_template + + descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course', + None) + + self.assertEqual(descriptor.__class__.__name__, + 'MalformedDescriptor') diff --git a/common/lib/xmodule/xmodule/editing_module.py b/common/lib/xmodule/xmodule/editing_module.py new file mode 100644 index 0000000000..4188165a24 --- /dev/null +++ b/common/lib/xmodule/xmodule/editing_module.py @@ -0,0 +1,24 @@ +from pkg_resources import resource_string +from lxml import etree +from xmodule.mako_module import MakoModuleDescriptor +import logging + +log = logging.getLogger(__name__) + +class EditingDescriptor(MakoModuleDescriptor): + """ + Module that provides a raw editing view of its data and children. It does not + perform any validation on its definition---just passes it along to the browser. + + This class is intended to be used as a mixin. + """ + mako_template = "widgets/raw-edit.html" + + js = {'coffee': [resource_string(__name__, 'js/src/raw/edit.coffee')]} + js_module_name = "RawDescriptor" + + def get_context(self): + return { + 'module': self, + 'data': self.definition['data'], + } diff --git a/common/lib/xmodule/xmodule/malformed_module.py b/common/lib/xmodule/xmodule/malformed_module.py new file mode 100644 index 0000000000..803813dee8 --- /dev/null +++ b/common/lib/xmodule/xmodule/malformed_module.py @@ -0,0 +1,41 @@ +from pkg_resources import resource_string +from lxml import etree +from xmodule.mako_module import MakoModuleDescriptor +from xmodule.xml_module import XmlDescriptor +from xmodule.editing_module import EditingDescriptor + +import logging + +log = logging.getLogger(__name__) + +class MalformedDescriptor(EditingDescriptor): + """ + Module that provides a raw editing view of broken xml. + """ + + @classmethod + def from_xml(cls, xml_data, system, org=None, course=None): + '''Create an instance of this descriptor from the supplied data. + + Does not try to parse the data--just stores it. + ''' + + # TODO (vshnayder): how does one get back from this to a valid descriptor? + # try to parse and if successfull, send back to x_module? + + definition = { 'data' : xml_data } + # TODO (vshnayder): Do we need a valid slug here? Just pick a random + # 64-bit num? + location = ['i4x', org, course, 'malformed', 'slug'] + metadata = {} # stays in the xml_data + + return cls(system, definition, location=location, metadata=metadata) + + def export_to_xml(self, resource_fs): + ''' + Export as a string wrapped in xml + ''' + root = etree.Element('malformed') + root.text = self.definition['data'] + return etree.tostring(root) + diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py index 90f4139bd5..f9f358f945 100644 --- a/common/lib/xmodule/xmodule/raw_module.py +++ b/common/lib/xmodule/xmodule/raw_module.py @@ -1,27 +1,17 @@ from pkg_resources import resource_string from lxml import etree +from xmodule.editing_module import EditingDescriptor from xmodule.mako_module import MakoModuleDescriptor from xmodule.xml_module import XmlDescriptor import logging log = logging.getLogger(__name__) - -class RawDescriptor(MakoModuleDescriptor, XmlDescriptor): +class RawDescriptor(XmlDescriptor, EditingDescriptor): """ - Module that provides a raw editing view of its data and children + Module that provides a raw editing view of its data and children. It + requires that the definition xml is valid. """ - mako_template = "widgets/raw-edit.html" - - js = {'coffee': [resource_string(__name__, 'js/src/raw/edit.coffee')]} - js_module_name = "RawDescriptor" - - def get_context(self): - return { - 'module': self, - 'data': self.definition['data'], - } - @classmethod def definition_from_xml(cls, xml_object, system): return {'data': etree.tostring(xml_object)} diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py index 97ae307809..c4c5110abc 100644 --- a/common/lib/xmodule/xmodule/x_module.py +++ b/common/lib/xmodule/xmodule/x_module.py @@ -1,10 +1,12 @@ from lxml import etree +from lxml.etree import XMLSyntaxError import pkg_resources import logging +from fs.errors import ResourceNotFoundError +from functools import partial from xmodule.modulestore import Location -from functools import partial log = logging.getLogger('mitx.' + __name__) @@ -443,16 +445,28 @@ class XModuleDescriptor(Plugin, HTMLSnippet): system is an XMLParsingSystem org and course are optional strings that will be used in the generated - modules url identifiers + module's url identifiers """ - class_ = XModuleDescriptor.load_class( - etree.fromstring(xml_data).tag, - default_class - ) - # leave next line, commented out - useful for low-level debugging - # log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % ( - # etree.fromstring(xml_data).tag,class_)) - return class_.from_xml(xml_data, system, org, course) + try: + class_ = XModuleDescriptor.load_class( + etree.fromstring(xml_data).tag, + default_class + ) + # leave next line, commented out - useful for low-level debugging + # log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % ( + # etree.fromstring(xml_data).tag,class_)) + + descriptor = class_.from_xml(xml_data, system, org, course) + except (ResourceNotFoundError, XMLSyntaxError) as err: + # Didn't load properly. Fall back on loading as a malformed + # descriptor. This should never error due to formatting. + + # Put import here to avoid circular import errors + from xmodule.malformed_module import MalformedDescriptor + + descriptor = MalformedDescriptor.from_xml(xml_data, system, org, course) + + return descriptor @classmethod def from_xml(cls, xml_data, system, org=None, course=None):