Parse XModuleDescriptors on import using from_xml

Also:
Render all XModuleDescriptors in the cms the same way
Default them to editing raw xml, if there is no specific module for them
This commit is contained in:
Calen Pennington
2012-06-26 11:55:20 -04:00
parent 5b4c501a71
commit e9a00ffc5a
17 changed files with 242 additions and 202 deletions

View File

@@ -125,7 +125,7 @@ class ModuleStore(object):
"""
An abstract interface for a database backend that stores XModuleDescriptor instances
"""
def get_item(self, location):
def get_item(self, location, default_class=None):
"""
Returns an XModuleDescriptor instance for the item at location.
If location.revision is None, returns the item with the most
@@ -136,6 +136,8 @@ class ModuleStore(object):
If no object is found at that location, raises keystore.exceptions.ItemNotFoundError
location: Something that can be passed to Location
default_class: An XModuleDescriptor subclass to use if no plugin matching the
location is found
"""
raise NotImplementedError

View File

@@ -8,6 +8,7 @@ from __future__ import absolute_import
from django.conf import settings
from .mongo import MongoModuleStore
from raw_module import RawDescriptor
_KEYSTORES = {}
@@ -16,6 +17,9 @@ def keystore(name='default'):
global _KEYSTORES
if name not in _KEYSTORES:
_KEYSTORES[name] = MongoModuleStore(**settings.KEYSTORE[name])
# TODO (cpennington): Load the default class from a string
_KEYSTORES[name] = MongoModuleStore(
default_class=RawDescriptor,
**settings.KEYSTORE[name])
return _KEYSTORES[name]

View File

@@ -8,7 +8,7 @@ class MongoModuleStore(ModuleStore):
"""
A Mongodb backed ModuleStore
"""
def __init__(self, host, db, collection, port=27017):
def __init__(self, host, db, collection, port=27017, default_class=None):
self.collection = pymongo.connection.Connection(
host=host,
port=port
@@ -16,6 +16,7 @@ class MongoModuleStore(ModuleStore):
# Force mongo to report errors, at the expense of performance
self.collection.safe = True
self.default_class = default_class
def get_item(self, location):
"""
@@ -28,6 +29,8 @@ class MongoModuleStore(ModuleStore):
If no object is found at that location, raises keystore.exceptions.ItemNotFoundError
location: Something that can be passed to Location
default_class: An XModuleDescriptor subclass to use if no plugin matching the
location is found
"""
query = {}
@@ -45,9 +48,10 @@ class MongoModuleStore(ModuleStore):
if item is None:
raise ItemNotFoundError(location)
return XModuleDescriptor.load_from_json(item, DescriptorSystem(self.get_item))
return XModuleDescriptor.load_from_json(
item, DescriptorSystem(self.get_item), self.default_class)
def create_item(self, location, editor):
def create_item(self, location):
"""
Create an empty item at the specified location with the supplied editor
@@ -55,7 +59,6 @@ class MongoModuleStore(ModuleStore):
"""
self.collection.insert({
'location': Location(location).dict(),
'editor': editor
})
def update_item(self, location, data):

View File

@@ -16,8 +16,8 @@ class HtmlModuleDescriptor(MakoModuleDescriptor):
"""
mako_template = "widgets/html-edit.html"
# TODO (cpennington): Make this into a proper module
js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]}
js_module = 'HTML'
class Module(XModule):

View File

@@ -0,0 +1,9 @@
class @Raw
constructor: (@id) ->
@edit_box = $("##{@id} .edit-box")
@preview = $("##{@id} .preview")
@edit_box.on('input', =>
@preview.empty().text(@edit_box.val())
)
save: -> @edit_box.val()

View File

@@ -12,7 +12,11 @@ class MakoModuleDescriptor(XModuleDescriptor):
the descriptor as the `module` parameter to that template
"""
def get_context(self):
"""
Return the context to render the mako template with
"""
return {'module': self}
def get_html(self):
return render_to_string(self.mako_template, {
'module': self
})
return render_to_string(self.mako_template, self.get_context())

View File

@@ -0,0 +1,41 @@
from pkg_resources import resource_string
from mako_module import MakoModuleDescriptor
from lxml import etree
class RawDescriptor(MakoModuleDescriptor):
"""
Module that provides a raw editing view of it's data and children
"""
mako_template = "widgets/raw-edit.html"
js = {'coffee': [resource_string(__name__, 'js/module/raw.coffee')]}
js_module = 'Raw'
def get_context(self):
return {
'module': self,
'data': self.definition['data'],
}
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for
this module
system: An XModuleSystem for interacting with external resources
org and course are optional strings that will be used in the generated modules
url identifiers
"""
xml_object = etree.fromstring(xml_data)
return cls(
system,
definition={'data': xml_data},
location=['i4x',
org,
course,
xml_object.tag,
xml_object.get('name')]
)

View File

@@ -115,5 +115,23 @@ class Module(XModule):
self.rendered = False
class SectionDescriptor(MakoModuleDescriptor):
class SequenceDescriptor(MakoModuleDescriptor):
mako_template = 'widgets/sequence-edit.html'
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
xml_object = etree.fromstring(xml_data)
children = [
system.process_xml(etree.tostring(child_module))
for child_module in xml_object
]
return cls(
system, {'children': children},
location=['i4x',
org,
course,
xml_object.tag,
xml_object.get('name')]
)

View File

@@ -13,18 +13,8 @@ setup(
# for a description of entry_points
entry_points={
'xmodule.v1': [
"Course = seq_module:SectionDescriptor",
"Week = seq_module:SectionDescriptor",
"Section = seq_module:SectionDescriptor",
"LectureSequence = seq_module:SectionDescriptor",
"Lab = seq_module:SectionDescriptor",
"Homework = seq_module:SectionDescriptor",
"TutorialIndex = seq_module:SectionDescriptor",
"Exam = seq_module:SectionDescriptor",
"VideoSegment = video_module:VideoSegmentDescriptor",
"ProblemSet = seq_module:SectionDescriptor",
"Problem = capa_module:CapaModuleDescriptor",
"HTML = html_module:HtmlModuleDescriptor",
"course = seq_module:SequenceDescriptor",
"html = html_module:HtmlModuleDescriptor",
]
}
)

View File

@@ -15,8 +15,24 @@ class ModuleMissingError(Exception):
class Plugin(object):
"""
Base class for a system that uses entry_points to load plugins.
Implementing classes are expected to have the following attributes:
entry_point: The name of the entry point to load plugins from
"""
@classmethod
def load_class(cls, identifier):
def load_class(cls, identifier, default=None):
"""
Loads a single class intance specified by identifier. If identifier
specifies more than a single class, then logs a warning and returns the first
class identified.
If default is not None, will return default if no entry_point matching identifier
is found. Otherwise, will raise a ModuleMissingError
"""
identifier = identifier.lower()
classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier))
if len(classes) > 1:
log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format(
@@ -25,6 +41,8 @@ class Plugin(object):
classes=", ".join(class_.module_name for class_ in classes)))
if len(classes) == 0:
if default is not None:
return default
raise ModuleMissingError(identifier)
return classes[0].load()
@@ -160,9 +178,10 @@ class XModuleDescriptor(Plugin):
"""
entry_point = "xmodule.v1"
js = {}
js_module = None
@staticmethod
def load_from_json(json_data, system):
def load_from_json(json_data, system, default_class=None):
"""
This method instantiates the correct subclass of XModuleDescriptor based
on the contents of json_data.
@@ -170,7 +189,10 @@ class XModuleDescriptor(Plugin):
json_data must contain a 'location' element, and must be suitable to be
passed into the subclasses `from_json` method.
"""
class_ = XModuleDescriptor.load_class(json_data['location']['category'])
class_ = XModuleDescriptor.load_class(
json_data['location']['category'],
default_class
)
return class_.from_json(json_data, system)
@classmethod
@@ -184,6 +206,36 @@ class XModuleDescriptor(Plugin):
"""
return cls(system=system, **json_data)
@staticmethod
def load_from_xml(xml_data, system, org=None, course=None, default_class=None):
"""
This method instantiates the correct subclass of XModuleDescriptor based
on the contents of xml_data.
xml_data must be a string containing valid xml
org and course are optional strings that will be used in the generated modules
url identifiers
"""
class_ = XModuleDescriptor.load_class(
etree.fromstring(xml_data).tag,
default_class
)
return class_.from_xml(xml_data, system, org, course)
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for
this module
system: An XModuleSystem for interacting with external resources
org and course are optional strings that will be used in the generated modules
url identifiers
"""
raise NotImplementedError('Modules must implement from_xml to be parsable from xml')
@classmethod
def get_javascript(cls):
"""
@@ -196,6 +248,12 @@ class XModuleDescriptor(Plugin):
"""
return cls.js
def js_module_name(self):
"""
Return the name of the javascript class to instantiate when
this module descriptor is loaded for editing
"""
return self.js_module
def __init__(self,
system,
@@ -230,15 +288,12 @@ class XModuleDescriptor(Plugin):
self._child_instances = None
def get_children(self, categories=None):
def get_children(self):
"""Returns a list of XModuleDescriptor instances for the children of this module"""
if self._child_instances is None:
self._child_instances = [self.system.load_item(child) for child in self.definition['children']]
self._child_instances = [self.system.load_item(child) for child in self.definition.get('children', [])]
if categories is None:
return self._child_instances
else:
return [child for child in self._child_instances if child.type in categories]
return self._child_instances
def get_html(self):
"""
@@ -275,9 +330,11 @@ class XModuleDescriptor(Plugin):
class DescriptorSystem(object):
def __init__(self, load_item):
def __init__(self, load_item, process_xml=None):
"""
load_item: Takes a Location and returns and XModuleDescriptor
load_item: Takes a Location and returns an XModuleDescriptor
process_xml: Takes an xml string, and returns the url of the XModuleDescriptor created from that xml
"""
self.load_item = load_item
self.process_xml = process_xml