Merge branch 'master' into kimth/lms-coderesponse
This commit is contained in:
@@ -5,6 +5,7 @@ from x_module import XModuleDescriptor
|
||||
from lxml import etree
|
||||
from functools import wraps
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -12,8 +13,8 @@ log = logging.getLogger(__name__)
|
||||
def process_includes(fn):
|
||||
"""
|
||||
Wraps a XModuleDescriptor.from_xml method, and modifies xml_data to replace
|
||||
any immediate child <include> items with the contents of the file that they are
|
||||
supposed to include
|
||||
any immediate child <include> items with the contents of the file that they
|
||||
are supposed to include
|
||||
"""
|
||||
@wraps(fn)
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
@@ -21,23 +22,31 @@ def process_includes(fn):
|
||||
next_include = xml_object.find('include')
|
||||
while next_include is not None:
|
||||
file = next_include.get('file')
|
||||
if file is not None:
|
||||
try:
|
||||
ifp = system.resources_fs.open(file)
|
||||
except Exception:
|
||||
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True)))
|
||||
log.exception('Cannot find file %s in %s' % (file, dir))
|
||||
raise
|
||||
try:
|
||||
# read in and convert to XML
|
||||
incxml = etree.XML(ifp.read())
|
||||
except Exception:
|
||||
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True)))
|
||||
log.exception('Cannot parse XML in %s' % (file))
|
||||
raise
|
||||
parent = next_include.getparent()
|
||||
|
||||
if file is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
ifp = system.resources_fs.open(file)
|
||||
# read in and convert to XML
|
||||
incxml = etree.XML(ifp.read())
|
||||
|
||||
# insert new XML into tree in place of inlcude
|
||||
parent = next_include.getparent()
|
||||
parent.insert(parent.index(next_include), incxml)
|
||||
except Exception:
|
||||
msg = "Error in problem xml include: %s" % (etree.tostring(next_include, pretty_print=True))
|
||||
log.exception(msg)
|
||||
parent = next_include.getparent()
|
||||
|
||||
errorxml = etree.Element('error')
|
||||
messagexml = etree.SubElement(errorxml, 'message')
|
||||
messagexml.text = msg
|
||||
stackxml = etree.SubElement(errorxml, 'stacktrace')
|
||||
stackxml.text = traceback.format_exc()
|
||||
|
||||
# insert error XML in place of include
|
||||
parent.insert(parent.index(next_include), errorxml)
|
||||
parent.remove(next_include)
|
||||
|
||||
next_include = xml_object.find('include')
|
||||
@@ -50,8 +59,8 @@ class SemanticSectionDescriptor(XModuleDescriptor):
|
||||
@process_includes
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
Removes sections single child elements in favor of just embedding the child element
|
||||
|
||||
Removes sections with single child elements in favor of just embedding
|
||||
the child element
|
||||
"""
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
|
||||
@@ -76,7 +85,6 @@ class TranslateCustomTagDescriptor(XModuleDescriptor):
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
tag = xml_object.tag
|
||||
xml_object.tag = 'customtag'
|
||||
impl = etree.SubElement(xml_object, 'impl')
|
||||
impl.text = tag
|
||||
xml_object.attrib['impl'] = tag
|
||||
|
||||
return system.process_xml(etree.tostring(xml_object))
|
||||
|
||||
@@ -67,7 +67,8 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
|
||||
class CapaModule(XModule):
|
||||
'''
|
||||
An XModule implementing LonCapa format problems, implemented by way of capa.capa_problem.LoncapaProblem
|
||||
An XModule implementing LonCapa format problems, implemented by way of
|
||||
capa.capa_problem.LoncapaProblem
|
||||
'''
|
||||
icon_class = 'problem'
|
||||
|
||||
@@ -77,8 +78,10 @@ class CapaModule(XModule):
|
||||
js_module_name = "Problem"
|
||||
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
def __init__(self, system, location, definition, instance_state=None,
|
||||
shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state,
|
||||
shared_state, **kwargs)
|
||||
|
||||
self.attempts = 0
|
||||
self.max_attempts = None
|
||||
@@ -133,7 +136,8 @@ class CapaModule(XModule):
|
||||
seed = None
|
||||
|
||||
try:
|
||||
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), instance_state, seed=seed, system=self.system)
|
||||
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
|
||||
instance_state, seed=seed, system=self.system)
|
||||
except Exception:
|
||||
msg = 'cannot create LoncapaProblem %s' % self.location.url()
|
||||
log.exception(msg)
|
||||
@@ -141,15 +145,20 @@ class CapaModule(XModule):
|
||||
msg = '<p>%s</p>' % msg.replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
# create a dummy problem with error message instead of failing
|
||||
problem_text = '<problem><text><font color="red" size="+2">Problem %s has an error:</font>%s</text></problem>' % (self.location.url(), msg)
|
||||
self.lcp = LoncapaProblem(problem_text, self.location.html_id(), instance_state, seed=seed, system=self.system)
|
||||
problem_text = ('<problem><text><font color="red" size="+2">'
|
||||
'Problem %s has an error:</font>%s</text></problem>' %
|
||||
(self.location.url(), msg))
|
||||
self.lcp = LoncapaProblem(
|
||||
problem_text, self.location.html_id(),
|
||||
instance_state, seed=seed, system=self.system)
|
||||
else:
|
||||
raise
|
||||
|
||||
@property
|
||||
def rerandomize(self):
|
||||
"""
|
||||
Property accessor that returns self.metadata['rerandomize'] in a canonical form
|
||||
Property accessor that returns self.metadata['rerandomize'] in a
|
||||
canonical form
|
||||
"""
|
||||
rerandomize = self.metadata.get('rerandomize', 'always')
|
||||
if rerandomize in ("", "always", "true"):
|
||||
@@ -203,7 +212,10 @@ class CapaModule(XModule):
|
||||
except Exception, err:
|
||||
if self.system.DEBUG:
|
||||
log.exception(err)
|
||||
msg = '[courseware.capa.capa_module] <font size="+1" color="red">Failed to generate HTML for problem %s</font>' % (self.location.url())
|
||||
msg = (
|
||||
'[courseware.capa.capa_module] <font size="+1" color="red">'
|
||||
'Failed to generate HTML for problem %s</font>' %
|
||||
(self.location.url()))
|
||||
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
html = msg
|
||||
@@ -215,8 +227,8 @@ class CapaModule(XModule):
|
||||
'weight': self.weight,
|
||||
}
|
||||
|
||||
# We using strings as truthy values, because the terminology of the check button
|
||||
# is context-specific.
|
||||
# We using strings as truthy values, because the terminology of the
|
||||
# check button is context-specific.
|
||||
check_button = "Grade" if self.max_attempts else "Check"
|
||||
reset_button = True
|
||||
save_button = True
|
||||
@@ -242,7 +254,8 @@ class CapaModule(XModule):
|
||||
if not self.lcp.done:
|
||||
reset_button = False
|
||||
|
||||
# We don't need a "save" button if infinite number of attempts and non-randomized
|
||||
# We don't need a "save" button if infinite number of attempts and
|
||||
# non-randomized
|
||||
if self.max_attempts is None and self.rerandomize != "always":
|
||||
save_button = False
|
||||
|
||||
@@ -517,11 +530,13 @@ class CapaModule(XModule):
|
||||
|
||||
self.lcp.do_reset()
|
||||
if self.rerandomize == "always":
|
||||
# reset random number generator seed (note the self.lcp.get_state() in next line)
|
||||
# reset random number generator seed (note the self.lcp.get_state()
|
||||
# in next line)
|
||||
self.lcp.seed = None
|
||||
|
||||
self.lcp = LoncapaProblem(self.definition['data'],
|
||||
self.location.html_id(), self.lcp.get_state(), system=self.system)
|
||||
self.location.html_id(), self.lcp.get_state(),
|
||||
system=self.system)
|
||||
|
||||
event_info['new_state'] = self.lcp.get_state()
|
||||
self.system.track_function('reset_problem', event_info)
|
||||
@@ -537,6 +552,7 @@ class CapaDescriptor(RawDescriptor):
|
||||
|
||||
module_class = CapaModule
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Delete this method once all fall 2012 course are being
|
||||
# edited in the cms
|
||||
@classmethod
|
||||
@@ -545,3 +561,7 @@ class CapaDescriptor(RawDescriptor):
|
||||
'problems/' + path[8:],
|
||||
path[8:],
|
||||
]
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
'''Problems always written in their own files'''
|
||||
return True
|
||||
|
||||
@@ -10,6 +10,7 @@ log = logging.getLogger(__name__)
|
||||
|
||||
class CourseDescriptor(SequenceDescriptor):
|
||||
module_class = SequenceModule
|
||||
metadata_attributes = SequenceDescriptor.metadata_attributes + ('org', 'course')
|
||||
|
||||
def __init__(self, system, definition=None, **kwargs):
|
||||
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
|
||||
@@ -17,23 +18,40 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
try:
|
||||
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
|
||||
except KeyError:
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
log.critical("Course loaded without a start date. " + str(self.id))
|
||||
except ValueError, e:
|
||||
self.start = time.gmtime(0) # The epoch
|
||||
log.critical("Course loaded with a bad start date. " + str(self.id) + " '" + str(e) + "'")
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
log.critical("Course loaded without a start date. %s", self.id)
|
||||
except ValueError as e:
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
log.critical("Course loaded with a bad start date. %s '%s'",
|
||||
self.id, e)
|
||||
|
||||
def has_started(self):
|
||||
return time.gmtime() > self.start
|
||||
|
||||
@classmethod
|
||||
def id_to_location(cls, course_id):
|
||||
@staticmethod
|
||||
def id_to_location(course_id):
|
||||
'''Convert the given course_id (org/course/name) to a location object.
|
||||
Throws ValueError if course_id is of the wrong format.
|
||||
'''
|
||||
org, course, name = course_id.split('/')
|
||||
return Location('i4x', org, course, 'course', name)
|
||||
|
||||
@staticmethod
|
||||
def location_to_id(location):
|
||||
'''Convert a location of a course to a course_id. If location category
|
||||
is not "course", raise a ValueError.
|
||||
|
||||
location: something that can be passed to Location
|
||||
'''
|
||||
loc = Location(location)
|
||||
if loc.category != "course":
|
||||
raise ValueError("{0} is not a course location".format(loc))
|
||||
return "/".join([loc.org, loc.course, loc.name])
|
||||
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return "/".join([self.location.org, self.location.course, self.location.name])
|
||||
return self.location_to_id(self.location)
|
||||
|
||||
@property
|
||||
def start_date_text(self):
|
||||
|
||||
45
common/lib/xmodule/xmodule/errorhandlers.py
Normal file
45
common/lib/xmodule/xmodule/errorhandlers.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import logging
|
||||
import sys
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def in_exception_handler():
|
||||
'''Is there an active exception?'''
|
||||
return sys.exc_info() != (None, None, None)
|
||||
|
||||
def strict_error_handler(msg, exc_info=None):
|
||||
'''
|
||||
Do not let errors pass. If exc_info is not None, ignore msg, and just
|
||||
re-raise. Otherwise, check if we are in an exception-handling context.
|
||||
If so, re-raise. Otherwise, raise Exception(msg).
|
||||
|
||||
Meant for use in validation, where any errors should trap.
|
||||
'''
|
||||
if exc_info is not None:
|
||||
raise exc_info[0], exc_info[1], exc_info[2]
|
||||
|
||||
if in_exception_handler():
|
||||
raise
|
||||
|
||||
raise Exception(msg)
|
||||
|
||||
|
||||
def logging_error_handler(msg, exc_info=None):
|
||||
'''Log all errors, but otherwise let them pass, relying on the caller to
|
||||
workaround.'''
|
||||
if exc_info is not None:
|
||||
log.exception(msg, exc_info=exc_info)
|
||||
return
|
||||
|
||||
if in_exception_handler():
|
||||
log.exception(msg)
|
||||
return
|
||||
|
||||
log.error(msg)
|
||||
|
||||
|
||||
def ignore_errors_handler(msg, exc_info=None):
|
||||
'''Ignore all errors, relying on the caller to workaround.
|
||||
Meant for use in the LMS, where an error in one part of the course
|
||||
shouldn't bring down the whole system'''
|
||||
pass
|
||||
@@ -1,5 +1,6 @@
|
||||
class InvalidDefinitionError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NotFoundError(Exception):
|
||||
pass
|
||||
|
||||
@@ -12,8 +12,10 @@ class HtmlModule(XModule):
|
||||
def get_html(self):
|
||||
return self.html
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
def __init__(self, system, location, definition,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition,
|
||||
instance_state, shared_state, **kwargs)
|
||||
self.html = self.definition['data']
|
||||
|
||||
|
||||
@@ -42,3 +44,8 @@ class HtmlDescriptor(RawDescriptor):
|
||||
def file_to_xml(cls, file_object):
|
||||
parser = etree.HTMLParser()
|
||||
return etree.parse(file_object, parser).getroot()
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
# never include inline html
|
||||
return True
|
||||
|
||||
@@ -31,8 +31,20 @@ class @Problem
|
||||
else
|
||||
$.postWithPrefix "#{@url}/problem_get", (response) =>
|
||||
@el.html(response.html)
|
||||
@executeProblemScripts()
|
||||
@bind()
|
||||
|
||||
executeProblemScripts: ->
|
||||
@el.find(".script_placeholder").each (index, placeholder) ->
|
||||
s = $("<script>")
|
||||
s.attr("type", "text/javascript")
|
||||
s.attr("src", $(placeholder).attr("data-src"))
|
||||
|
||||
# Need to use the DOM elements directly or the scripts won't execute
|
||||
# properly.
|
||||
$('head')[0].appendChild(s[0])
|
||||
$(placeholder).remove()
|
||||
|
||||
check: =>
|
||||
Logger.log 'problem_check', @answers
|
||||
$.postWithPrefix "#{@url}/problem_check", @answers, (response) =>
|
||||
|
||||
@@ -52,6 +52,7 @@ function update_schematics() {
|
||||
schematics[i].setAttribute("loaded","true");
|
||||
}
|
||||
}
|
||||
window.update_schematics = update_schematics;
|
||||
|
||||
// add ourselves to the tasks that get performed when window is loaded
|
||||
function add_schematic_handler(other_onload) {
|
||||
|
||||
@@ -2,9 +2,12 @@ from x_module import XModuleDescriptor, DescriptorSystem
|
||||
|
||||
|
||||
class MakoDescriptorSystem(DescriptorSystem):
|
||||
def __init__(self, render_template, *args, **kwargs):
|
||||
def __init__(self, load_item, resources_fs, error_handler,
|
||||
render_template):
|
||||
super(MakoDescriptorSystem, self).__init__(
|
||||
load_item, resources_fs, error_handler)
|
||||
|
||||
self.render_template = render_template
|
||||
super(MakoDescriptorSystem, self).__init__(*args, **kwargs)
|
||||
|
||||
|
||||
class MakoModuleDescriptor(XModuleDescriptor):
|
||||
@@ -19,7 +22,9 @@ class MakoModuleDescriptor(XModuleDescriptor):
|
||||
|
||||
def __init__(self, system, definition=None, **kwargs):
|
||||
if getattr(system, 'render_template', None) is None:
|
||||
raise TypeError('{system} must have a render_template function in order to use a MakoDescriptor'.format(system=system))
|
||||
raise TypeError('{system} must have a render_template function'
|
||||
' in order to use a MakoDescriptor'.format(
|
||||
system=system))
|
||||
super(MakoModuleDescriptor, self).__init__(system, definition, **kwargs)
|
||||
|
||||
def get_context(self):
|
||||
@@ -29,4 +34,5 @@ class MakoModuleDescriptor(XModuleDescriptor):
|
||||
return {'module': self}
|
||||
|
||||
def get_html(self):
|
||||
return self.system.render_template(self.mako_template, self.get_context())
|
||||
return self.system.render_template(
|
||||
self.mako_template, self.get_context())
|
||||
|
||||
@@ -45,13 +45,28 @@ class Location(_LocationBase):
|
||||
"""
|
||||
return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
|
||||
|
||||
def __new__(_cls, loc_or_tag=None, org=None, course=None, category=None, name=None, revision=None):
|
||||
@classmethod
|
||||
def is_valid(cls, value):
|
||||
'''
|
||||
Check if the value is a valid location, in any acceptable format.
|
||||
'''
|
||||
try:
|
||||
Location(value)
|
||||
except InvalidLocationError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __new__(_cls, loc_or_tag=None, org=None, course=None, category=None,
|
||||
name=None, revision=None):
|
||||
"""
|
||||
Create a new location that is a clone of the specifed one.
|
||||
|
||||
location - Can be any of the following types:
|
||||
string: should be of the form {tag}://{org}/{course}/{category}/{name}[/{revision}]
|
||||
string: should be of the form
|
||||
{tag}://{org}/{course}/{category}/{name}[/{revision}]
|
||||
|
||||
list: should be of the form [tag, org, course, category, name, revision]
|
||||
|
||||
dict: should be of the form {
|
||||
'tag': tag,
|
||||
'org': org,
|
||||
@@ -62,16 +77,19 @@ class Location(_LocationBase):
|
||||
}
|
||||
Location: another Location object
|
||||
|
||||
In both the dict and list forms, the revision is optional, and can be ommitted.
|
||||
In both the dict and list forms, the revision is optional, and can be
|
||||
ommitted.
|
||||
|
||||
Components must be composed of alphanumeric characters, or the characters '_', '-', and '.'
|
||||
Components must be composed of alphanumeric characters, or the
|
||||
characters '_', '-', and '.'
|
||||
|
||||
Components may be set to None, which may be interpreted by some contexts to mean
|
||||
wildcard selection
|
||||
Components may be set to None, which may be interpreted by some contexts
|
||||
to mean wildcard selection
|
||||
"""
|
||||
|
||||
|
||||
if org is None and course is None and category is None and name is None and revision is None:
|
||||
if (org is None and course is None and category is None and
|
||||
name is None and revision is None):
|
||||
location = loc_or_tag
|
||||
else:
|
||||
location = (loc_or_tag, org, course, category, name, revision)
|
||||
@@ -131,9 +149,11 @@ class Location(_LocationBase):
|
||||
|
||||
def html_id(self):
|
||||
"""
|
||||
Return a string with a version of the location that is safe for use in html id attributes
|
||||
Return a string with a version of the location that is safe for use in
|
||||
html id attributes
|
||||
"""
|
||||
return "-".join(str(v) for v in self.list() if v is not None).replace('.', '_')
|
||||
return "-".join(str(v) for v in self.list()
|
||||
if v is not None).replace('.', '_')
|
||||
|
||||
def dict(self):
|
||||
"""
|
||||
@@ -154,7 +174,8 @@ class Location(_LocationBase):
|
||||
|
||||
class ModuleStore(object):
|
||||
"""
|
||||
An abstract interface for a database backend that stores XModuleDescriptor instances
|
||||
An abstract interface for a database backend that stores XModuleDescriptor
|
||||
instances
|
||||
"""
|
||||
def get_item(self, location, depth=0):
|
||||
"""
|
||||
@@ -164,13 +185,16 @@ class ModuleStore(object):
|
||||
|
||||
If any segment of the location is None except revision, raises
|
||||
xmodule.modulestore.exceptions.InsufficientSpecificationError
|
||||
If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError
|
||||
|
||||
If no object is found at that location, raises
|
||||
xmodule.modulestore.exceptions.ItemNotFoundError
|
||||
|
||||
location: Something that can be passed to Location
|
||||
|
||||
depth (int): An argument that some module stores may use to prefetch descendents of the queried modules
|
||||
for more efficient results later in the request. The depth is counted in the number of
|
||||
calls to get_children() to cache. None indicates to cache all descendents
|
||||
depth (int): An argument that some module stores may use to prefetch
|
||||
descendents of the queried modules for more efficient results later
|
||||
in the request. The depth is counted in the number of calls to
|
||||
get_children() to cache. None indicates to cache all descendents
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -182,9 +206,10 @@ class ModuleStore(object):
|
||||
|
||||
location: Something that can be passed to Location
|
||||
|
||||
depth: An argument that some module stores may use to prefetch descendents of the queried modules
|
||||
for more efficient results later in the request. The depth is counted in the number of calls
|
||||
to get_children() to cache. None indicates to cache all descendents
|
||||
depth: An argument that some module stores may use to prefetch
|
||||
descendents of the queried modules for more efficient results later
|
||||
in the request. The depth is counted in the number of calls to
|
||||
get_children() to cache. None indicates to cache all descendents
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -229,3 +254,25 @@ class ModuleStore(object):
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def path_to_location(self, location, course=None, chapter=None, section=None):
|
||||
'''
|
||||
Try to find a course/chapter/section[/position] path to this location.
|
||||
|
||||
raise ItemNotFoundError if the location doesn't exist.
|
||||
|
||||
If course, chapter, section are not None, restrict search to paths with those
|
||||
components as specified.
|
||||
|
||||
raise NoPathToItem if the location exists, but isn't accessible via
|
||||
a path that matches the course/chapter/section restrictions.
|
||||
|
||||
In general, a location may be accessible via many paths. This method may
|
||||
return any valid path.
|
||||
|
||||
Return a tuple (course, chapter, section, position).
|
||||
|
||||
If the section a sequence, position should be the position of this location
|
||||
in that sequence. Otherwise, position should be None.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -13,3 +13,11 @@ class InsufficientSpecificationError(Exception):
|
||||
|
||||
class InvalidLocationError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoPathToItem(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DuplicateItemError(Exception):
|
||||
pass
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import pymongo
|
||||
|
||||
from bson.objectid import ObjectId
|
||||
from bson.son import SON
|
||||
from fs.osfs import OSFS
|
||||
from itertools import repeat
|
||||
from path import path
|
||||
|
||||
from importlib import import_module
|
||||
from xmodule.errorhandlers import strict_error_handler
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
from . import ModuleStore, Location
|
||||
from .exceptions import ItemNotFoundError, InsufficientSpecificationError
|
||||
from .exceptions import (ItemNotFoundError, InsufficientSpecificationError,
|
||||
NoPathToItem, DuplicateItemError)
|
||||
|
||||
# TODO (cpennington): This code currently operates under the assumption that
|
||||
# there is only one revision for each item. Once we start versioning inside the CMS,
|
||||
@@ -23,15 +26,26 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
|
||||
A system that has a cache of module json that it will use to load modules
|
||||
from, with a backup of calling to the underlying modulestore for more data
|
||||
"""
|
||||
def __init__(self, modulestore, module_data, default_class, resources_fs, render_template):
|
||||
def __init__(self, modulestore, module_data, default_class, resources_fs,
|
||||
error_handler, render_template):
|
||||
"""
|
||||
modulestore: the module store that can be used to retrieve additional modules
|
||||
module_data: a dict mapping Location -> json that was cached from the underlying modulestore
|
||||
default_class: The default_class to use when loading an XModuleDescriptor from the module_data
|
||||
|
||||
module_data: a dict mapping Location -> json that was cached from the
|
||||
underlying modulestore
|
||||
|
||||
default_class: The default_class to use when loading an
|
||||
XModuleDescriptor from the module_data
|
||||
|
||||
resources_fs: a filesystem, as per MakoDescriptorSystem
|
||||
render_template: a function for rendering templates, as per MakoDescriptorSystem
|
||||
|
||||
error_handler:
|
||||
|
||||
render_template: a function for rendering templates, as per
|
||||
MakoDescriptorSystem
|
||||
"""
|
||||
super(CachingDescriptorSystem, self).__init__(render_template, self.load_item, resources_fs)
|
||||
super(CachingDescriptorSystem, self).__init__(
|
||||
self.load_item, resources_fs, error_handler, render_template)
|
||||
self.modulestore = modulestore
|
||||
self.module_data = module_data
|
||||
self.default_class = default_class
|
||||
@@ -83,7 +97,7 @@ class MongoModuleStore(ModuleStore):
|
||||
module_path, _, class_name = default_class.rpartition('.')
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
self.fs_root = fs_root
|
||||
self.fs_root = path(fs_root)
|
||||
|
||||
def _clean_item_data(self, item):
|
||||
"""
|
||||
@@ -127,19 +141,23 @@ class MongoModuleStore(ModuleStore):
|
||||
"""
|
||||
Load an XModuleDescriptor from item, using the children stored in data_cache
|
||||
"""
|
||||
resource_fs = OSFS(self.fs_root / item.get('data_dir', item['location']['course']))
|
||||
data_dir = item.get('metadata', {}).get('data_dir', item['location']['course'])
|
||||
resource_fs = OSFS(self.fs_root / data_dir)
|
||||
|
||||
system = CachingDescriptorSystem(
|
||||
self,
|
||||
data_cache,
|
||||
self.default_class,
|
||||
resource_fs,
|
||||
render_to_string
|
||||
strict_error_handler,
|
||||
render_to_string,
|
||||
)
|
||||
return system.load_item(item['location'])
|
||||
|
||||
def _load_items(self, items, depth=0):
|
||||
"""
|
||||
Load a list of xmodules from the data in items, with children cached up to specified depth
|
||||
Load a list of xmodules from the data in items, with children cached up
|
||||
to specified depth
|
||||
"""
|
||||
data_cache = self._cache_children(items, depth)
|
||||
|
||||
@@ -153,6 +171,14 @@ class MongoModuleStore(ModuleStore):
|
||||
course_filter = Location("i4x", category="course")
|
||||
return self.get_items(course_filter)
|
||||
|
||||
def _find_one(self, location):
|
||||
'''Look for a given location in the collection.
|
||||
If revision isn't specified, returns the latest.'''
|
||||
return self.collection.find_one(
|
||||
location_to_query(location),
|
||||
sort=[('revision', pymongo.ASCENDING)],
|
||||
)
|
||||
|
||||
def get_item(self, location, depth=0):
|
||||
"""
|
||||
Returns an XModuleDescriptor instance for the item at location.
|
||||
@@ -176,10 +202,7 @@ class MongoModuleStore(ModuleStore):
|
||||
if key != 'revision' and val is None:
|
||||
raise InsufficientSpecificationError(location)
|
||||
|
||||
item = self.collection.find_one(
|
||||
location_to_query(location),
|
||||
sort=[('revision', pymongo.ASCENDING)],
|
||||
)
|
||||
item = self._find_one(location)
|
||||
if item is None:
|
||||
raise ItemNotFoundError(location)
|
||||
return self._load_items([item], depth)[0]
|
||||
@@ -192,15 +215,22 @@ class MongoModuleStore(ModuleStore):
|
||||
|
||||
return self._load_items(list(items), depth)
|
||||
|
||||
# TODO (cpennington): This needs to be replaced by clone_item as soon as we allow
|
||||
# creation of items from the cms
|
||||
def create_item(self, location):
|
||||
"""
|
||||
Create an empty item at the specified location with the supplied editor
|
||||
Create an empty item at the specified location.
|
||||
|
||||
If that location already exists, raises a DuplicateItemError
|
||||
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
self.collection.insert({
|
||||
'_id': Location(location).dict(),
|
||||
})
|
||||
try:
|
||||
self.collection.insert({
|
||||
'_id': Location(location).dict(),
|
||||
})
|
||||
except pymongo.errors.DuplicateKeyError:
|
||||
raise DuplicateItemError(location)
|
||||
|
||||
def update_item(self, location, data):
|
||||
"""
|
||||
@@ -237,7 +267,7 @@ class MongoModuleStore(ModuleStore):
|
||||
|
||||
def update_metadata(self, location, metadata):
|
||||
"""
|
||||
Set the children for the item specified by the location to
|
||||
Set the metadata for the item specified by the location to
|
||||
metadata
|
||||
|
||||
location: Something that can be passed to Location
|
||||
@@ -250,3 +280,98 @@ class MongoModuleStore(ModuleStore):
|
||||
{'_id': Location(location).dict()},
|
||||
{'$set': {'metadata': metadata}}
|
||||
)
|
||||
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location.
|
||||
Mostly intended for use in path_to_location, but exposed for testing
|
||||
and possible other usefulness.
|
||||
|
||||
returns an iterable of things that can be passed to Location.
|
||||
'''
|
||||
location = Location(location)
|
||||
items = self.collection.find({'definition.children': str(location)},
|
||||
{'_id': True})
|
||||
return [i['_id'] for i in items]
|
||||
|
||||
def path_to_location(self, location, course_name=None):
|
||||
'''
|
||||
Try to find a course_id/chapter/section[/position] path to this location.
|
||||
The courseware insists that the first level in the course is chapter,
|
||||
but any kind of module can be a "section".
|
||||
|
||||
location: something that can be passed to Location
|
||||
course_name: [optional]. If not None, restrict search to paths
|
||||
in that course.
|
||||
|
||||
raise ItemNotFoundError if the location doesn't exist.
|
||||
|
||||
raise NoPathToItem if the location exists, but isn't accessible via
|
||||
a chapter/section path in the course(s) being searched.
|
||||
|
||||
Return a tuple (course_id, chapter, section, position) suitable for the
|
||||
courseware index view.
|
||||
|
||||
A location may be accessible via many paths. This method may
|
||||
return any valid path.
|
||||
|
||||
If the section is a sequence, position will be the position
|
||||
of this location in that sequence. Otherwise, position will
|
||||
be None. TODO (vshnayder): Not true yet.
|
||||
'''
|
||||
# Check that location is present at all
|
||||
if self._find_one(location) is None:
|
||||
raise ItemNotFoundError(location)
|
||||
|
||||
def flatten(xs):
|
||||
'''Convert lisp-style (a, (b, (c, ()))) lists into a python list.
|
||||
Not a general flatten function. '''
|
||||
p = []
|
||||
while xs != ():
|
||||
p.append(xs[0])
|
||||
xs = xs[1]
|
||||
return p
|
||||
|
||||
def find_path_to_course(location, course_name=None):
|
||||
'''Find a path up the location graph to a node with the
|
||||
specified category. If no path exists, return None. If a
|
||||
path exists, return it as a list with target location
|
||||
first, and the starting location last.
|
||||
'''
|
||||
# Standard DFS
|
||||
|
||||
# To keep track of where we came from, the work queue has
|
||||
# tuples (location, path-so-far). To avoid lots of
|
||||
# copying, the path-so-far is stored as a lisp-style
|
||||
# list--nested hd::tl tuples, and flattened at the end.
|
||||
queue = [(location, ())]
|
||||
while len(queue) > 0:
|
||||
(loc, path) = queue.pop() # Takes from the end
|
||||
loc = Location(loc)
|
||||
# print 'Processing loc={0}, path={1}'.format(loc, path)
|
||||
if loc.category == "course":
|
||||
if course_name is None or course_name == loc.name:
|
||||
# Found it!
|
||||
path = (loc, path)
|
||||
return flatten(path)
|
||||
|
||||
# otherwise, add parent locations at the end
|
||||
newpath = (loc, path)
|
||||
parents = self.get_parent_locations(loc)
|
||||
queue.extend(zip(parents, repeat(newpath)))
|
||||
|
||||
# If we're here, there is no path
|
||||
return None
|
||||
|
||||
path = find_path_to_course(location, course_name)
|
||||
if path is None:
|
||||
raise(NoPathToItem(location))
|
||||
|
||||
n = len(path)
|
||||
course_id = CourseDescriptor.location_to_id(path[0])
|
||||
chapter = path[1].name if n > 1 else None
|
||||
section = path[2].name if n > 2 else None
|
||||
|
||||
# TODO (vshnayder): not handling position at all yet...
|
||||
position = None
|
||||
|
||||
return (course_id, chapter, section, position)
|
||||
|
||||
@@ -13,14 +13,51 @@ def test_string_roundtrip():
|
||||
check_string_roundtrip("tag://org/course/category/name/revision")
|
||||
|
||||
|
||||
input_dict = {
|
||||
'tag': 'tag',
|
||||
'course': 'course',
|
||||
'category': 'category',
|
||||
'name': 'name',
|
||||
'org': 'org'
|
||||
}
|
||||
|
||||
input_list = ['tag', 'org', 'course', 'category', 'name']
|
||||
|
||||
input_str = "tag://org/course/category/name"
|
||||
input_str_rev = "tag://org/course/category/name/revision"
|
||||
|
||||
valid = (input_list, input_dict, input_str, input_str_rev)
|
||||
|
||||
invalid_dict = {
|
||||
'tag': 'tag',
|
||||
'course': 'course',
|
||||
'category': 'category',
|
||||
'name': 'name/more_name',
|
||||
'org': 'org'
|
||||
}
|
||||
|
||||
invalid_dict2 = {
|
||||
'tag': 'tag',
|
||||
'course': 'course',
|
||||
'category': 'category',
|
||||
'name': 'name ', # extra space
|
||||
'org': 'org'
|
||||
}
|
||||
|
||||
invalid = ("foo", ["foo"], ["foo", "bar"],
|
||||
["foo", "bar", "baz", "blat", "foo/bar"],
|
||||
"tag://org/course/category/name with spaces/revision",
|
||||
invalid_dict,
|
||||
invalid_dict2)
|
||||
|
||||
def test_is_valid():
|
||||
for v in valid:
|
||||
assert_equals(Location.is_valid(v), True)
|
||||
|
||||
for v in invalid:
|
||||
assert_equals(Location.is_valid(v), False)
|
||||
|
||||
def test_dict():
|
||||
input_dict = {
|
||||
'tag': 'tag',
|
||||
'course': 'course',
|
||||
'category': 'category',
|
||||
'name': 'name',
|
||||
'org': 'org'
|
||||
}
|
||||
assert_equals("tag://org/course/category/name", Location(input_dict).url())
|
||||
assert_equals(dict(revision=None, **input_dict), Location(input_dict).dict())
|
||||
|
||||
@@ -30,7 +67,6 @@ def test_dict():
|
||||
|
||||
|
||||
def test_list():
|
||||
input_list = ['tag', 'org', 'course', 'category', 'name']
|
||||
assert_equals("tag://org/course/category/name", Location(input_list).url())
|
||||
assert_equals(input_list + [None], Location(input_list).list())
|
||||
|
||||
@@ -65,3 +101,13 @@ def test_equality():
|
||||
Location('tag', 'org', 'course', 'category', 'name1'),
|
||||
Location('tag', 'org', 'course', 'category', 'name')
|
||||
)
|
||||
|
||||
def test_clean():
|
||||
pairs = [ ('',''),
|
||||
(' ', '_'),
|
||||
('abc,', 'abc_'),
|
||||
('ab fg!@//\\aj', 'ab_fg_aj'),
|
||||
(u"ab\xA9", "ab_"), # no unicode allowed for now
|
||||
]
|
||||
for input, output in pairs:
|
||||
assert_equals(Location.clean(input), output)
|
||||
|
||||
136
common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
Normal file
136
common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
Normal file
@@ -0,0 +1,136 @@
|
||||
import pymongo
|
||||
|
||||
from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup
|
||||
from path import path
|
||||
from pprint import pprint
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.mongo import MongoModuleStore
|
||||
from xmodule.modulestore.xml_importer import import_from_xml
|
||||
|
||||
# from ~/mitx_all/mitx/common/lib/xmodule/xmodule/modulestore/tests/
|
||||
# to ~/mitx_all/mitx/common/test
|
||||
TEST_DIR = path(__file__).abspath().dirname()
|
||||
for i in range(5):
|
||||
TEST_DIR = TEST_DIR.dirname()
|
||||
TEST_DIR = TEST_DIR / 'test'
|
||||
|
||||
DATA_DIR = TEST_DIR / 'data'
|
||||
|
||||
|
||||
HOST = 'localhost'
|
||||
PORT = 27017
|
||||
DB = 'test'
|
||||
COLLECTION = 'modulestore'
|
||||
FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item
|
||||
DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
|
||||
|
||||
|
||||
class TestMongoModuleStore(object):
|
||||
|
||||
@classmethod
|
||||
def setupClass(cls):
|
||||
cls.connection = pymongo.connection.Connection(HOST, PORT)
|
||||
cls.connection.drop_database(DB)
|
||||
|
||||
# NOTE: Creating a single db for all the tests to save time. This
|
||||
# is ok only as long as none of the tests modify the db.
|
||||
# If (when!) that changes, need to either reload the db, or load
|
||||
# once and copy over to a tmp db for each test.
|
||||
cls.store = cls.initdb()
|
||||
|
||||
@classmethod
|
||||
def teardownClass(cls):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def initdb():
|
||||
# connect to the db
|
||||
store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, default_class=DEFAULT_CLASS)
|
||||
# Explicitly list the courses to load (don't want the big one)
|
||||
courses = ['toy', 'simple']
|
||||
import_from_xml(store, DATA_DIR, courses)
|
||||
return store
|
||||
|
||||
@staticmethod
|
||||
def destroy_db(connection):
|
||||
# Destroy the test db.
|
||||
connection.drop_database(DB)
|
||||
|
||||
def setUp(self):
|
||||
# make a copy for convenience
|
||||
self.connection = TestMongoModuleStore.connection
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
def test_init(self):
|
||||
'''Make sure the db loads, and print all the locations in the db.
|
||||
Call this directly from failing tests to see what's loaded'''
|
||||
ids = list(self.connection[DB][COLLECTION].find({}, {'_id': True}))
|
||||
|
||||
pprint([Location(i['_id']).url() for i in ids])
|
||||
|
||||
def test_get_courses(self):
|
||||
'''Make sure the course objects loaded properly'''
|
||||
courses = self.store.get_courses()
|
||||
assert_equals(len(courses), 2)
|
||||
courses.sort(key=lambda c: c.id)
|
||||
assert_equals(courses[0].id, 'edX/simple/2012_Fall')
|
||||
assert_equals(courses[1].id, 'edX/toy/2012_Fall')
|
||||
|
||||
def test_loads(self):
|
||||
assert_not_equals(
|
||||
self.store.get_item("i4x://edX/toy/course/2012_Fall"),
|
||||
None)
|
||||
|
||||
assert_not_equals(
|
||||
self.store.get_item("i4x://edX/simple/course/2012_Fall"),
|
||||
None)
|
||||
|
||||
assert_not_equals(
|
||||
self.store.get_item("i4x://edX/toy/video/Welcome"),
|
||||
None)
|
||||
|
||||
|
||||
|
||||
def test_find_one(self):
|
||||
assert_not_equals(
|
||||
self.store._find_one(Location("i4x://edX/toy/course/2012_Fall")),
|
||||
None)
|
||||
|
||||
assert_not_equals(
|
||||
self.store._find_one(Location("i4x://edX/simple/course/2012_Fall")),
|
||||
None)
|
||||
|
||||
assert_not_equals(
|
||||
self.store._find_one(Location("i4x://edX/toy/video/Welcome")),
|
||||
None)
|
||||
|
||||
def test_path_to_location(self):
|
||||
'''Make sure that path_to_location works'''
|
||||
should_work = (
|
||||
("i4x://edX/toy/video/Welcome",
|
||||
("edX/toy/2012_Fall", "Overview", "Welcome", None)),
|
||||
("i4x://edX/toy/html/toylab",
|
||||
("edX/toy/2012_Fall", "Overview", "Toy_Videos", None)),
|
||||
)
|
||||
for location, expected in should_work:
|
||||
assert_equals(self.store.path_to_location(location), expected)
|
||||
|
||||
not_found = (
|
||||
"i4x://edX/toy/video/WelcomeX",
|
||||
)
|
||||
for location in not_found:
|
||||
assert_raises(ItemNotFoundError, self.store.path_to_location, location)
|
||||
|
||||
# Since our test files are valid, there shouldn't be any
|
||||
# elements with no path to them. But we can look for them in
|
||||
# another course.
|
||||
no_path = (
|
||||
"i4x://edX/simple/video/Lost_Video",
|
||||
)
|
||||
for location in no_path:
|
||||
assert_raises(NoPathToItem, self.store.path_to_location, location, "toy")
|
||||
|
||||
@@ -3,6 +3,7 @@ from fs.osfs import OSFS
|
||||
from importlib import import_module
|
||||
from lxml import etree
|
||||
from path import path
|
||||
from xmodule.errorhandlers import logging_error_handler
|
||||
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from cStringIO import StringIO
|
||||
@@ -12,152 +13,188 @@ import re
|
||||
from . import ModuleStore, Location
|
||||
from .exceptions import ItemNotFoundError
|
||||
|
||||
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True))
|
||||
etree.set_default_parser(
|
||||
etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True))
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses have been imported into the cms from xml
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses have been imported
|
||||
# into the cms from xml
|
||||
def clean_out_mako_templating(xml_string):
|
||||
xml_string = xml_string.replace('%include', 'include')
|
||||
xml_string = re.sub("(?m)^\s*%.*$", '', xml_string)
|
||||
return xml_string
|
||||
|
||||
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
def __init__(self, xmlstore, org, course, course_dir, error_handler):
|
||||
"""
|
||||
A class that handles loading from xml. Does some munging to ensure that
|
||||
all elements have unique slugs.
|
||||
|
||||
xmlstore: the XMLModuleStore to store the loaded modules in
|
||||
"""
|
||||
self.unnamed_modules = 0
|
||||
self.used_slugs = set()
|
||||
|
||||
def process_xml(xml):
|
||||
try:
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses
|
||||
# have been imported into the cms from xml
|
||||
xml = clean_out_mako_templating(xml)
|
||||
xml_data = etree.fromstring(xml)
|
||||
except:
|
||||
log.exception("Unable to parse xml: {xml}".format(xml=xml))
|
||||
raise
|
||||
|
||||
# VS[compat]. Take this out once course conversion is done
|
||||
if xml_data.get('slug') is None and xml_data.get('url_name') is None:
|
||||
if xml_data.get('name'):
|
||||
slug = Location.clean(xml_data.get('name'))
|
||||
elif xml_data.get('display_name'):
|
||||
slug = Location.clean(xml_data.get('display_name'))
|
||||
else:
|
||||
self.unnamed_modules += 1
|
||||
slug = '{tag}_{count}'.format(tag=xml_data.tag,
|
||||
count=self.unnamed_modules)
|
||||
|
||||
while slug in self.used_slugs:
|
||||
self.unnamed_modules += 1
|
||||
slug = '{slug}_{count}'.format(slug=slug,
|
||||
count=self.unnamed_modules)
|
||||
|
||||
self.used_slugs.add(slug)
|
||||
# log.debug('-> slug=%s' % slug)
|
||||
xml_data.set('url_name', slug)
|
||||
|
||||
module = XModuleDescriptor.load_from_xml(
|
||||
etree.tostring(xml_data), self, org,
|
||||
course, xmlstore.default_class)
|
||||
|
||||
#log.debug('==> importing module location %s' % repr(module.location))
|
||||
module.metadata['data_dir'] = course_dir
|
||||
|
||||
xmlstore.modules[module.location] = module
|
||||
|
||||
if xmlstore.eager:
|
||||
module.get_children()
|
||||
return module
|
||||
|
||||
render_template = lambda: ''
|
||||
load_item = xmlstore.get_item
|
||||
resources_fs = OSFS(xmlstore.data_dir / course_dir)
|
||||
|
||||
MakoDescriptorSystem.__init__(self, load_item, resources_fs,
|
||||
error_handler, render_template)
|
||||
XMLParsingSystem.__init__(self, load_item, resources_fs,
|
||||
error_handler, process_xml)
|
||||
|
||||
|
||||
|
||||
class XMLModuleStore(ModuleStore):
|
||||
"""
|
||||
An XML backed ModuleStore
|
||||
"""
|
||||
def __init__(self, data_dir, default_class=None, eager=False, course_dirs=None):
|
||||
def __init__(self, data_dir, default_class=None, eager=False,
|
||||
course_dirs=None,
|
||||
error_handler=logging_error_handler):
|
||||
"""
|
||||
Initialize an XMLModuleStore from data_dir
|
||||
|
||||
data_dir: path to data directory containing the course directories
|
||||
default_class: dot-separated string defining the default descriptor class to use if non is specified in entry_points
|
||||
eager: If true, load the modules children immediately to force the entire course tree to be parsed
|
||||
course_dirs: If specified, the list of course_dirs to load. Otherwise, load
|
||||
all course dirs
|
||||
|
||||
default_class: dot-separated string defining the default descriptor
|
||||
class to use if none is specified in entry_points
|
||||
|
||||
eager: If true, load the modules children immediately to force the
|
||||
entire course tree to be parsed
|
||||
|
||||
course_dirs: If specified, the list of course_dirs to load. Otherwise,
|
||||
load all course dirs
|
||||
|
||||
error_handler: The error handler used here and in the underlying
|
||||
DescriptorSystem. By default, raise exceptions for all errors.
|
||||
See the comments in x_module.py:DescriptorSystem
|
||||
"""
|
||||
|
||||
self.eager = eager
|
||||
self.data_dir = path(data_dir)
|
||||
self.modules = {} # location -> XModuleDescriptor
|
||||
self.courses = {} # course_dir -> XModuleDescriptor for the course
|
||||
self.error_handler = error_handler
|
||||
|
||||
if default_class is None:
|
||||
self.default_class = None
|
||||
else:
|
||||
module_path, _, class_name = default_class.rpartition('.')
|
||||
log.debug('module_path = %s' % module_path)
|
||||
#log.debug('module_path = %s' % module_path)
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
|
||||
log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager, self.data_dir))
|
||||
log.debug('default_class = %s' % self.default_class)
|
||||
# TODO (cpennington): We need a better way of selecting specific sets of
|
||||
# debug messages to enable. These were drowning out important messages
|
||||
#log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager, self.data_dir))
|
||||
#log.debug('default_class = %s' % self.default_class)
|
||||
|
||||
for course_dir in os.listdir(self.data_dir):
|
||||
if course_dirs is not None and course_dir not in course_dirs:
|
||||
continue
|
||||
|
||||
if not os.path.exists(self.data_dir / course_dir / "course.xml"):
|
||||
continue
|
||||
# If we are specifically asked for missing courses, that should
|
||||
# be an error. If we are asked for "all" courses, find the ones
|
||||
# that have a course.xml
|
||||
if course_dirs is None:
|
||||
course_dirs = [d for d in os.listdir(self.data_dir) if
|
||||
os.path.exists(self.data_dir / d / "course.xml")]
|
||||
|
||||
for course_dir in course_dirs:
|
||||
try:
|
||||
course_descriptor = self.load_course(course_dir)
|
||||
self.courses[course_dir] = course_descriptor
|
||||
except:
|
||||
log.exception("Failed to load course %s" % course_dir)
|
||||
msg = "Failed to load course '%s'" % course_dir
|
||||
log.exception(msg)
|
||||
error_handler(msg)
|
||||
|
||||
|
||||
def load_course(self, course_dir):
|
||||
"""
|
||||
Load a course into this module store
|
||||
course_path: Course directory name
|
||||
|
||||
returns a CourseDescriptor for the course
|
||||
"""
|
||||
log.debug('========> Starting course import from {0}'.format(course_dir))
|
||||
|
||||
with open(self.data_dir / course_dir / "course.xml") as course_file:
|
||||
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses have been imported
|
||||
# into the cms from xml
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses have
|
||||
# been imported into the cms from xml
|
||||
course_file = StringIO(clean_out_mako_templating(course_file.read()))
|
||||
|
||||
course_data = etree.parse(course_file).getroot()
|
||||
org = course_data.get('org')
|
||||
|
||||
if org is None:
|
||||
log.error(
|
||||
"No 'org' attribute set for course in {dir}. Using default 'edx'".format(
|
||||
dir=course_dir))
|
||||
log.error("No 'org' attribute set for course in {dir}. "
|
||||
"Using default 'edx'".format(dir=course_dir))
|
||||
org = 'edx'
|
||||
|
||||
course = course_data.get('course')
|
||||
|
||||
if course is None:
|
||||
log.error(
|
||||
"No 'course' attribute set for course in {dir}. Using default '{default}'".format(
|
||||
dir=course_dir,
|
||||
default=course_dir
|
||||
))
|
||||
log.error("No 'course' attribute set for course in {dir}."
|
||||
" Using default '{default}'".format(
|
||||
dir=course_dir,
|
||||
default=course_dir
|
||||
))
|
||||
course = course_dir
|
||||
|
||||
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
def __init__(self, xmlstore):
|
||||
"""
|
||||
xmlstore: the XMLModuleStore to store the loaded modules in
|
||||
"""
|
||||
self.unnamed_modules = 0
|
||||
self.used_slugs = set()
|
||||
system = ImportSystem(self, org, course, course_dir,
|
||||
self.error_handler)
|
||||
|
||||
def process_xml(xml):
|
||||
try:
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses
|
||||
# have been imported into the cms from xml
|
||||
xml = clean_out_mako_templating(xml)
|
||||
xml_data = etree.fromstring(xml)
|
||||
except:
|
||||
log.exception("Unable to parse xml: {xml}".format(xml=xml))
|
||||
raise
|
||||
if xml_data.get('slug') is None:
|
||||
if xml_data.get('name'):
|
||||
slug = Location.clean(xml_data.get('name'))
|
||||
else:
|
||||
self.unnamed_modules += 1
|
||||
slug = '{tag}_{count}'.format(tag=xml_data.tag,
|
||||
count=self.unnamed_modules)
|
||||
|
||||
if slug in self.used_slugs:
|
||||
self.unnamed_modules += 1
|
||||
slug = '{slug}_{count}'.format(slug=slug,
|
||||
count=self.unnamed_modules)
|
||||
|
||||
self.used_slugs.add(slug)
|
||||
# log.debug('-> slug=%s' % slug)
|
||||
xml_data.set('slug', slug)
|
||||
|
||||
module = XModuleDescriptor.load_from_xml(
|
||||
etree.tostring(xml_data), self, org,
|
||||
course, xmlstore.default_class)
|
||||
log.debug('==> importing module location %s' % repr(module.location))
|
||||
module.metadata['data_dir'] = course_dir
|
||||
|
||||
xmlstore.modules[module.location] = module
|
||||
|
||||
if xmlstore.eager:
|
||||
module.get_children()
|
||||
return module
|
||||
|
||||
system_kwargs = dict(
|
||||
render_template=lambda: '',
|
||||
load_item=xmlstore.get_item,
|
||||
resources_fs=OSFS(xmlstore.data_dir / course_dir),
|
||||
process_xml=process_xml
|
||||
)
|
||||
MakoDescriptorSystem.__init__(self, **system_kwargs)
|
||||
XMLParsingSystem.__init__(self, **system_kwargs)
|
||||
|
||||
|
||||
course_descriptor = ImportSystem(self).process_xml(etree.tostring(course_data))
|
||||
log.debug('========> Done with course import')
|
||||
course_descriptor = system.process_xml(etree.tostring(course_data))
|
||||
log.debug('========> Done with course import from {0}'.format(course_dir))
|
||||
return course_descriptor
|
||||
|
||||
def get_item(self, location, depth=0):
|
||||
@@ -168,7 +205,9 @@ class XMLModuleStore(ModuleStore):
|
||||
|
||||
If any segment of the location is None except revision, raises
|
||||
xmodule.modulestore.exceptions.InsufficientSpecificationError
|
||||
If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError
|
||||
|
||||
If no object is found at that location, raises
|
||||
xmodule.modulestore.exceptions.ItemNotFoundError
|
||||
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
|
||||
40
common/lib/xmodule/xmodule/modulestore/xml_importer.py
Normal file
40
common/lib/xmodule/xmodule/modulestore/xml_importer.py
Normal file
@@ -0,0 +1,40 @@
|
||||
import logging
|
||||
|
||||
from .xml import XMLModuleStore
|
||||
from .exceptions import DuplicateItemError
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def import_from_xml(store, data_dir, course_dirs=None, eager=True,
|
||||
default_class='xmodule.raw_module.RawDescriptor'):
|
||||
"""
|
||||
Import the specified xml data_dir into the "store" modulestore,
|
||||
using org and course as the location org and course.
|
||||
|
||||
course_dirs: If specified, the list of course_dirs to load. Otherwise, load
|
||||
all course dirs
|
||||
|
||||
"""
|
||||
module_store = XMLModuleStore(
|
||||
data_dir,
|
||||
default_class=default_class,
|
||||
eager=eager,
|
||||
course_dirs=course_dirs
|
||||
)
|
||||
for module in module_store.modules.itervalues():
|
||||
|
||||
# TODO (cpennington): This forces import to overrite the same items.
|
||||
# This should in the future create new revisions of the items on import
|
||||
try:
|
||||
store.create_item(module.location)
|
||||
except DuplicateItemError:
|
||||
log.exception('Item already exists at %s' % module.location.url())
|
||||
pass
|
||||
if 'data' in module.definition:
|
||||
store.update_item(module.location, module.definition['data'])
|
||||
if 'children' in module.definition:
|
||||
store.update_children(module.location, module.definition['children'])
|
||||
store.update_metadata(module.location, dict(module.metadata))
|
||||
|
||||
return module_store
|
||||
@@ -6,9 +6,10 @@ import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
"""
|
||||
Module that provides a raw editing view of it's data and children
|
||||
Module that provides a raw editing view of its data and children
|
||||
"""
|
||||
mako_template = "widgets/raw-edit.html"
|
||||
|
||||
@@ -31,8 +32,11 @@ class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
except etree.XMLSyntaxError as err:
|
||||
lines = self.definition['data'].split('\n')
|
||||
line, offset = err.position
|
||||
log.exception("Unable to create xml for problem {loc}. Context: '{context}'".format(
|
||||
context=lines[line-1][offset - 40:offset + 40],
|
||||
loc=self.location
|
||||
))
|
||||
msg = ("Unable to create xml for problem {loc}. "
|
||||
"Context: '{context}'".format(
|
||||
context=lines[line - 1][offset - 40:offset + 40],
|
||||
loc=self.location))
|
||||
log.exception(msg)
|
||||
self.system.error_handler(msg)
|
||||
# no workaround possible, so just re-raise
|
||||
raise
|
||||
|
||||
@@ -20,12 +20,15 @@ class_priority = ['video', 'problem']
|
||||
class SequenceModule(XModule):
|
||||
''' Layout module which lays out content in a temporal sequence
|
||||
'''
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/sequence/display.coffee')]}
|
||||
js = {'coffee': [resource_string(__name__,
|
||||
'js/src/sequence/display.coffee')]}
|
||||
css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
|
||||
js_module_name = "Sequence"
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
def __init__(self, system, location, definition, instance_state=None,
|
||||
shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition,
|
||||
instance_state, shared_state, **kwargs)
|
||||
self.position = 1
|
||||
|
||||
if instance_state is not None:
|
||||
@@ -92,7 +95,8 @@ class SequenceModule(XModule):
|
||||
self.rendered = True
|
||||
|
||||
def get_icon_class(self):
|
||||
child_classes = set(child.get_icon_class() for child in self.get_children())
|
||||
child_classes = set(child.get_icon_class()
|
||||
for child in self.get_children())
|
||||
new_class = 'other'
|
||||
for c in class_priority:
|
||||
if c in child_classes:
|
||||
@@ -114,5 +118,20 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
def definition_to_xml(self, resource_fs):
|
||||
xml_object = etree.Element('sequential')
|
||||
for child in self.get_children():
|
||||
xml_object.append(etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
xml_object.append(
|
||||
etree.fromstring(child.export_to_xml(resource_fs)))
|
||||
return xml_object
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
# Note: if we end up needing subclasses, can port this logic there.
|
||||
yes = ('chapter',)
|
||||
no = ('course',)
|
||||
|
||||
if xml_object.tag in yes:
|
||||
return True
|
||||
elif xml_object.tag in no:
|
||||
return False
|
||||
|
||||
# otherwise maybe--delegate to superclass.
|
||||
return XmlDescriptor.split_to_file(xml_object)
|
||||
|
||||
@@ -21,19 +21,23 @@ class CustomTagModule(XModule):
|
||||
|
||||
course.xml::
|
||||
...
|
||||
<customtag page="234"><impl>book</impl></customtag>
|
||||
<customtag page="234" impl="book"/>
|
||||
...
|
||||
|
||||
Renders to::
|
||||
More information given in <a href="/book/234">the text</a>
|
||||
"""
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
def __init__(self, system, location, definition,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition,
|
||||
instance_state, shared_state, **kwargs)
|
||||
|
||||
xmltree = etree.fromstring(self.definition['data'])
|
||||
template_name = xmltree.find('impl').text
|
||||
template_name = xmltree.attrib['impl']
|
||||
params = dict(xmltree.items())
|
||||
with self.system.filestore.open('custom_tags/{name}'.format(name=template_name)) as template:
|
||||
with self.system.filestore.open(
|
||||
'custom_tags/{name}'.format(name=template_name)) as template:
|
||||
self.html = Template(template.read()).render(**params)
|
||||
|
||||
def get_html(self):
|
||||
|
||||
@@ -60,7 +60,7 @@ class VideoModule(XModule):
|
||||
return None
|
||||
|
||||
def get_instance_state(self):
|
||||
log.debug(u"STATE POSITION {0}".format(self.position))
|
||||
#log.debug(u"STATE POSITION {0}".format(self.position))
|
||||
return json.dumps({'position': self.position})
|
||||
|
||||
def video_list(self):
|
||||
|
||||
@@ -3,6 +3,7 @@ import pkg_resources
|
||||
import logging
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from functools import partial
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
@@ -31,23 +32,28 @@ class Plugin(object):
|
||||
def load_class(cls, identifier, default=None):
|
||||
"""
|
||||
Loads a single class instance specified by identifier. If identifier
|
||||
specifies more than a single class, then logs a warning and returns the first
|
||||
class identified.
|
||||
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
|
||||
If default is not None, will return default if no entry_point matching
|
||||
identifier is found. Otherwise, will raise a ModuleMissingError
|
||||
"""
|
||||
if cls._plugin_cache is None:
|
||||
cls._plugin_cache = {}
|
||||
|
||||
if identifier not in cls._plugin_cache:
|
||||
identifier = identifier.lower()
|
||||
classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=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(
|
||||
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)))
|
||||
classes=", ".join(
|
||||
class_.module_name for class_ in classes)))
|
||||
|
||||
if len(classes) == 0:
|
||||
if default is not None:
|
||||
@@ -79,9 +85,12 @@ class HTMLSnippet(object):
|
||||
def get_javascript(cls):
|
||||
"""
|
||||
Return a dictionary containing some of the following keys:
|
||||
|
||||
coffee: A list of coffeescript fragments that should be compiled and
|
||||
placed on the page
|
||||
js: A list of javascript fragments that should be included on the page
|
||||
|
||||
js: A list of javascript fragments that should be included on the
|
||||
page
|
||||
|
||||
All of these will be loaded onto the page in the CMS
|
||||
"""
|
||||
@@ -91,12 +100,15 @@ class HTMLSnippet(object):
|
||||
def get_css(cls):
|
||||
"""
|
||||
Return a dictionary containing some of the following keys:
|
||||
css: A list of css fragments that should be applied to the html contents
|
||||
of the snippet
|
||||
sass: A list of sass fragments that should be applied to the html contents
|
||||
of the snippet
|
||||
scss: A list of scss fragments that should be applied to the html contents
|
||||
of the snippet
|
||||
|
||||
css: A list of css fragments that should be applied to the html
|
||||
contents of the snippet
|
||||
|
||||
sass: A list of sass fragments that should be applied to the html
|
||||
contents of the snippet
|
||||
|
||||
scss: A list of scss fragments that should be applied to the html
|
||||
contents of the snippet
|
||||
"""
|
||||
return cls.css
|
||||
|
||||
@@ -104,46 +116,70 @@ class HTMLSnippet(object):
|
||||
"""
|
||||
Return the html used to display this snippet
|
||||
"""
|
||||
raise NotImplementedError("get_html() must be provided by specific modules")
|
||||
raise NotImplementedError(
|
||||
"get_html() must be provided by specific modules - not present in {0}"
|
||||
.format(self.__class__))
|
||||
|
||||
|
||||
class XModule(HTMLSnippet):
|
||||
''' Implements a generic learning module.
|
||||
|
||||
Subclasses must at a minimum provide a definition for get_html in order to be displayed to users.
|
||||
Subclasses must at a minimum provide a definition for get_html in order
|
||||
to be displayed to users.
|
||||
|
||||
See the HTML module for a simple example.
|
||||
'''
|
||||
|
||||
# The default implementation of get_icon_class returns the icon_class attribute of the class
|
||||
# This attribute can be overridden by subclasses, and the function can also be overridden
|
||||
# if the icon class depends on the data in the module
|
||||
# The default implementation of get_icon_class returns the icon_class
|
||||
# attribute of the class
|
||||
#
|
||||
# This attribute can be overridden by subclasses, and
|
||||
# the function can also be overridden if the icon class depends on the data
|
||||
# in the module
|
||||
icon_class = 'other'
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
def __init__(self, system, location, definition,
|
||||
instance_state=None, shared_state=None, **kwargs):
|
||||
'''
|
||||
Construct a new xmodule
|
||||
|
||||
system: A ModuleSystem allowing access to external resources
|
||||
|
||||
location: Something Location-like that identifies this xmodule
|
||||
definition: A dictionary containing 'data' and 'children'. Both are optional
|
||||
'data': is JSON-like (string, dictionary, list, bool, or None, optionally nested).
|
||||
This defines all of the data necessary for a problem to display that is intrinsic to the problem.
|
||||
It should not include any data that would vary between two courses using the same problem
|
||||
|
||||
definition: A dictionary containing 'data' and 'children'. Both are
|
||||
optional
|
||||
|
||||
'data': is JSON-like (string, dictionary, list, bool, or None,
|
||||
optionally nested).
|
||||
|
||||
This defines all of the data necessary for a problem to display
|
||||
that is intrinsic to the problem. It should not include any
|
||||
data that would vary between two courses using the same problem
|
||||
(due dates, grading policy, randomization, etc.)
|
||||
'children': is a list of Location-like values for child modules that this module depends on
|
||||
instance_state: A string of serialized json that contains the state of this module for
|
||||
current student accessing the system, or None if no state has been saved
|
||||
shared_state: A string of serialized json that contains the state that is shared between
|
||||
this module and any modules of the same type with the same shared_state_key. This
|
||||
state is only shared per-student, not across different students
|
||||
kwargs: Optional arguments. Subclasses should always accept kwargs and pass them
|
||||
to the parent class constructor.
|
||||
|
||||
'children': is a list of Location-like values for child modules that
|
||||
this module depends on
|
||||
|
||||
instance_state: A string of serialized json that contains the state of
|
||||
this module for current student accessing the system, or None if
|
||||
no state has been saved
|
||||
|
||||
shared_state: A string of serialized json that contains the state that
|
||||
is shared between this module and any modules of the same type with
|
||||
the same shared_state_key. This state is only shared per-student,
|
||||
not across different students
|
||||
|
||||
kwargs: Optional arguments. Subclasses should always accept kwargs and
|
||||
pass them to the parent class constructor.
|
||||
|
||||
Current known uses of kwargs:
|
||||
metadata: SCAFFOLDING - This dictionary will be split into several different types of metadata
|
||||
in the future (course policy, modification history, etc).
|
||||
A dictionary containing data that specifies information that is particular
|
||||
to a problem in the context of a course
|
||||
|
||||
metadata: SCAFFOLDING - This dictionary will be split into
|
||||
several different types of metadata in the future (course
|
||||
policy, modification history, etc). A dictionary containing
|
||||
data that specifies information that is particular to a
|
||||
problem in the context of a course
|
||||
'''
|
||||
self.system = system
|
||||
self.location = Location(location)
|
||||
@@ -157,24 +193,23 @@ class XModule(HTMLSnippet):
|
||||
self._loaded_children = None
|
||||
|
||||
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"
|
||||
return self.name
|
||||
|
||||
def get_children(self):
|
||||
'''
|
||||
Return module instances for all the children of this module.
|
||||
'''
|
||||
if self._loaded_children is None:
|
||||
self._loaded_children = [self.system.get_module(child) for child in self.definition.get('children', [])]
|
||||
self._loaded_children = [
|
||||
self.system.get_module(child)
|
||||
for child in self.definition.get('children', [])]
|
||||
|
||||
return self._loaded_children
|
||||
|
||||
def get_display_items(self):
|
||||
'''
|
||||
Returns a list of descendent module instances that will display immediately
|
||||
inside this module
|
||||
Returns a list of descendent module instances that will display
|
||||
immediately inside this module
|
||||
'''
|
||||
items = []
|
||||
for child in self.get_children():
|
||||
@@ -184,8 +219,8 @@ class XModule(HTMLSnippet):
|
||||
|
||||
def displayable_items(self):
|
||||
'''
|
||||
Returns list of displayable modules contained by this module. If this module
|
||||
is visible, should return [self]
|
||||
Returns list of displayable modules contained by this module. If this
|
||||
module is visible, should return [self]
|
||||
'''
|
||||
return [self]
|
||||
|
||||
@@ -216,16 +251,21 @@ class XModule(HTMLSnippet):
|
||||
|
||||
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.
|
||||
|
||||
* 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_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.
|
||||
''' 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.
|
||||
'''
|
||||
@@ -239,13 +279,14 @@ class XModule(HTMLSnippet):
|
||||
|
||||
class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
"""
|
||||
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.
|
||||
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).
|
||||
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"
|
||||
module_class = XModule
|
||||
@@ -254,46 +295,58 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
inheritable_metadata = (
|
||||
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
|
||||
|
||||
# This is used by the XMLModuleStore to provide for locations for static files,
|
||||
# and will need to be removed when that code is removed
|
||||
# TODO: This is used by the XMLModuleStore to provide for locations for
|
||||
# static files, and will need to be removed when that code is removed
|
||||
'data_dir'
|
||||
)
|
||||
|
||||
# A list of descriptor attributes that must be equal for the descriptors to be
|
||||
# equal
|
||||
equality_attributes = ('definition', 'metadata', 'location', 'shared_state_key', '_inherited_metadata')
|
||||
# A list of descriptor attributes that must be equal for the descriptors to
|
||||
# be equal
|
||||
equality_attributes = ('definition', 'metadata', 'location',
|
||||
'shared_state_key', '_inherited_metadata')
|
||||
|
||||
# ============================= STRUCTURAL MANIPULATION ===========================
|
||||
# ============================= STRUCTURAL MANIPULATION ===================
|
||||
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).
|
||||
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.
|
||||
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
|
||||
system: A DescriptorSystem for interacting with external resources
|
||||
|
||||
definition: A dict containing `data` and `children` representing the
|
||||
problem definition
|
||||
|
||||
Current arguments passed in kwargs:
|
||||
location: A xmodule.modulestore.Location object indicating the name and ownership of this problem
|
||||
shared_state_key: The key to use for sharing StudentModules with other
|
||||
modules of this type
|
||||
|
||||
location: A xmodule.modulestore.Location object indicating the name
|
||||
and ownership of this problem
|
||||
|
||||
shared_state_key: The key to use for sharing StudentModules with
|
||||
other modules of this type
|
||||
|
||||
metadata: A dictionary containing the following optional keys:
|
||||
goals: A list of strings of learning goals associated with this module
|
||||
display_name: The name to use for displaying this module to the user
|
||||
goals: A list of strings of learning goals associated with this
|
||||
module
|
||||
display_name: The name to use for displaying this module to the
|
||||
user
|
||||
format: The format of this module ('Homework', 'Lab', etc)
|
||||
graded (bool): Whether this module is should be graded or not
|
||||
start (string): The date for which this module will be available
|
||||
due (string): The due date for this module
|
||||
graceperiod (string): The amount of grace period to allow when enforcing the due date
|
||||
graceperiod (string): The amount of grace period to allow when
|
||||
enforcing the due date
|
||||
showanswer (string): When to show answers for this module
|
||||
rerandomize (string): When to generate a newly randomized instance of the module data
|
||||
rerandomize (string): When to generate a newly randomized
|
||||
instance of the module data
|
||||
"""
|
||||
self.system = system
|
||||
self.metadata = kwargs.get('metadata', {})
|
||||
@@ -320,7 +373,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
self.metadata[attr] = metadata[attr]
|
||||
|
||||
def get_children(self):
|
||||
"""Returns a list of XModuleDescriptor instances for the children of this module"""
|
||||
"""Returns a list of XModuleDescriptor instances for the children of
|
||||
this module"""
|
||||
if self._child_instances is None:
|
||||
self._child_instances = []
|
||||
for child_loc in self.definition.get('children', []):
|
||||
@@ -332,8 +386,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
|
||||
def xmodule_constructor(self, system):
|
||||
"""
|
||||
Returns a constructor for an XModule. This constructor takes two arguments:
|
||||
instance_state and shared_state, and returns a fully nstantiated XModule
|
||||
Returns a constructor for an XModule. This constructor takes two
|
||||
arguments: instance_state and shared_state, and returns a fully
|
||||
instantiated XModule
|
||||
"""
|
||||
return partial(
|
||||
self.module_class,
|
||||
@@ -343,7 +398,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
metadata=self.metadata
|
||||
)
|
||||
|
||||
# ================================= JSON PARSING ===================================
|
||||
# ================================= JSON PARSING ===========================
|
||||
@staticmethod
|
||||
def load_from_json(json_data, system, default_class=None):
|
||||
"""
|
||||
@@ -365,13 +420,14 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
Creates an instance of this descriptor from the supplied json_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
json_data: A json object specifying the definition and any optional keyword arguments for
|
||||
the XModuleDescriptor
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
json_data: A json object specifying the definition and any optional
|
||||
keyword arguments for the XModuleDescriptor
|
||||
|
||||
system: A DescriptorSystem for interacting with external resources
|
||||
"""
|
||||
return cls(system=system, **json_data)
|
||||
|
||||
# ================================= XML PARSING ====================================
|
||||
# ================================= XML PARSING ============================
|
||||
@staticmethod
|
||||
def load_from_xml(xml_data,
|
||||
system,
|
||||
@@ -383,16 +439,19 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
on the contents of xml_data.
|
||||
|
||||
xml_data must be a string containing valid xml
|
||||
|
||||
system is an XMLParsingSystem
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
|
||||
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
|
||||
)
|
||||
# leave next line in code, commented out - useful for low-level debugging
|
||||
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (etree.fromstring(xml_data).tag,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)
|
||||
|
||||
@classmethod
|
||||
@@ -401,35 +460,42 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
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
|
||||
xml_data: A string of xml that will be translated into data and children
|
||||
for this module
|
||||
|
||||
system is an XMLParsingSystem
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
|
||||
org and course are optional strings that will be used in the generated
|
||||
module's url identifiers
|
||||
"""
|
||||
raise NotImplementedError('Modules must implement from_xml to be parsable from xml')
|
||||
raise NotImplementedError(
|
||||
'Modules must implement from_xml to be parsable from xml')
|
||||
|
||||
def export_to_xml(self, resource_fs):
|
||||
"""
|
||||
Returns an xml string representing this module, and all modules underneath it.
|
||||
May also write required resources out to resource_fs
|
||||
Returns an xml string representing this module, and all modules
|
||||
underneath it. May also write required resources out to resource_fs
|
||||
|
||||
Assumes that modules have single parantage (that no module appears twice in the same course),
|
||||
and that it is thus safe to nest modules as xml children as appropriate.
|
||||
Assumes that modules have single parentage (that no module appears twice
|
||||
in the same course), and that it is thus safe to nest modules as xml
|
||||
children as appropriate.
|
||||
|
||||
The returned XML should be able to be parsed back into an identical XModuleDescriptor
|
||||
using the from_xml method with the same system, org, and course
|
||||
The returned XML should be able to be parsed back into an identical
|
||||
XModuleDescriptor using the from_xml method with the same system, org,
|
||||
and course
|
||||
"""
|
||||
raise NotImplementedError('Modules must implement export_to_xml to enable xml export')
|
||||
raise NotImplementedError(
|
||||
'Modules must implement export_to_xml to enable xml export')
|
||||
|
||||
# =============================== Testing ===================================
|
||||
# =============================== Testing ==================================
|
||||
def get_sample_state(self):
|
||||
"""
|
||||
Return a list of tuples of instance_state, shared_state. Each tuple defines a sample case for this module
|
||||
Return a list of tuples of instance_state, shared_state. Each tuple
|
||||
defines a sample case for this module
|
||||
"""
|
||||
return [('{}', '{}')]
|
||||
|
||||
# =============================== BUILTIN METHODS ===========================
|
||||
# =============================== BUILTIN METHODS ==========================
|
||||
def __eq__(self, other):
|
||||
eq = (self.__class__ == other.__class__ and
|
||||
all(getattr(self, attr, None) == getattr(other, attr, None)
|
||||
@@ -437,38 +503,72 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
|
||||
if not eq:
|
||||
for attr in self.equality_attributes:
|
||||
print getattr(self, attr, None), getattr(other, attr, None), getattr(self, attr, None) == getattr(other, attr, None)
|
||||
print(getattr(self, attr, None),
|
||||
getattr(other, attr, None),
|
||||
getattr(self, attr, None) == getattr(other, attr, None))
|
||||
|
||||
return eq
|
||||
|
||||
def __repr__(self):
|
||||
return "{class_}({system!r}, {definition!r}, location={location!r}, metadata={metadata!r})".format(
|
||||
return ("{class_}({system!r}, {definition!r}, location={location!r},"
|
||||
" metadata={metadata!r})".format(
|
||||
class_=self.__class__.__name__,
|
||||
system=self.system,
|
||||
definition=self.definition,
|
||||
location=self.location,
|
||||
metadata=self.metadata
|
||||
)
|
||||
))
|
||||
|
||||
|
||||
class DescriptorSystem(object):
|
||||
def __init__(self, load_item, resources_fs, **kwargs):
|
||||
def __init__(self, load_item, resources_fs, error_handler):
|
||||
"""
|
||||
load_item: Takes a Location and returns an XModuleDescriptor
|
||||
|
||||
resources_fs: A Filesystem object that contains all of the
|
||||
resources needed for the course
|
||||
|
||||
error_handler: A hook for handling errors in loading the descriptor.
|
||||
Must be a function of (error_msg, exc_info=None).
|
||||
See errorhandlers.py for some simple ones.
|
||||
|
||||
Patterns for using the error handler:
|
||||
try:
|
||||
x = access_some_resource()
|
||||
check_some_format(x)
|
||||
except SomeProblem:
|
||||
msg = 'Grommet {0} is broken'.format(x)
|
||||
log.exception(msg) # don't rely on handler to log
|
||||
self.system.error_handler(msg)
|
||||
# if we get here, work around if possible
|
||||
raise # if no way to work around
|
||||
OR
|
||||
return 'Oops, couldn't load grommet'
|
||||
|
||||
OR, if not in an exception context:
|
||||
|
||||
if not check_something(thingy):
|
||||
msg = "thingy {0} is broken".format(thingy)
|
||||
log.critical(msg)
|
||||
error_handler(msg)
|
||||
# if we get here, work around
|
||||
pass # e.g. if no workaround needed
|
||||
"""
|
||||
|
||||
self.load_item = load_item
|
||||
self.resources_fs = resources_fs
|
||||
self.error_handler = error_handler
|
||||
|
||||
|
||||
class XMLParsingSystem(DescriptorSystem):
|
||||
def __init__(self, load_item, resources_fs, process_xml, **kwargs):
|
||||
def __init__(self, load_item, resources_fs, error_handler, process_xml):
|
||||
"""
|
||||
process_xml: Takes an xml string, and returns the the XModuleDescriptor created from that xml
|
||||
load_item, resources_fs, error_handler: see DescriptorSystem
|
||||
|
||||
process_xml: Takes an xml string, and returns a XModuleDescriptor
|
||||
created from that xml
|
||||
"""
|
||||
DescriptorSystem.__init__(self, load_item, resources_fs)
|
||||
DescriptorSystem.__init__(self, load_item, resources_fs, error_handler)
|
||||
self.process_xml = process_xml
|
||||
|
||||
|
||||
@@ -486,24 +586,33 @@ class ModuleSystem(object):
|
||||
'''
|
||||
def __init__(self, ajax_url, track_function,
|
||||
get_module, render_template, replace_urls,
|
||||
user=None, filestore=None, debug=False, xqueue_callback_url=None):
|
||||
user=None, filestore=None, debug=False,
|
||||
xqueue_callback_url=None):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
|
||||
ajax_url - the url where ajax calls to the encapsulating module go.
|
||||
|
||||
track_function - function of (event_type, event), intended for logging
|
||||
or otherwise tracking the event.
|
||||
TODO: Not used, and has inconsistent args in different
|
||||
files. Update or remove.
|
||||
|
||||
get_module - function that takes (location) and returns a corresponding
|
||||
module instance object.
|
||||
render_template - a function that takes (template_file, context), and returns
|
||||
rendered html.
|
||||
user - The user to base the random number generator seed off of for this request
|
||||
filestore - A filestore ojbect. Defaults to an instance of OSFS based at
|
||||
settings.DATA_DIR.
|
||||
module instance object.
|
||||
|
||||
render_template - a function that takes (template_file, context), and
|
||||
returns rendered html.
|
||||
|
||||
user - The user to base the random number generator seed off of for this
|
||||
request
|
||||
|
||||
filestore - A filestore ojbect. Defaults to an instance of OSFS based
|
||||
at settings.DATA_DIR.
|
||||
|
||||
replace_urls - TEMPORARY - A function like static_replace.replace_urls
|
||||
that capa_module can use to fix up the static urls in ajax results.
|
||||
that capa_module can use to fix up the static urls in
|
||||
ajax results.
|
||||
'''
|
||||
self.ajax_url = ajax_url
|
||||
self.xqueue_callback_url = xqueue_callback_url
|
||||
@@ -528,4 +637,3 @@ class ModuleSystem(object):
|
||||
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
|
||||
@@ -1,25 +1,33 @@
|
||||
from collections import MutableMapping
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
import copy
|
||||
import logging
|
||||
import traceback
|
||||
from collections import namedtuple
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import os
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO (cpennington): This was implemented in an attempt to improve performance,
|
||||
# but the actual improvement wasn't measured (and it was implemented late at night).
|
||||
# We should check if it hurts, and whether there's a better way of doing lazy loading
|
||||
|
||||
|
||||
class LazyLoadingDict(MutableMapping):
|
||||
"""
|
||||
A dictionary object that lazily loads it's contents from a provided
|
||||
function on reads (of members that haven't already been set)
|
||||
A dictionary object that lazily loads its contents from a provided
|
||||
function on reads (of members that haven't already been set).
|
||||
"""
|
||||
|
||||
def __init__(self, loader):
|
||||
'''
|
||||
On the first read from this dictionary, it will call loader() to
|
||||
populate its contents. loader() must return something dict-like. Any
|
||||
elements set before the first read will be preserved.
|
||||
'''
|
||||
self._contents = {}
|
||||
self._loaded = False
|
||||
self._loader = loader
|
||||
@@ -70,10 +78,17 @@ _AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata')
|
||||
|
||||
class AttrMap(_AttrMapBase):
|
||||
"""
|
||||
A class that specifies a metadata_key, a function to transform an xml attribute to be placed in that key,
|
||||
and to transform that key value
|
||||
A class that specifies a metadata_key, and two functions:
|
||||
|
||||
to_metadata: convert value from the xml representation into
|
||||
an internal python representation
|
||||
|
||||
from_metadata: convert the internal python representation into
|
||||
the value to store in the xml.
|
||||
"""
|
||||
def __new__(_cls, metadata_key, to_metadata=lambda x: x, from_metadata=lambda x: x):
|
||||
def __new__(_cls, metadata_key,
|
||||
to_metadata=lambda x: x,
|
||||
from_metadata=lambda x: x):
|
||||
return _AttrMapBase.__new__(_cls, metadata_key, to_metadata, from_metadata)
|
||||
|
||||
|
||||
@@ -88,15 +103,35 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
# The attributes will be removed from the definition xml passed
|
||||
# to definition_from_xml, and from the xml returned by definition_to_xml
|
||||
metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize',
|
||||
'start', 'due', 'graded', 'name', 'slug')
|
||||
'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc',
|
||||
# VS[compat] Remove once unused.
|
||||
'name', 'slug')
|
||||
|
||||
# A dictionary mapping xml attribute names to functions of the value
|
||||
# that return the metadata key and value
|
||||
|
||||
# A dictionary mapping xml attribute names AttrMaps that describe how
|
||||
# to import and export them
|
||||
xml_attribute_map = {
|
||||
'graded': AttrMap('graded', lambda val: val == 'true', lambda val: str(val).lower()),
|
||||
'name': AttrMap('display_name'),
|
||||
# type conversion: want True/False in python, "true"/"false" in xml
|
||||
'graded': AttrMap('graded',
|
||||
lambda val: val == 'true',
|
||||
lambda val: str(val).lower()),
|
||||
}
|
||||
|
||||
|
||||
# VS[compat]. Backwards compatibility code that can go away after
|
||||
# importing 2012 courses.
|
||||
# A set of metadata key conversions that we want to make
|
||||
metadata_translations = {
|
||||
'slug' : 'url_name',
|
||||
'name' : 'display_name',
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def _translate(cls, key):
|
||||
'VS[compat]'
|
||||
return cls.metadata_translations.get(key, key)
|
||||
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
@@ -105,12 +140,14 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
|
||||
xml_object: An etree Element
|
||||
"""
|
||||
raise NotImplementedError("%s does not implement definition_from_xml" % cls.__name__)
|
||||
raise NotImplementedError(
|
||||
"%s does not implement definition_from_xml" % cls.__name__)
|
||||
|
||||
@classmethod
|
||||
def clean_metadata_from_xml(cls, xml_object):
|
||||
"""
|
||||
Remove any attribute named in self.metadata_attributes from the supplied xml_object
|
||||
Remove any attribute named in cls.metadata_attributes from the supplied
|
||||
xml_object
|
||||
"""
|
||||
for attr in cls.metadata_attributes:
|
||||
if xml_object.get(attr) is not None:
|
||||
@@ -134,20 +171,25 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
|
||||
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
|
||||
system: A DescriptorSystem 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)
|
||||
# VS[compat] -- just have the url_name lookup once translation is done
|
||||
slug = xml_object.get('url_name', xml_object.get('slug'))
|
||||
location = Location('i4x', org, course, xml_object.tag, slug)
|
||||
|
||||
def metadata_loader():
|
||||
metadata = {}
|
||||
for attr in cls.metadata_attributes:
|
||||
val = xml_object.get(attr)
|
||||
if val is not None:
|
||||
# VS[compat]. Remove after all key translations done
|
||||
attr = cls._translate(attr)
|
||||
|
||||
attr_map = cls.xml_attribute_map.get(attr, AttrMap(attr))
|
||||
metadata[attr_map.metadata_key] = attr_map.to_metadata(val)
|
||||
|
||||
return metadata
|
||||
|
||||
def definition_loader():
|
||||
@@ -157,6 +199,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
else:
|
||||
filepath = cls._format_filepath(xml_object.tag, filename)
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): If the file doesn't exist at the right path,
|
||||
# give the class a chance to fix it up. The file will be written out again
|
||||
# in the correct format.
|
||||
@@ -169,13 +212,20 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
filepath = candidate
|
||||
break
|
||||
|
||||
log.debug('filepath=%s, resources_fs=%s' % (filepath, system.resources_fs))
|
||||
try:
|
||||
with system.resources_fs.open(filepath) as file:
|
||||
definition_xml = cls.file_to_xml(file)
|
||||
except (ResourceNotFoundError, etree.XMLSyntaxError):
|
||||
log.exception('Unable to load file contents at path %s' % filepath)
|
||||
return {'data': 'Error loading file contents at path %s' % filepath}
|
||||
msg = 'Unable to load file contents at path %s for item %s' % (filepath, location.url())
|
||||
log.exception(msg)
|
||||
system.error_handler(msg)
|
||||
# if error_handler didn't reraise, work around problem.
|
||||
error_elem = etree.Element('error')
|
||||
message_elem = etree.SubElement(error_elem, 'error_message')
|
||||
message_elem.text = msg
|
||||
stack_elem = etree.SubElement(error_elem, 'stack_trace')
|
||||
stack_elem.text = traceback.format_exc()
|
||||
return {'data': etree.tostring(error_elem)}
|
||||
|
||||
cls.clean_metadata_from_xml(definition_xml)
|
||||
return cls.definition_from_xml(definition_xml, system)
|
||||
@@ -183,64 +233,90 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
return cls(
|
||||
system,
|
||||
LazyLoadingDict(definition_loader),
|
||||
location=['i4x',
|
||||
org,
|
||||
course,
|
||||
xml_object.tag,
|
||||
xml_object.get('slug')],
|
||||
location=location,
|
||||
metadata=LazyLoadingDict(metadata_loader),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _format_filepath(cls, category, name):
|
||||
return u'{category}/{name}.{ext}'.format(category=category, name=name, ext=cls.filename_extension)
|
||||
return u'{category}/{name}.{ext}'.format(category=category,
|
||||
name=name,
|
||||
ext=cls.filename_extension)
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
'''
|
||||
Decide whether to write this object to a separate file or not.
|
||||
|
||||
xml_object: an xml definition of an instance of cls.
|
||||
|
||||
This default implementation will split if this has more than 7
|
||||
descendant tags.
|
||||
|
||||
Can be overridden by subclasses.
|
||||
'''
|
||||
return len(list(xml_object.iter())) > 7
|
||||
|
||||
def export_to_xml(self, resource_fs):
|
||||
"""
|
||||
Returns an xml string representing this module, and all modules underneath it.
|
||||
May also write required resources out to resource_fs
|
||||
Returns an xml string representing this module, and all modules
|
||||
underneath it. May also write required resources out to resource_fs
|
||||
|
||||
Assumes that modules have single parantage (that no module appears twice in the same course),
|
||||
and that it is thus safe to nest modules as xml children as appropriate.
|
||||
Assumes that modules have single parentage (that no module appears twice
|
||||
in the same course), and that it is thus safe to nest modules as xml
|
||||
children as appropriate.
|
||||
|
||||
The returned XML should be able to be parsed back into an identical XModuleDescriptor
|
||||
using the from_xml method with the same system, org, and course
|
||||
The returned XML should be able to be parsed back into an identical
|
||||
XModuleDescriptor using the from_xml method with the same system, org,
|
||||
and course
|
||||
|
||||
resource_fs is a pyfilesystem office (from the fs package)
|
||||
resource_fs is a pyfilesystem object (from the fs package)
|
||||
"""
|
||||
|
||||
# Get the definition
|
||||
xml_object = self.definition_to_xml(resource_fs)
|
||||
self.__class__.clean_metadata_from_xml(xml_object)
|
||||
|
||||
# Put content in a separate file if it's large (has more than 5 descendent tags)
|
||||
if len(list(xml_object.iter())) > 5:
|
||||
# Set the tag first, so it's right if writing to a file
|
||||
xml_object.tag = self.category
|
||||
|
||||
# Write it to a file if necessary
|
||||
if self.split_to_file(xml_object):
|
||||
# Put this object in it's own file
|
||||
filepath = self.__class__._format_filepath(self.category, self.name)
|
||||
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as file:
|
||||
file.write(etree.tostring(xml_object, pretty_print=True))
|
||||
|
||||
# ...and remove all of its children here
|
||||
for child in xml_object:
|
||||
xml_object.remove(child)
|
||||
# also need to remove the text of this object.
|
||||
xml_object.text = ''
|
||||
# and the tail for good measure...
|
||||
xml_object.tail = ''
|
||||
|
||||
|
||||
xml_object.set('filename', self.name)
|
||||
|
||||
xml_object.set('slug', self.name)
|
||||
xml_object.tag = self.category
|
||||
|
||||
# Add the metadata
|
||||
xml_object.set('url_name', self.name)
|
||||
for attr in self.metadata_attributes:
|
||||
attr_map = self.xml_attribute_map.get(attr, AttrMap(attr))
|
||||
metadata_key = attr_map.metadata_key
|
||||
|
||||
if metadata_key not in self.metadata or metadata_key in self._inherited_metadata:
|
||||
if (metadata_key not in self.metadata or
|
||||
metadata_key in self._inherited_metadata):
|
||||
continue
|
||||
|
||||
val = attr_map.from_metadata(self.metadata[metadata_key])
|
||||
xml_object.set(attr, val)
|
||||
|
||||
# Now we just have to make it beautiful
|
||||
return etree.tostring(xml_object, pretty_print=True)
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
"""
|
||||
Return a new etree Element object created from this modules definition.
|
||||
"""
|
||||
raise NotImplementedError("%s does not implement definition_to_xml" % self.__class__.__name__)
|
||||
raise NotImplementedError(
|
||||
"%s does not implement definition_to_xml" % self.__class__.__name__)
|
||||
|
||||
Reference in New Issue
Block a user