Commit 63f34f2e by Victor Shnayder

Line length and doc string cleanups

* no functionality changed in this commit.
parent d43831e1
...@@ -12,8 +12,8 @@ log = logging.getLogger(__name__) ...@@ -12,8 +12,8 @@ log = logging.getLogger(__name__)
def process_includes(fn): def process_includes(fn):
""" """
Wraps a XModuleDescriptor.from_xml method, and modifies xml_data to replace 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 any immediate child <include> items with the contents of the file that they
supposed to include are supposed to include
""" """
@wraps(fn) @wraps(fn)
def from_xml(cls, xml_data, system, org=None, course=None): def from_xml(cls, xml_data, system, org=None, course=None):
...@@ -25,15 +25,19 @@ def process_includes(fn): ...@@ -25,15 +25,19 @@ def process_includes(fn):
try: try:
ifp = system.resources_fs.open(file) ifp = system.resources_fs.open(file)
except Exception: except Exception:
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True))) msg = 'Error in problem xml include: %s\n' % (
log.exception('Cannot find file %s in %s' % (file, dir)) etree.tostring(next_include, pretty_print=True))
msg += 'Cannot find file %s in %s' % (file, dir)
log.exception(msg)
raise raise
try: try:
# read in and convert to XML # read in and convert to XML
incxml = etree.XML(ifp.read()) incxml = etree.XML(ifp.read())
except Exception: except Exception:
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True))) msg = 'Error in problem xml include: %s\n' % (
log.exception('Cannot parse XML in %s' % (file)) etree.tostring(next_include, pretty_print=True))
msg += 'Cannot parse XML in %s' % (file)
log.exception(msg)
raise raise
# insert new XML into tree in place of inlcude # insert new XML into tree in place of inlcude
parent = next_include.getparent() parent = next_include.getparent()
...@@ -50,8 +54,8 @@ class SemanticSectionDescriptor(XModuleDescriptor): ...@@ -50,8 +54,8 @@ class SemanticSectionDescriptor(XModuleDescriptor):
@process_includes @process_includes
def from_xml(cls, xml_data, system, org=None, course=None): 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) xml_object = etree.fromstring(xml_data)
......
...@@ -67,7 +67,8 @@ class ComplexEncoder(json.JSONEncoder): ...@@ -67,7 +67,8 @@ class ComplexEncoder(json.JSONEncoder):
class CapaModule(XModule): 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' icon_class = 'problem'
...@@ -77,8 +78,10 @@ class CapaModule(XModule): ...@@ -77,8 +78,10 @@ class CapaModule(XModule):
js_module_name = "Problem" js_module_name = "Problem"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): def __init__(self, system, location, definition, instance_state=None,
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state,
shared_state, **kwargs)
self.attempts = 0 self.attempts = 0
self.max_attempts = None self.max_attempts = None
...@@ -133,7 +136,8 @@ class CapaModule(XModule): ...@@ -133,7 +136,8 @@ class CapaModule(XModule):
seed = None seed = None
try: 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: except Exception:
msg = 'cannot create LoncapaProblem %s' % self.location.url() msg = 'cannot create LoncapaProblem %s' % self.location.url()
log.exception(msg) log.exception(msg)
...@@ -141,15 +145,20 @@ class CapaModule(XModule): ...@@ -141,15 +145,20 @@ class CapaModule(XModule):
msg = '<p>%s</p>' % msg.replace('<', '&lt;') msg = '<p>%s</p>' % msg.replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;') msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
# create a dummy problem with error message instead of failing # 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) problem_text = ('<problem><text><font color="red" size="+2">'
self.lcp = LoncapaProblem(problem_text, self.location.html_id(), instance_state, seed=seed, system=self.system) '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: else:
raise raise
@property @property
def rerandomize(self): 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') rerandomize = self.metadata.get('rerandomize', 'always')
if rerandomize in ("", "always", "true"): if rerandomize in ("", "always", "true"):
...@@ -203,7 +212,10 @@ class CapaModule(XModule): ...@@ -203,7 +212,10 @@ class CapaModule(XModule):
except Exception, err: except Exception, err:
if self.system.DEBUG: if self.system.DEBUG:
log.exception(err) 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('<', '&lt;') msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;') msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
html = msg html = msg
...@@ -215,8 +227,8 @@ class CapaModule(XModule): ...@@ -215,8 +227,8 @@ class CapaModule(XModule):
'weight': self.weight, 'weight': self.weight,
} }
# We using strings as truthy values, because the terminology of the check button # We using strings as truthy values, because the terminology of the
# is context-specific. # check button is context-specific.
check_button = "Grade" if self.max_attempts else "Check" check_button = "Grade" if self.max_attempts else "Check"
reset_button = True reset_button = True
save_button = True save_button = True
...@@ -242,7 +254,8 @@ class CapaModule(XModule): ...@@ -242,7 +254,8 @@ class CapaModule(XModule):
if not self.lcp.done: if not self.lcp.done:
reset_button = False 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": if self.max_attempts is None and self.rerandomize != "always":
save_button = False save_button = False
......
...@@ -17,11 +17,12 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -17,11 +17,12 @@ class CourseDescriptor(SequenceDescriptor):
try: try:
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M") self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
except KeyError: except KeyError:
self.start = time.gmtime(0) # The epoch self.start = time.gmtime(0) #The epoch
log.critical("Course loaded without a start date. " + str(self.id)) log.critical("Course loaded without a start date. %s", self.id)
except ValueError, e: except ValueError as e:
self.start = time.gmtime(0) # The epoch self.start = time.gmtime(0) #The epoch
log.critical("Course loaded with a bad start date. " + str(self.id) + " '" + str(e) + "'") log.critical("Course loaded with a bad start date. %s '%s'",
self.id, e)
def has_started(self): def has_started(self):
return time.gmtime() > self.start return time.gmtime() > self.start
......
...@@ -19,7 +19,9 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -19,7 +19,9 @@ class MakoModuleDescriptor(XModuleDescriptor):
def __init__(self, system, definition=None, **kwargs): def __init__(self, system, definition=None, **kwargs):
if getattr(system, 'render_template', None) is None: 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) super(MakoModuleDescriptor, self).__init__(system, definition, **kwargs)
def get_context(self): def get_context(self):
...@@ -29,4 +31,5 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -29,4 +31,5 @@ class MakoModuleDescriptor(XModuleDescriptor):
return {'module': self} return {'module': self}
def get_html(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,17 @@ class Location(_LocationBase): ...@@ -45,13 +45,17 @@ class Location(_LocationBase):
""" """
return re.sub('_+', '_', INVALID_CHARS.sub('_', value)) return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
def __new__(_cls, loc_or_tag=None, org=None, course=None, category=None, name=None, revision=None): 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. Create a new location that is a clone of the specifed one.
location - Can be any of the following types: 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] list: should be of the form [tag, org, course, category, name, revision]
dict: should be of the form { dict: should be of the form {
'tag': tag, 'tag': tag,
'org': org, 'org': org,
...@@ -62,16 +66,19 @@ class Location(_LocationBase): ...@@ -62,16 +66,19 @@ class Location(_LocationBase):
} }
Location: another Location object 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 Components may be set to None, which may be interpreted by some contexts
wildcard selection 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 location = loc_or_tag
else: else:
location = (loc_or_tag, org, course, category, name, revision) location = (loc_or_tag, org, course, category, name, revision)
...@@ -131,9 +138,11 @@ class Location(_LocationBase): ...@@ -131,9 +138,11 @@ class Location(_LocationBase):
def html_id(self): 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): def dict(self):
""" """
...@@ -154,7 +163,8 @@ class Location(_LocationBase): ...@@ -154,7 +163,8 @@ class Location(_LocationBase):
class ModuleStore(object): 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): def get_item(self, location, depth=0):
""" """
...@@ -164,13 +174,16 @@ class ModuleStore(object): ...@@ -164,13 +174,16 @@ class ModuleStore(object):
If any segment of the location is None except revision, raises If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError 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 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 depth (int): An argument that some module stores may use to prefetch
for more efficient results later in the request. The depth is counted in the number of descendents of the queried modules for more efficient results later
calls to get_children() to cache. None indicates to cache all descendents 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 raise NotImplementedError
...@@ -182,9 +195,10 @@ class ModuleStore(object): ...@@ -182,9 +195,10 @@ class ModuleStore(object):
location: Something that can be passed to Location location: Something that can be passed to Location
depth: An argument that some module stores may use to prefetch descendents of the queried modules depth: An argument that some module stores may use to prefetch
for more efficient results later in the request. The depth is counted in the number of calls descendents of the queried modules for more efficient results later
to get_children() to cache. None indicates to cache all descendents 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 raise NotImplementedError
...@@ -228,4 +242,3 @@ class ModuleStore(object): ...@@ -228,4 +242,3 @@ class ModuleStore(object):
in this modulestore. in this modulestore.
''' '''
raise NotImplementedError raise NotImplementedError
...@@ -8,7 +8,7 @@ log = logging.getLogger(__name__) ...@@ -8,7 +8,7 @@ log = logging.getLogger(__name__)
class RawDescriptor(MakoModuleDescriptor, XmlDescriptor): 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" mako_template = "widgets/raw-edit.html"
......
...@@ -20,12 +20,15 @@ class_priority = ['video', 'problem'] ...@@ -20,12 +20,15 @@ class_priority = ['video', 'problem']
class SequenceModule(XModule): class SequenceModule(XModule):
''' Layout module which lays out content in a temporal sequence ''' 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')]} css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
js_module_name = "Sequence" js_module_name = "Sequence"
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): def __init__(self, system, location, definition, instance_state=None,
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition,
instance_state, shared_state, **kwargs)
self.position = 1 self.position = 1
if instance_state is not None: if instance_state is not None:
...@@ -92,7 +95,8 @@ class SequenceModule(XModule): ...@@ -92,7 +95,8 @@ class SequenceModule(XModule):
self.rendered = True self.rendered = True
def get_icon_class(self): 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' new_class = 'other'
for c in class_priority: for c in class_priority:
if c in child_classes: if c in child_classes:
...@@ -114,5 +118,6 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): ...@@ -114,5 +118,6 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
xml_object = etree.Element('sequential') xml_object = etree.Element('sequential')
for child in self.get_children(): 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 return xml_object
...@@ -31,23 +31,28 @@ class Plugin(object): ...@@ -31,23 +31,28 @@ class Plugin(object):
def load_class(cls, identifier, default=None): def load_class(cls, identifier, default=None):
""" """
Loads a single class instance specified by identifier. If identifier Loads a single class instance specified by identifier. If identifier
specifies more than a single class, then logs a warning and returns the first specifies more than a single class, then logs a warning and returns the
class identified. first class identified.
If default is not None, will return default if no entry_point matching identifier If default is not None, will return default if no entry_point matching
is found. Otherwise, will raise a ModuleMissingError identifier is found. Otherwise, will raise a ModuleMissingError
""" """
if cls._plugin_cache is None: if cls._plugin_cache is None:
cls._plugin_cache = {} cls._plugin_cache = {}
if identifier not in cls._plugin_cache: if identifier not in cls._plugin_cache:
identifier = identifier.lower() 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: 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, entry_point=cls.entry_point,
id=identifier, 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 len(classes) == 0:
if default is not None: if default is not None:
...@@ -79,9 +84,12 @@ class HTMLSnippet(object): ...@@ -79,9 +84,12 @@ class HTMLSnippet(object):
def get_javascript(cls): def get_javascript(cls):
""" """
Return a dictionary containing some of the following keys: Return a dictionary containing some of the following keys:
coffee: A list of coffeescript fragments that should be compiled and coffee: A list of coffeescript fragments that should be compiled and
placed on the page 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 All of these will be loaded onto the page in the CMS
""" """
...@@ -91,12 +99,15 @@ class HTMLSnippet(object): ...@@ -91,12 +99,15 @@ class HTMLSnippet(object):
def get_css(cls): def get_css(cls):
""" """
Return a dictionary containing some of the following keys: 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 css: A list of css fragments that should be applied to the html
sass: A list of sass fragments that should be applied to the html contents contents of the snippet
of the snippet
scss: A list of scss fragments that should be applied to the html contents sass: A list of sass fragments that should be applied to the html
of the snippet contents of the snippet
scss: A list of scss fragments that should be applied to the html
contents of the snippet
""" """
return cls.css return cls.css
...@@ -104,47 +115,70 @@ class HTMLSnippet(object): ...@@ -104,47 +115,70 @@ class HTMLSnippet(object):
""" """
Return the html used to display this snippet Return the html used to display this snippet
""" """
raise NotImplementedError("get_html() must be provided by specific modules - not present in {0}" raise NotImplementedError(
"get_html() must be provided by specific modules - not present in {0}"
.format(self.__class__)) .format(self.__class__))
class XModule(HTMLSnippet): class XModule(HTMLSnippet):
''' Implements a generic learning module. ''' 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. See the HTML module for a simple example.
''' '''
# The default implementation of get_icon_class returns the icon_class attribute of the class # The default implementation of get_icon_class returns the icon_class
# This attribute can be overridden by subclasses, and the function can also be overridden # attribute of the class
# if the icon class depends on the data in the module #
# 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' 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 Construct a new xmodule
system: A ModuleSystem allowing access to external resources system: A ModuleSystem allowing access to external resources
location: Something Location-like that identifies this xmodule 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). definition: A dictionary containing 'data' and 'children'. Both are
This defines all of the data necessary for a problem to display that is intrinsic to the problem. optional
It should not include any data that would vary between two courses using the same problem
'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.) (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 'children': is a list of Location-like values for child modules that
current student accessing the system, or None if no state has been saved this module depends on
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 instance_state: A string of serialized json that contains the state of
state is only shared per-student, not across different students this module for current student accessing the system, or None if
kwargs: Optional arguments. Subclasses should always accept kwargs and pass them no state has been saved
to the parent class constructor.
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: 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). metadata: SCAFFOLDING - This dictionary will be split into
A dictionary containing data that specifies information that is particular several different types of metadata in the future (course
to a problem in the context of a 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.system = system
self.location = Location(location) self.location = Location(location)
...@@ -217,16 +251,21 @@ class XModule(HTMLSnippet): ...@@ -217,16 +251,21 @@ class XModule(HTMLSnippet):
def max_score(self): def max_score(self):
''' Maximum score. Two notes: ''' 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 * This is generic; in abstract, a problem could be 3/5 points on one
should get fixed), and (b) break some analytics we plan to put in place. 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 return None
def get_progress(self): def get_progress(self):
''' Return a progress.Progress object that represents how far the student has gone ''' Return a progress.Progress object that represents how far the
in this module. Must be implemented to get correct progress tracking behavior in student has gone in this module. Must be implemented to get correct
nesting modules like sequence and vertical. progress tracking behavior in nesting modules like sequence and
vertical.
If this module has no notion of progress, return None. If this module has no notion of progress, return None.
''' '''
...@@ -240,13 +279,14 @@ class XModule(HTMLSnippet): ...@@ -240,13 +279,14 @@ class XModule(HTMLSnippet):
class XModuleDescriptor(Plugin, HTMLSnippet): class XModuleDescriptor(Plugin, HTMLSnippet):
""" """
An XModuleDescriptor is a specification for an element of a course. This could An XModuleDescriptor is a specification for an element of a course. This
be a problem, an organizational element (a group of content), or a segment of video, could be a problem, an organizational element (a group of content), or a
for example. segment of video, for example.
XModuleDescriptors are independent and agnostic to the current student state on a XModuleDescriptors are independent and agnostic to the current student state
problem. They handle the editing interface used by instructors to create a problem, on a problem. They handle the editing interface used by instructors to
and can generate XModules (which do know about student state). create a problem, and can generate XModules (which do know about student
state).
""" """
entry_point = "xmodule.v1" entry_point = "xmodule.v1"
module_class = XModule module_class = XModule
...@@ -255,46 +295,58 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -255,46 +295,58 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
inheritable_metadata = ( inheritable_metadata = (
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize', 'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
# This is used by the XMLModuleStore to provide for locations for static files, # TODO: This is used by the XMLModuleStore to provide for locations for
# and will need to be removed when that code is removed # static files, and will need to be removed when that code is removed
'data_dir' 'data_dir'
) )
# A list of descriptor attributes that must be equal for the descriptors to be # A list of descriptor attributes that must be equal for the descriptors to
# equal # be equal
equality_attributes = ('definition', 'metadata', 'location', 'shared_state_key', '_inherited_metadata') equality_attributes = ('definition', 'metadata', 'location',
'shared_state_key', '_inherited_metadata')
# ============================= STRUCTURAL MANIPULATION =========================== # ============================= STRUCTURAL MANIPULATION ===================
def __init__(self, def __init__(self,
system, system,
definition=None, definition=None,
**kwargs): **kwargs):
""" """
Construct a new XModuleDescriptor. The only required arguments are the Construct a new XModuleDescriptor. The only required arguments are the
system, used for interaction with external resources, and the definition, system, used for interaction with external resources, and the
which specifies all the data needed to edit and display the problem (but none definition, which specifies all the data needed to edit and display the
of the associated metadata that handles recordkeeping around the problem). 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 system: A DescriptorSystem for interacting with external resources
backwards compatibility.
system: An XModuleSystem for interacting with external resources definition: A dict containing `data` and `children` representing the
definition: A dict containing `data` and `children` representing the problem definition problem definition
Current arguments passed in kwargs: 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 location: A xmodule.modulestore.Location object indicating the name
modules of this type 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: metadata: A dictionary containing the following optional keys:
goals: A list of strings of learning goals associated with this module goals: A list of strings of learning goals associated with this
display_name: The name to use for displaying this module to the user module
display_name: The name to use for displaying this module to the
user
format: The format of this module ('Homework', 'Lab', etc) format: The format of this module ('Homework', 'Lab', etc)
graded (bool): Whether this module is should be graded or not graded (bool): Whether this module is should be graded or not
start (string): The date for which this module will be available start (string): The date for which this module will be available
due (string): The due date for this module 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 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.system = system
self.metadata = kwargs.get('metadata', {}) self.metadata = kwargs.get('metadata', {})
...@@ -321,7 +373,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -321,7 +373,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
self.metadata[attr] = metadata[attr] self.metadata[attr] = metadata[attr]
def get_children(self): 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: if self._child_instances is None:
self._child_instances = [] self._child_instances = []
for child_loc in self.definition.get('children', []): for child_loc in self.definition.get('children', []):
...@@ -333,8 +386,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -333,8 +386,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
def xmodule_constructor(self, system): def xmodule_constructor(self, system):
""" """
Returns a constructor for an XModule. This constructor takes two arguments: Returns a constructor for an XModule. This constructor takes two
instance_state and shared_state, and returns a fully nstantiated XModule arguments: instance_state and shared_state, and returns a fully
instantiated XModule
""" """
return partial( return partial(
self.module_class, self.module_class,
...@@ -344,7 +398,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -344,7 +398,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
metadata=self.metadata metadata=self.metadata
) )
# ================================= JSON PARSING =================================== # ================================= JSON PARSING ===========================
@staticmethod @staticmethod
def load_from_json(json_data, system, default_class=None): def load_from_json(json_data, system, default_class=None):
""" """
...@@ -366,13 +420,14 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -366,13 +420,14 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
Creates an instance of this descriptor from the supplied json_data. Creates an instance of this descriptor from the supplied json_data.
This may be overridden by subclasses This may be overridden by subclasses
json_data: A json object specifying the definition and any optional keyword arguments for json_data: A json object specifying the definition and any optional
the XModuleDescriptor keyword arguments for the XModuleDescriptor
system: An XModuleSystem for interacting with external resources
system: A DescriptorSystem for interacting with external resources
""" """
return cls(system=system, **json_data) return cls(system=system, **json_data)
# ================================= XML PARSING ==================================== # ================================= XML PARSING ============================
@staticmethod @staticmethod
def load_from_xml(xml_data, def load_from_xml(xml_data,
system, system,
...@@ -487,24 +542,33 @@ class ModuleSystem(object): ...@@ -487,24 +542,33 @@ class ModuleSystem(object):
''' '''
def __init__(self, ajax_url, track_function, def __init__(self, ajax_url, track_function,
get_module, render_template, replace_urls, 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. Create a closure around the system environment.
ajax_url - the url where ajax calls to the encapsulating module go. ajax_url - the url where ajax calls to the encapsulating module go.
track_function - function of (event_type, event), intended for logging track_function - function of (event_type, event), intended for logging
or otherwise tracking the event. or otherwise tracking the event.
TODO: Not used, and has inconsistent args in different TODO: Not used, and has inconsistent args in different
files. Update or remove. files. Update or remove.
get_module - function that takes (location) and returns a corresponding get_module - function that takes (location) and returns a corresponding
module instance object. module instance object.
render_template - a function that takes (template_file, context), and returns
rendered html. render_template - a function that takes (template_file, context), and
user - The user to base the random number generator seed off of for this request returns rendered html.
filestore - A filestore ojbect. Defaults to an instance of OSFS based at
settings.DATA_DIR. 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 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.ajax_url = ajax_url
self.xqueue_callback_url = xqueue_callback_url self.xqueue_callback_url = xqueue_callback_url
...@@ -529,4 +593,3 @@ class ModuleSystem(object): ...@@ -529,4 +593,3 @@ class ModuleSystem(object):
def __str__(self): def __str__(self):
return str(self.__dict__) return str(self.__dict__)
...@@ -13,13 +13,19 @@ log = logging.getLogger(__name__) ...@@ -13,13 +13,19 @@ log = logging.getLogger(__name__)
# TODO (cpennington): This was implemented in an attempt to improve performance, # 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). # 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 # We should check if it hurts, and whether there's a better way of doing lazy loading
class LazyLoadingDict(MutableMapping): class LazyLoadingDict(MutableMapping):
""" """
A dictionary object that lazily loads it's contents from a provided A dictionary object that lazily loads its contents from a provided
function on reads (of members that haven't already been set) function on reads (of members that haven't already been set).
""" """
def __init__(self, loader): 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._contents = {}
self._loaded = False self._loaded = False
self._loader = loader self._loader = loader
...@@ -70,10 +76,12 @@ _AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata') ...@@ -70,10 +76,12 @@ _AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata')
class AttrMap(_AttrMapBase): class AttrMap(_AttrMapBase):
""" """
A class that specifies a metadata_key, a function to transform an xml attribute to be placed in that key, A class that specifies a metadata_key, a function to transform an xml
and to transform that key value attribute to be placed in that key, and to transform that key value
""" """
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) return _AttrMapBase.__new__(_cls, metadata_key, to_metadata, from_metadata)
...@@ -93,7 +101,9 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -93,7 +101,9 @@ class XmlDescriptor(XModuleDescriptor):
# A dictionary mapping xml attribute names to functions of the value # A dictionary mapping xml attribute names to functions of the value
# that return the metadata key and value # that return the metadata key and value
xml_attribute_map = { xml_attribute_map = {
'graded': AttrMap('graded', lambda val: val == 'true', lambda val: str(val).lower()), 'graded': AttrMap('graded',
lambda val: val == 'true',
lambda val: str(val).lower()),
'name': AttrMap('display_name'), 'name': AttrMap('display_name'),
} }
...@@ -105,12 +115,14 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -105,12 +115,14 @@ class XmlDescriptor(XModuleDescriptor):
xml_object: An etree Element 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 @classmethod
def clean_metadata_from_xml(cls, xml_object): 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: for attr in cls.metadata_attributes:
if xml_object.get(attr) is not None: if xml_object.get(attr) is not None:
...@@ -134,7 +146,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -134,7 +146,7 @@ class XmlDescriptor(XModuleDescriptor):
xml_data: A string of xml that will be translated into data and children for xml_data: A string of xml that will be translated into data and children for
this module 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 org and course are optional strings that will be used in the generated modules
url identifiers url identifiers
""" """
...@@ -157,6 +169,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -157,6 +169,7 @@ class XmlDescriptor(XModuleDescriptor):
else: else:
filepath = cls._format_filepath(xml_object.tag, filename) filepath = cls._format_filepath(xml_object.tag, filename)
# VS[compat]
# TODO (cpennington): If the file doesn't exist at the right path, # 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 # give the class a chance to fix it up. The file will be written out again
# in the correct format. # in the correct format.
...@@ -243,4 +256,5 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -243,4 +256,5 @@ class XmlDescriptor(XModuleDescriptor):
""" """
Return a new etree Element object created from this modules definition. 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__)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment