195 lines
6.3 KiB
Python
195 lines
6.3 KiB
Python
from lxml import etree
|
|
import pkg_resources
|
|
import logging
|
|
from keystore import Location
|
|
|
|
log = logging.getLogger('mitx.' + __name__)
|
|
|
|
def dummy_track(event_type, event):
|
|
pass
|
|
|
|
|
|
class ModuleMissingError(Exception):
|
|
pass
|
|
|
|
|
|
class Plugin(object):
|
|
@classmethod
|
|
def load_class(cls, identifier):
|
|
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(
|
|
entry_point=cls.entry_point,
|
|
id=identifier,
|
|
classes=", ".join([class_.module_name for class_ in classes])))
|
|
|
|
if len(classes) == 0:
|
|
raise ModuleMissingError(identifier)
|
|
|
|
return classes[0].load()
|
|
|
|
|
|
class XModule(object):
|
|
''' Implements a generic learning module.
|
|
Initialized on access with __init__, first time with state=None, and
|
|
then with state
|
|
|
|
See the HTML module for a simple example
|
|
'''
|
|
id_attribute='id' # An attribute guaranteed to be unique
|
|
|
|
@classmethod
|
|
def get_xml_tags(c):
|
|
''' Tags in the courseware file guaranteed to correspond to the module '''
|
|
return []
|
|
|
|
@classmethod
|
|
def get_usage_tags(c):
|
|
''' We should convert to a real module system
|
|
For now, this tells us whether we use this as an xmodule, a CAPA response type
|
|
or a CAPA input type '''
|
|
return ['xmodule']
|
|
|
|
def get_name(self):
|
|
name = self.__xmltree.get('name')
|
|
if name:
|
|
return name
|
|
else:
|
|
raise "We should iterate through children and find a default name"
|
|
|
|
def rendered_children(self):
|
|
'''
|
|
Render all children.
|
|
This really ought to return a list of xmodules, instead of dictionaries
|
|
'''
|
|
children = [self.render_function(e) for e in self.__xmltree]
|
|
return children
|
|
|
|
def __init__(self, system = None, xml = None, item_id = None,
|
|
json = None, track_url=None, state=None):
|
|
''' In most cases, you must pass state or xml'''
|
|
if not item_id:
|
|
raise ValueError("Missing Index")
|
|
if not xml and not json:
|
|
raise ValueError("xml or json required")
|
|
if not system:
|
|
raise ValueError("System context required")
|
|
|
|
self.xml = xml
|
|
self.json = json
|
|
self.item_id = item_id
|
|
self.state = state
|
|
self.DEBUG = False
|
|
|
|
self.__xmltree = etree.fromstring(xml) # PRIVATE
|
|
|
|
if system:
|
|
## These are temporary; we really should go
|
|
## through self.system.
|
|
self.ajax_url = system.ajax_url
|
|
self.tracker = system.track_function
|
|
self.filestore = system.filestore
|
|
self.render_function = system.render_function
|
|
self.DEBUG = system.DEBUG
|
|
self.system = system
|
|
|
|
### Functions used in the LMS
|
|
|
|
def get_state(self):
|
|
''' State of the object, as stored in the database
|
|
'''
|
|
return ""
|
|
|
|
def get_score(self):
|
|
''' Score the student received on the problem.
|
|
'''
|
|
return None
|
|
|
|
def max_score(self):
|
|
''' Maximum score. Two notes:
|
|
* This is generic; in abstract, a problem could be 3/5 points on one randomization, and 5/7 on another
|
|
* In practice, this is a Very Bad Idea, and (a) will break some code in place (although that code
|
|
should get fixed), and (b) break some analytics we plan to put in place.
|
|
'''
|
|
return None
|
|
|
|
def get_html(self):
|
|
''' HTML, as shown in the browser. This is the only method that must be implemented
|
|
'''
|
|
return "Unimplemented"
|
|
|
|
def handle_ajax(self, dispatch, get):
|
|
''' dispatch is last part of the URL.
|
|
get is a dictionary-like object '''
|
|
return ""
|
|
|
|
|
|
class XModuleDescriptor(Plugin):
|
|
|
|
entry_point = "xmodule.v1"
|
|
|
|
@staticmethod
|
|
def load_from_json(json_data, load_item):
|
|
class_ = XModuleDescriptor.load_class(json_data['location']['category'])
|
|
return class_.from_json(json_data, load_item)
|
|
|
|
@classmethod
|
|
def from_json(cls, json_data, load_item):
|
|
"""
|
|
Creates an instance of this descriptor from the supplied json_data.
|
|
|
|
json_data: Json data specifying the data, children, and metadata for the descriptor
|
|
load_item: A function that takes an i4x url and returns a module descriptor
|
|
"""
|
|
return cls(load_item=load_item, **json_data)
|
|
|
|
def __init__(self,
|
|
load_item,
|
|
data=None,
|
|
children=None,
|
|
**kwargs):
|
|
self.load_item = load_item
|
|
self.data = data if data is not None else {}
|
|
self.children = children if children is not None else []
|
|
self.name = Location(kwargs.get('location')).name
|
|
self.type = Location(kwargs.get('location')).category
|
|
self._child_instances = None
|
|
|
|
def get_children(self, categories=None):
|
|
"""Returns a list of XModuleDescriptor instances for the children of this module"""
|
|
if self._child_instances is None:
|
|
self._child_instances = [self.load_item(child) for child in self.children]
|
|
|
|
if categories is None:
|
|
return self._child_instances
|
|
else:
|
|
return [child for child in self._child_instances if child.type in categories]
|
|
|
|
|
|
def get_xml(self):
|
|
''' For conversions between JSON and legacy XML representations.
|
|
'''
|
|
if self.xml:
|
|
return self.xml
|
|
else:
|
|
raise NotImplementedError("JSON->XML Translation not implemented")
|
|
|
|
def get_json(self):
|
|
''' For conversions between JSON and legacy XML representations.
|
|
'''
|
|
if self.json:
|
|
raise NotImplementedError
|
|
return self.json # TODO: Return context as well -- files, etc.
|
|
else:
|
|
raise NotImplementedError("XML->JSON Translation not implemented")
|
|
|
|
#def handle_cms_json(self):
|
|
# raise NotImplementedError
|
|
|
|
#def render(self, size):
|
|
# ''' Size: [thumbnail, small, full]
|
|
# Small ==> what we drag around
|
|
# Full ==> what we edit
|
|
# '''
|
|
# raise NotImplementedError
|