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:
@@ -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
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
9
common/lib/xmodule/js/module/raw.coffee
Normal file
9
common/lib/xmodule/js/module/raw.coffee
Normal 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()
|
||||
@@ -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())
|
||||
|
||||
41
common/lib/xmodule/raw_module.py
Normal file
41
common/lib/xmodule/raw_module.py
Normal 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')]
|
||||
)
|
||||
@@ -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')]
|
||||
)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user