Files
edx-platform/common/lib/xmodule/x_module.py
Victor Shnayder 04dd8ee6d1 Initial progress display.
* add module_from_xml param to I4xSystem
* use it to implement xmodule.get_children()
* fix a few comments here and there
* Render-time progress display for seq and vertical modules.
  - Computes fraction of subproblems done.
* Pass problem state back to js during ajax calls.
* general cleanup in capa_module.py
* add progress_changed and progress fields to json returned from each ajax handler

* Coffeescript changes to hook up sequence tracking of problem progress

* net result: sequence 'a' tags now have a progress class
* properly set css class on initial load
* fire event when progress changes after ajax calls
* also save state in 'progress' property of problems-wrapper tag
* event handler finds those tags, computes updated progress
2012-06-21 10:59:47 -04:00

258 lines
8.9 KiB
Python

from lxml import etree
import pkg_resources
import logging
from keystore import Location
from progress import Progress
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 get_children(self):
'''
Return module instances for all the children of this module.
'''
children = [self.module_from_xml(e) for e in self.__xmltree]
return children
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.module_from_xml = system.module_from_xml
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 get_progress(self):
''' Return a progress.Progress object that represents how far the student has gone
in this module. Must be implemented to get correct progress tracking behavior in
nesting modules like sequence and vertical.
If this module has no notion of progress, return None.
'''
return None
def handle_ajax(self, dispatch, get):
''' dispatch is last part of the URL.
get is a dictionary-like object '''
return ""
class XModuleDescriptor(Plugin):
"""
An XModuleDescriptor is a specification for an element of a course. This could
be a problem, an organizational element (a group of content), or a segment of video,
for example.
XModuleDescriptors are independent and agnostic to the current student state on a
problem. They handle the editing interface used by instructors to create a problem,
and can generate XModules (which do know about student state).
"""
entry_point = "xmodule.v1"
@staticmethod
def load_from_json(json_data, system):
"""
This method instantiates the correct subclass of XModuleDescriptor based
on the contents of json_data.
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'])
return class_.from_json(json_data, system)
@classmethod
def from_json(cls, json_data, system):
"""
Creates an instance of this descriptor from the supplied json_data.
This may be overridden by subclasses
json_data: Json data specifying the data, children, and metadata for the descriptor
system: An XModuleSystem for interacting with external resources
"""
return cls(system=system, **json_data)
def __init__(self,
system,
definition=None,
**kwargs):
"""
Construct a new XModuleDescriptor. The only required arguments are the
system, used for interaction with external resources, and the definition,
which specifies all the data needed to edit and display the problem (but none
of the associated metadata that handles recordkeeping around the problem).
This allows for maximal flexibility to add to the interface while preserving
backwards compatibility.
system: An XModuleSystem for interacting with external resources
definition: A dict containing `data` and `children` representing the problem definition
Current arguments passed in kwargs:
location: A keystore.Location object indicating the name and ownership of this problem
goals: A list of strings of learning goals associated with this module
"""
self.system = system
self.definition = definition if definition is not None else {}
self.name = Location(kwargs.get('location')).name
self.type = Location(kwargs.get('location')).category
# For now, we represent goals as a list of strings, but this
# is one of the things that we are going to be iterating on heavily
# to find the best teaching method
self.goals = kwargs.get('goals', [])
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.system.load_item(child) for child in self.definition['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
class DescriptorSystem(object):
def __init__(self, load_item):
"""
load_item: Takes a Location and returns and XModuleDescriptor
"""
self.load_item = load_item