Begin using a Keystore for XML parsing. Still broken: sequence icons, custom tags, problems, video js
This commit is contained in:
87
common/lib/keystore/xml.py
Normal file
87
common/lib/keystore/xml.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from fs.osfs import OSFS
|
||||
from importlib import import_module
|
||||
from lxml import etree
|
||||
from path import path
|
||||
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
|
||||
|
||||
from . import ModuleStore, Location
|
||||
from .exceptions import ItemNotFoundError
|
||||
|
||||
|
||||
class XMLModuleStore(ModuleStore):
|
||||
"""
|
||||
An XML backed ModuleStore
|
||||
"""
|
||||
def __init__(self, org, course, data_dir, default_class=None):
|
||||
self.data_dir = path(data_dir)
|
||||
self.modules = {}
|
||||
|
||||
module_path, _, class_name = default_class.rpartition('.')
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
|
||||
with open(data_dir / "course.xml") as course_file:
|
||||
class ImportSystem(XMLParsingSystem):
|
||||
def __init__(self, keystore):
|
||||
self.unnamed_modules = 0
|
||||
|
||||
def process_xml(xml):
|
||||
try:
|
||||
xml_data = etree.fromstring(xml)
|
||||
except:
|
||||
print xml
|
||||
raise
|
||||
if xml_data.get('name'):
|
||||
xml_data.set('slug', Location.clean(xml_data.get('name')))
|
||||
else:
|
||||
self.unnamed_modules += 1
|
||||
xml_data.set('slug', '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules))
|
||||
|
||||
module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, keystore.default_class)
|
||||
keystore.modules[module.url] = module
|
||||
return module
|
||||
|
||||
XMLParsingSystem.__init__(self, keystore.get_item, OSFS(data_dir), process_xml)
|
||||
|
||||
ImportSystem(self).process_xml(course_file.read())
|
||||
|
||||
def get_item(self, location):
|
||||
"""
|
||||
Returns an XModuleDescriptor instance for the item at location.
|
||||
If location.revision is None, returns the most item with the most
|
||||
recent revision
|
||||
|
||||
If any segment of the location is None except revision, raises
|
||||
keystore.exceptions.InsufficientSpecificationError
|
||||
If no object is found at that location, raises keystore.exceptions.ItemNotFoundError
|
||||
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
location = Location(location)
|
||||
try:
|
||||
return self.modules[location.url()]
|
||||
except KeyError:
|
||||
raise ItemNotFoundError(location)
|
||||
|
||||
def create_item(self, location):
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
def update_item(self, location, data):
|
||||
"""
|
||||
Set the data in the item specified by the location to
|
||||
data
|
||||
|
||||
location: Something that can be passed to Location
|
||||
data: A nested dictionary of problem data
|
||||
"""
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
|
||||
def update_children(self, location, children):
|
||||
"""
|
||||
Set the children for the item specified by the location to
|
||||
data
|
||||
|
||||
location: Something that can be passed to Location
|
||||
children: A list of child item identifiers
|
||||
"""
|
||||
raise NotImplementedError("XMLModuleStores are read-only")
|
||||
@@ -1,62 +0,0 @@
|
||||
import capa_module
|
||||
import html_module
|
||||
import schematic_module
|
||||
import seq_module
|
||||
import template_module
|
||||
import vertical_module
|
||||
import video_module
|
||||
|
||||
# Import all files in modules directory, excluding backups (# and . in name)
|
||||
# and __init__
|
||||
#
|
||||
# Stick them in a list
|
||||
# modx_module_list = []
|
||||
|
||||
# for f in os.listdir(os.path.dirname(__file__)):
|
||||
# if f!='__init__.py' and \
|
||||
# f[-3:] == ".py" and \
|
||||
# "." not in f[:-3] \
|
||||
# and '#' not in f:
|
||||
# mod_path = 'courseware.modules.'+f[:-3]
|
||||
# mod = __import__(mod_path, fromlist = "courseware.modules")
|
||||
# if 'Module' in mod.__dict__:
|
||||
# modx_module_list.append(mod)
|
||||
|
||||
#print modx_module_list
|
||||
modx_module_list = [capa_module, html_module, schematic_module, seq_module, template_module, vertical_module, video_module]
|
||||
#print modx_module_list
|
||||
|
||||
modx_modules = {}
|
||||
|
||||
# Convert list to a dictionary for lookup by tag
|
||||
def update_modules():
|
||||
global modx_modules
|
||||
modx_modules = dict()
|
||||
for module in modx_module_list:
|
||||
for tag in module.Module.get_xml_tags():
|
||||
modx_modules[tag] = module.Module
|
||||
|
||||
update_modules()
|
||||
|
||||
def get_module_class(tag):
|
||||
''' Given an XML tag (e.g. 'video'), return
|
||||
the associated module (e.g. video_module.Module).
|
||||
'''
|
||||
if tag not in modx_modules:
|
||||
update_modules()
|
||||
return modx_modules[tag]
|
||||
|
||||
def get_module_id(tag):
|
||||
''' Given an XML tag (e.g. 'video'), return
|
||||
the default ID for that module (e.g. 'youtube_id')
|
||||
'''
|
||||
return modx_modules[tag].id_attribute
|
||||
|
||||
def get_valid_tags():
|
||||
return modx_modules.keys()
|
||||
|
||||
def get_default_ids():
|
||||
tags = get_valid_tags()
|
||||
ids = map(get_module_id, tags)
|
||||
return dict(zip(tags, ids))
|
||||
|
||||
|
||||
95
common/lib/xmodule/abtest_module.py
Normal file
95
common/lib/xmodule/abtest_module.py
Normal file
@@ -0,0 +1,95 @@
|
||||
import json
|
||||
import random
|
||||
from lxml import etree
|
||||
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
|
||||
def group_from_value(groups, v):
|
||||
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
|
||||
in [0,1], return the associated group (in the above case, return
|
||||
'a' if v<0.3, 'b' if 0.3<=v<0.7, and 'c' if v>0.7
|
||||
'''
|
||||
sum = 0
|
||||
for (g, p) in groups:
|
||||
sum = sum + p
|
||||
if sum > v:
|
||||
return g
|
||||
|
||||
# Round off errors might cause us to run to the end of the list
|
||||
# If the do, return the last element
|
||||
return g
|
||||
|
||||
|
||||
class Module(XModule):
|
||||
"""
|
||||
Implements an A/B test with an aribtrary number of competing groups
|
||||
|
||||
Format:
|
||||
<abtest>
|
||||
<group name="a" portion=".1"><contenta/></group>
|
||||
<group name="b" portion=".2"><contentb/></group>
|
||||
<default><contentdefault/></default>
|
||||
</abtest>
|
||||
"""
|
||||
|
||||
def __init__(self, system, xml, item_id, instance_state=None, shared_state=None):
|
||||
XModule.__init__(self, system, xml, item_id, instance_state, shared_state)
|
||||
self.xmltree = etree.fromstring(xml)
|
||||
|
||||
target_groups = self.xmltree.findall('group')
|
||||
if shared_state is None:
|
||||
target_values = [
|
||||
(elem.get('name'), float(elem.get('portion')))
|
||||
for elem in target_groups
|
||||
]
|
||||
default_value = 1 - sum(val for (_, val) in target_values)
|
||||
|
||||
self.group = group_from_value(
|
||||
target_values + [(None, default_value)],
|
||||
random.uniform(0, 1)
|
||||
)
|
||||
else:
|
||||
shared_state = json.loads(shared_state)
|
||||
|
||||
# TODO (cpennington): Remove this once we aren't passing in
|
||||
# groups from django groups
|
||||
if 'groups' in shared_state:
|
||||
self.group = None
|
||||
target_names = [elem.get('name') for elem in target_groups]
|
||||
for group in shared_state['groups']:
|
||||
if group in target_names:
|
||||
self.group = group
|
||||
break
|
||||
else:
|
||||
self.group = shared_state['group']
|
||||
|
||||
def get_shared_state(self):
|
||||
return json.dumps({'group': self.group})
|
||||
|
||||
def _xml_children(self):
|
||||
group = None
|
||||
if self.group is None:
|
||||
group = self.xmltree.find('default')
|
||||
else:
|
||||
for candidate_group in self.xmltree.find('group'):
|
||||
if self.group == candidate_group.get('name'):
|
||||
group = candidate_group
|
||||
break
|
||||
|
||||
if group is None:
|
||||
return []
|
||||
return list(group)
|
||||
|
||||
def get_children(self):
|
||||
return [self.module_from_xml(child) for child in self._xml_children()]
|
||||
|
||||
def rendered_children(self):
|
||||
return [self.render_function(child) for child in self._xml_children()]
|
||||
|
||||
def get_html(self):
|
||||
return '\n'.join(child.get_html() for child in self.get_children())
|
||||
@@ -81,14 +81,7 @@ class Module(XModule):
|
||||
reset.
|
||||
'''
|
||||
|
||||
id_attribute = "filename"
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ["problem"]
|
||||
|
||||
|
||||
def get_state(self):
|
||||
def get_instance_state(self):
|
||||
state = self.lcp.get_state()
|
||||
state['attempts'] = self.attempts
|
||||
return json.dumps(state)
|
||||
@@ -191,8 +184,8 @@ class Module(XModule):
|
||||
|
||||
return html
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
def __init__(self, system, xml, item_id, instance_state=None, shared_state=None):
|
||||
XModule.__init__(self, system, xml, item_id, instance_state, shared_state)
|
||||
|
||||
self.attempts = 0
|
||||
self.max_attempts = None
|
||||
@@ -232,19 +225,19 @@ class Module(XModule):
|
||||
self.show_answer = "closed"
|
||||
|
||||
self.rerandomize = only_one(dom2.xpath('/problem/@rerandomize'))
|
||||
if self.rerandomize == "" or self.rerandomize=="always" or self.rerandomize=="true":
|
||||
self.rerandomize="always"
|
||||
elif self.rerandomize=="false" or self.rerandomize=="per_student":
|
||||
self.rerandomize="per_student"
|
||||
elif self.rerandomize=="never":
|
||||
self.rerandomize="never"
|
||||
if self.rerandomize == "" or self.rerandomize == "always" or self.rerandomize == "true":
|
||||
self.rerandomize = "always"
|
||||
elif self.rerandomize == "false" or self.rerandomize == "per_student":
|
||||
self.rerandomize = "per_student"
|
||||
elif self.rerandomize == "never":
|
||||
self.rerandomize = "never"
|
||||
else:
|
||||
raise Exception("Invalid rerandomize attribute "+self.rerandomize)
|
||||
raise Exception("Invalid rerandomize attribute " + self.rerandomize)
|
||||
|
||||
if state!=None:
|
||||
state=json.loads(state)
|
||||
if state!=None and 'attempts' in state:
|
||||
self.attempts=state['attempts']
|
||||
if instance_state != None:
|
||||
instance_state = json.loads(instance_state)
|
||||
if instance_state != None and 'attempts' in instance_state:
|
||||
self.attempts = instance_state['attempts']
|
||||
|
||||
# TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename'))
|
||||
self.filename= "problems/"+only_one(dom2.xpath('/problem/@filename'))+".xml"
|
||||
@@ -267,7 +260,7 @@ class Module(XModule):
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system)
|
||||
self.lcp=LoncapaProblem(fp, self.item_id, instance_state, seed = seed, system=self.system)
|
||||
except Exception,err:
|
||||
msg = '[courseware.capa.capa_module.Module.init] error %s: cannot create LoncapaProblem %s' % (err,self.filename)
|
||||
log.exception(msg)
|
||||
@@ -277,7 +270,7 @@ class Module(XModule):
|
||||
# create a dummy problem with error message instead of failing
|
||||
fp = StringIO.StringIO('<problem><text><font color="red" size="+2">Problem file %s has an error:</font>%s</text></problem>' % (self.filename,msg))
|
||||
fp.name = "StringIO"
|
||||
self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system)
|
||||
self.lcp=LoncapaProblem(fp, self.item_id, instance_state, seed = seed, system=self.system)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
10
common/lib/xmodule/hidden_module.py
Normal file
10
common/lib/xmodule/hidden_module.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
|
||||
|
||||
class HiddenModule(XModule):
|
||||
pass
|
||||
|
||||
|
||||
class HiddenDescriptor(RawDescriptor):
|
||||
module_class = HiddenModule
|
||||
@@ -1,75 +1,34 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from x_module import XModule
|
||||
from mako_module import MakoModuleDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
class HtmlModuleDescriptor(MakoModuleDescriptor):
|
||||
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)
|
||||
self.html = self.definition['data']['text']
|
||||
|
||||
|
||||
class HtmlDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
"""
|
||||
Module for putting raw html in a course
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = HtmlModule
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]}
|
||||
js_module = 'HTML'
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
Creates an instance of this descriptor from the supplied xml_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
xml_data: A string of xml that will be translated into data and children for
|
||||
this module
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
"""
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
return cls(
|
||||
system,
|
||||
definition={'data': {'text': xml_data}},
|
||||
location=['i4x',
|
||||
org,
|
||||
course,
|
||||
xml_object.tag,
|
||||
xml_object.get('name')]
|
||||
)
|
||||
|
||||
class Module(XModule):
|
||||
id_attribute = 'filename'
|
||||
|
||||
def get_state(self):
|
||||
return json.dumps({ })
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ["html"]
|
||||
|
||||
def get_html(self):
|
||||
if self.filename==None:
|
||||
xmltree=etree.fromstring(self.xml)
|
||||
textlist=[xmltree.text]+[etree.tostring(i) for i in xmltree]+[xmltree.tail]
|
||||
textlist=[i for i in textlist if type(i)==str]
|
||||
return "".join(textlist)
|
||||
try:
|
||||
filename="html/"+self.filename
|
||||
return self.filestore.open(filename).read()
|
||||
except: # For backwards compatibility. TODO: Remove
|
||||
if self.DEBUG:
|
||||
log.info('[courseware.modules.html_module] filename=%s' % self.filename)
|
||||
return self.system.render_template(self.filename, {'id': self.item_id}, namespace='course')
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
xmltree=etree.fromstring(xml)
|
||||
self.filename = None
|
||||
filename_l=xmltree.xpath("/html/@filename")
|
||||
if len(filename_l)>0:
|
||||
self.filename=str(filename_l[0])
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return {'data': {'text': etree.tostring(xml_object)}}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from pkg_resources import resource_string
|
||||
from mako_module import MakoModuleDescriptor
|
||||
from lxml import etree
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
|
||||
class RawDescriptor(MakoModuleDescriptor):
|
||||
class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
"""
|
||||
Module that provides a raw editing view of it's data and children
|
||||
"""
|
||||
@@ -18,24 +19,5 @@ class RawDescriptor(MakoModuleDescriptor):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
Creates an instance of this descriptor from the supplied xml_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
xml_data: A string of xml that will be translated into data and children for
|
||||
this module
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
"""
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
return cls(
|
||||
system,
|
||||
definition={'data': xml_data},
|
||||
location=['i4x',
|
||||
org,
|
||||
course,
|
||||
xml_object.tag,
|
||||
xml_object.get('name')]
|
||||
)
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return {'data': etree.tostring(xml_object)}
|
||||
|
||||
@@ -6,18 +6,5 @@ class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
id_attribute = 'id'
|
||||
|
||||
def get_state(self):
|
||||
return json.dumps({ })
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ["schematic"]
|
||||
|
||||
def get_html(self):
|
||||
return '<input type="hidden" class="schematic" name="{item_id}" height="480" width="640">'.format(item_id=self.item_id)
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import logging
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from x_module import XModule
|
||||
from mako_module import MakoModuleDescriptor
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.progress import Progress
|
||||
|
||||
log = logging.getLogger("mitx.common.lib.seq_module")
|
||||
@@ -13,32 +14,17 @@ log = logging.getLogger("mitx.common.lib.seq_module")
|
||||
# OBSOLETE: This obsoletes 'type'
|
||||
class_priority = ['video', 'problem']
|
||||
|
||||
class Module(XModule):
|
||||
|
||||
class SequenceModule(XModule):
|
||||
''' Layout module which lays out content in a temporal sequence
|
||||
'''
|
||||
id_attribute = 'id'
|
||||
def get_instance_state(self):
|
||||
return json.dumps({'position': self.position})
|
||||
|
||||
def get_state(self):
|
||||
return json.dumps({ 'position':self.position })
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
obsolete_tags = ["sequential", 'tab']
|
||||
modern_tags = ["videosequence"]
|
||||
return obsolete_tags + modern_tags
|
||||
|
||||
def get_html(self):
|
||||
self.render()
|
||||
return self.content
|
||||
|
||||
def get_init_js(self):
|
||||
self.render()
|
||||
return self.init_js
|
||||
|
||||
def get_destroy_js(self):
|
||||
self.render()
|
||||
return self.destroy_js
|
||||
|
||||
def get_progress(self):
|
||||
''' Return the total progress, adding total done and total available.
|
||||
(assumes that each submodule uses the same "units" for progress.)
|
||||
@@ -60,53 +46,51 @@ class Module(XModule):
|
||||
if self.rendered:
|
||||
return
|
||||
## Returns a set of all types of all sub-children
|
||||
child_classes = [set([i.tag for i in e.iter()]) for e in self.xmltree]
|
||||
contents = []
|
||||
for child in self.get_display_items():
|
||||
progress = child.get_progress()
|
||||
contents.append({
|
||||
'content': child.get_html(),
|
||||
'title': "\n".join(
|
||||
grand_child.name.strip()
|
||||
for grand_child in child.get_children()
|
||||
if grand_child.name is not None
|
||||
),
|
||||
'progress_status': Progress.to_js_status_str(progress),
|
||||
'progress_detail': Progress.to_js_detail_str(progress),
|
||||
'type': child.get_icon_class(),
|
||||
})
|
||||
|
||||
titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") is not None]) \
|
||||
for e in self.xmltree]
|
||||
|
||||
children = self.get_children()
|
||||
progresses = [child.get_progress() for child in children]
|
||||
|
||||
self.contents = self.rendered_children()
|
||||
|
||||
for contents, title, progress in zip(self.contents, titles, progresses):
|
||||
contents['title'] = title
|
||||
contents['progress_status'] = Progress.to_js_status_str(progress)
|
||||
contents['progress_detail'] = Progress.to_js_detail_str(progress)
|
||||
|
||||
for (content, element_class) in zip(self.contents, child_classes):
|
||||
new_class = 'other'
|
||||
for c in class_priority:
|
||||
if c in element_class:
|
||||
new_class = c
|
||||
content['type'] = new_class
|
||||
print json.dumps(contents, indent=4)
|
||||
|
||||
# Split </script> tags -- browsers handle this as end
|
||||
# of script, even if it occurs mid-string. Do this after json.dumps()ing
|
||||
# so that we can be sure of the quotations being used
|
||||
params={'items': json.dumps(self.contents).replace('</script>', '<"+"/script>'),
|
||||
'id': self.item_id,
|
||||
'position': self.position,
|
||||
'titles': titles,
|
||||
'tag': self.xmltree.tag}
|
||||
params = {'items': json.dumps(contents).replace('</script>', '<"+"/script>'),
|
||||
'element_id': "-".join(str(v) for v in self.location.list()),
|
||||
'item_id': self.id,
|
||||
'position': self.position,
|
||||
'tag': self.location.category}
|
||||
|
||||
if self.xmltree.tag in ['sequential', 'videosequence']:
|
||||
self.content = self.system.render_template('seq_module.html', params)
|
||||
if self.xmltree.tag == 'tab':
|
||||
self.content = self.system.render_template('tab_module.html', params)
|
||||
log.debug("rendered content: %s", content)
|
||||
self.content = self.system.render_template('seq_module.html', params)
|
||||
self.rendered = True
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
self.xmltree = etree.fromstring(xml)
|
||||
def get_icon_class(self):
|
||||
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:
|
||||
new_class = c
|
||||
return new_class
|
||||
|
||||
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 state is not None:
|
||||
state = json.loads(state)
|
||||
if 'position' in state: self.position = int(state['position'])
|
||||
if instance_state is not None:
|
||||
state = json.loads(instance_state)
|
||||
if 'position' in state:
|
||||
self.position = int(state['position'])
|
||||
|
||||
# if position is specified in system, then use that instead
|
||||
if system.get('position'):
|
||||
@@ -115,23 +99,13 @@ class Module(XModule):
|
||||
self.rendered = False
|
||||
|
||||
|
||||
class SequenceDescriptor(MakoModuleDescriptor):
|
||||
class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
mako_template = 'widgets/sequence-edit.html'
|
||||
module_class = SequenceModule
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
|
||||
children = [
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return {'children': [
|
||||
system.process_xml(etree.tostring(child_module)).url
|
||||
for child_module in xml_object
|
||||
]
|
||||
|
||||
return cls(
|
||||
system, {'children': children},
|
||||
location=['i4x',
|
||||
org,
|
||||
course,
|
||||
xml_object.tag,
|
||||
xml_object.get('name')]
|
||||
)
|
||||
]}
|
||||
|
||||
@@ -13,14 +13,15 @@ setup(
|
||||
# for a description of entry_points
|
||||
entry_points={
|
||||
'xmodule.v1': [
|
||||
"chapter = seq_module:SequenceDescriptor",
|
||||
"course = seq_module:SequenceDescriptor",
|
||||
"html = html_module:HtmlModuleDescriptor",
|
||||
"section = translation_module:SemanticSectionDescriptor",
|
||||
"sequential = seq_module:SequenceDescriptor",
|
||||
"vertical = seq_module:SequenceDescriptor",
|
||||
"problemset = seq_module:SequenceDescriptor",
|
||||
"videosequence = seq_module:SequenceDescriptor",
|
||||
"chapter = xmodule.seq_module:SequenceDescriptor",
|
||||
"course = xmodule.seq_module:SequenceDescriptor",
|
||||
"html = xmodule.html_module:HtmlDescriptor",
|
||||
"section = xmodule.translation_module:SemanticSectionDescriptor",
|
||||
"sequential = xmodule.seq_module:SequenceDescriptor",
|
||||
"vertical = xmodule.vertical_module:VerticalDescriptor",
|
||||
"problemset = xmodule.seq_module:SequenceDescriptor",
|
||||
"videosequence = xmodule.seq_module:SequenceDescriptor",
|
||||
"video = xmodule.video_module:VideoDescriptor",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -31,18 +31,11 @@ class Module(XModule):
|
||||
Renders to::
|
||||
More information given in <a href="/book/234">the text</a>
|
||||
"""
|
||||
def get_state(self):
|
||||
return json.dumps({})
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ['customtag']
|
||||
|
||||
def get_html(self):
|
||||
return self.html
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
def __init__(self, system, xml, item_id, instance_state=None, shared_state=None):
|
||||
XModule.__init__(self, system, xml, item_id, instance_state, shared_state)
|
||||
xmltree = etree.fromstring(xml)
|
||||
filename = xmltree.find('impl').text
|
||||
params = dict(xmltree.items())
|
||||
|
||||
@@ -1,23 +1,10 @@
|
||||
import json
|
||||
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.seq_module import SequenceDescriptor
|
||||
from xmodule.progress import Progress
|
||||
from lxml import etree
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
class VerticalModule(XModule):
|
||||
''' Layout module for laying out submodules vertically.'''
|
||||
id_attribute = 'id'
|
||||
|
||||
def get_state(self):
|
||||
return json.dumps({ })
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ["vertical", "problemset"]
|
||||
|
||||
def get_html(self):
|
||||
return self.system.render_template('vert_module.html', {
|
||||
'items': self.contents
|
||||
@@ -30,8 +17,10 @@ class Module(XModule):
|
||||
progress = reduce(Progress.add_counts, progresses)
|
||||
return progress
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
xmltree=etree.fromstring(xml)
|
||||
self.contents=[(e.get("name"),self.render_function(e)) \
|
||||
for e in xmltree]
|
||||
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.contents = [child.get_html() for child in self.get_display_items()]
|
||||
|
||||
|
||||
class VerticalDescriptor(SequenceDescriptor):
|
||||
module_class = VerticalModule
|
||||
|
||||
@@ -3,16 +3,13 @@ import logging
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
from progress import Progress
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
|
||||
log = logging.getLogger("mitx.courseware.modules")
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
id_attribute = 'youtube'
|
||||
class VideoModule(XModule):
|
||||
video_time = 0
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
@@ -39,14 +36,9 @@ class Module(XModule):
|
||||
'''
|
||||
return None
|
||||
|
||||
def get_state(self):
|
||||
def get_instance_state(self):
|
||||
log.debug(u"STATE POSITION {0}".format(self.position))
|
||||
return json.dumps({ 'position': self.position })
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
'''Tags in the courseware file guaranteed to correspond to the module'''
|
||||
return ["video"]
|
||||
return json.dumps({'position': self.position})
|
||||
|
||||
def video_list(self):
|
||||
return self.youtube
|
||||
@@ -54,27 +46,27 @@ class Module(XModule):
|
||||
def get_html(self):
|
||||
return self.system.render_template('video.html', {
|
||||
'streams': self.video_list(),
|
||||
'id': self.item_id,
|
||||
'id': self.id,
|
||||
'position': self.position,
|
||||
'name': self.name,
|
||||
'annotations': self.annotations,
|
||||
})
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
xmltree = etree.fromstring(xml)
|
||||
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'])
|
||||
self.youtube = xmltree.get('youtube')
|
||||
self.name = xmltree.get('name')
|
||||
self.position = 0
|
||||
|
||||
if state is not None:
|
||||
state = json.loads(state)
|
||||
if instance_state is not None:
|
||||
state = json.loads(instance_state)
|
||||
if 'position' in state:
|
||||
self.position = int(float(state['position']))
|
||||
|
||||
self.annotations=[(e.get("name"),self.render_function(e)) \
|
||||
self.annotations = [(e.get("name"), self.render_function(e)) \
|
||||
for e in xmltree]
|
||||
|
||||
|
||||
class VideoSegmentDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
class VideoDescriptor(RawDescriptor):
|
||||
module_class = VideoModule
|
||||
|
||||
@@ -3,6 +3,7 @@ import pkg_resources
|
||||
import logging
|
||||
|
||||
from keystore import Location
|
||||
from functools import partial
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
@@ -56,85 +57,87 @@ class Plugin(object):
|
||||
|
||||
class XModule(object):
|
||||
''' Implements a generic learning module.
|
||||
Initialized on access with __init__, first time with state=None, and
|
||||
then with state
|
||||
Initialized on access with __init__, first time with instance_state=None, and
|
||||
shared_state=None. In later instantiations, instance_state will not be None,
|
||||
but shared_state may be
|
||||
|
||||
See the HTML module for a simple example
|
||||
'''
|
||||
id_attribute='id' # An attribute guaranteed to be unique
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
'''
|
||||
Construct a new xmodule
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
''' Tags in the courseware file guaranteed to correspond to the module '''
|
||||
return []
|
||||
|
||||
@classmethod
|
||||
def get_usage_tags(c):
|
||||
''' We should convert to a real module system
|
||||
For now, this tells us whether we use this as an xmodule, a CAPA response type
|
||||
or a CAPA input type '''
|
||||
return ['xmodule']
|
||||
system: An I4xSystem 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 a json object specifying the behavior of this xmodule
|
||||
'children': is a list of xmodule urls for child modules that this module depends on
|
||||
'''
|
||||
self.system = system
|
||||
self.location = Location(location)
|
||||
self.definition = definition
|
||||
self.instance_state = instance_state
|
||||
self.shared_state = shared_state
|
||||
self.id = self.location.url()
|
||||
self.name = self.location.name
|
||||
self.display_name = kwargs.get('display_name', '')
|
||||
self._loaded_children = None
|
||||
|
||||
def get_name(self):
|
||||
name = self.__xmltree.get('name')
|
||||
if name:
|
||||
if name:
|
||||
return name
|
||||
else:
|
||||
else:
|
||||
raise "We should iterate through children and find a default name"
|
||||
|
||||
def get_children(self):
|
||||
'''
|
||||
Return module instances for all the children of this module.
|
||||
'''
|
||||
children = [self.module_from_xml(e) for e in self.__xmltree]
|
||||
return children
|
||||
if self._loaded_children is None:
|
||||
self._loaded_children = [self.system.get_module(child) for child in self.definition.get('children', [])]
|
||||
return self._loaded_children
|
||||
|
||||
def rendered_children(self):
|
||||
def get_display_items(self):
|
||||
'''
|
||||
Render all children.
|
||||
This really ought to return a list of xmodules, instead of dictionaries
|
||||
Returns a list of descendent module instances that will display immediately
|
||||
inside this module
|
||||
'''
|
||||
children = [self.render_function(e) for e in self.__xmltree]
|
||||
return children
|
||||
items = []
|
||||
for child in self.get_children():
|
||||
items.extend(child.displayable_items())
|
||||
|
||||
def __init__(self, system = None, xml = None, item_id = None,
|
||||
json = None, track_url=None, state=None):
|
||||
''' In most cases, you must pass state or xml'''
|
||||
if not item_id:
|
||||
raise ValueError("Missing Index")
|
||||
if not xml and not json:
|
||||
raise ValueError("xml or json required")
|
||||
if not system:
|
||||
raise ValueError("System context required")
|
||||
return items
|
||||
|
||||
self.xml = xml
|
||||
self.json = json
|
||||
self.item_id = item_id
|
||||
self.state = state
|
||||
self.DEBUG = False
|
||||
|
||||
self.__xmltree = etree.fromstring(xml) # PRIVATE
|
||||
def displayable_items(self):
|
||||
'''
|
||||
Returns list of displayable modules contained by this module. If this module
|
||||
is visible, should return [self]
|
||||
'''
|
||||
return [self]
|
||||
|
||||
if system:
|
||||
## These are temporary; we really should go
|
||||
## through self.system.
|
||||
self.ajax_url = system.ajax_url
|
||||
self.tracker = system.track_function
|
||||
self.filestore = system.filestore
|
||||
self.render_function = system.render_function
|
||||
self.module_from_xml = system.module_from_xml
|
||||
self.DEBUG = system.DEBUG
|
||||
self.system = system
|
||||
def get_icon_class(self):
|
||||
'''
|
||||
Return a class identifying this module in the context of an icon
|
||||
'''
|
||||
return 'other'
|
||||
|
||||
### Functions used in the LMS
|
||||
|
||||
def get_state(self):
|
||||
''' State of the object, as stored in the database
|
||||
def get_instance_state(self):
|
||||
''' State of the object, as stored in the database
|
||||
'''
|
||||
return ""
|
||||
return '{}'
|
||||
|
||||
def get_shared_state(self):
|
||||
'''
|
||||
Get state that should be shared with other instances
|
||||
using the same 'shared_state_key' attribute.
|
||||
'''
|
||||
return '{}'
|
||||
|
||||
def get_score(self):
|
||||
''' Score the student received on the problem.
|
||||
''' Score the student received on the problem.
|
||||
'''
|
||||
return None
|
||||
|
||||
@@ -281,6 +284,7 @@ class XModuleDescriptor(Plugin):
|
||||
self.name = Location(kwargs.get('location')).name
|
||||
self.type = Location(kwargs.get('location')).category
|
||||
self.url = Location(kwargs.get('location')).url()
|
||||
self.display_name = kwargs.get('display_name')
|
||||
|
||||
# For now, we represent goals as a list of strings, but this
|
||||
# is one of the things that we are going to be iterating on heavily
|
||||
@@ -302,33 +306,13 @@ class XModuleDescriptor(Plugin):
|
||||
"""
|
||||
raise NotImplementedError("get_html() must be provided by specific modules")
|
||||
|
||||
def get_xml(self):
|
||||
''' For conversions between JSON and legacy XML representations.
|
||||
'''
|
||||
if self.xml:
|
||||
return self.xml
|
||||
else:
|
||||
raise NotImplementedError("JSON->XML Translation not implemented")
|
||||
|
||||
def get_json(self):
|
||||
''' For conversions between JSON and legacy XML representations.
|
||||
'''
|
||||
if self.json:
|
||||
raise NotImplementedError
|
||||
return self.json # TODO: Return context as well -- files, etc.
|
||||
else:
|
||||
raise NotImplementedError("XML->JSON Translation not implemented")
|
||||
|
||||
#def handle_cms_json(self):
|
||||
# raise NotImplementedError
|
||||
|
||||
#def render(self, size):
|
||||
# ''' Size: [thumbnail, small, full]
|
||||
# Small ==> what we drag around
|
||||
# Full ==> what we edit
|
||||
# '''
|
||||
# raise NotImplementedError
|
||||
|
||||
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
|
||||
"""
|
||||
return partial(self.module_class, system, self.url, self.definition,
|
||||
display_name=self.display_name)
|
||||
|
||||
class DescriptorSystem(object):
|
||||
def __init__(self, load_item, resources_fs):
|
||||
|
||||
41
common/lib/xmodule/xml_module.py
Normal file
41
common/lib/xmodule/xml_module.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from lxml import etree
|
||||
|
||||
|
||||
class XmlDescriptor(XModuleDescriptor):
|
||||
"""
|
||||
Mixin class for standardized parsing of from xml
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
"""
|
||||
Return the definition to be passed to the newly created descriptor
|
||||
during from_xml
|
||||
"""
|
||||
raise NotImplementedError("%s does not implement definition_from_xml" % cls.__class__.__name__)
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
Creates an instance of this descriptor from the supplied xml_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
xml_data: A string of xml that will be translated into data and children for
|
||||
this module
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
"""
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
|
||||
return cls(
|
||||
system,
|
||||
cls.definition_from_xml(xml_object, system),
|
||||
location=['i4x',
|
||||
org,
|
||||
course,
|
||||
xml_object.tag,
|
||||
xml_object.get('slug')],
|
||||
display_name=xml_object.get('name')
|
||||
)
|
||||
@@ -19,10 +19,12 @@ from django.conf import settings
|
||||
|
||||
from student.models import UserProfile
|
||||
from student.models import UserTestGroup
|
||||
from courseware.models import StudentModuleCache
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from util.cache import cache
|
||||
from multicourse import multicourse_settings
|
||||
import xmodule
|
||||
from keystore.django import keystore
|
||||
|
||||
''' This file will eventually form an abstraction layer between the
|
||||
course XML file and the rest of the system.
|
||||
@@ -103,6 +105,7 @@ def course_xml_process(tree):
|
||||
items without. Propagate due dates, grace periods, etc. to child
|
||||
items.
|
||||
'''
|
||||
process_includes(tree)
|
||||
replace_custom_tags(tree)
|
||||
id_tag(tree)
|
||||
propogate_downward_tag(tree, "due")
|
||||
@@ -113,45 +116,32 @@ def course_xml_process(tree):
|
||||
return tree
|
||||
|
||||
|
||||
def toc_from_xml(dom, active_chapter, active_section):
|
||||
'''
|
||||
Create a table of contents from the course xml.
|
||||
|
||||
Return format:
|
||||
[ {'name': name, 'sections': SECTIONS, 'active': bool}, ... ]
|
||||
|
||||
where SECTIONS is a list
|
||||
[ {'name': name, 'format': format, 'due': due, 'active' : bool}, ...]
|
||||
|
||||
active is set for the section and chapter corresponding to the passed
|
||||
parameters. Everything else comes from the xml, or defaults to "".
|
||||
|
||||
chapters with name 'hidden' are skipped.
|
||||
'''
|
||||
name = dom.xpath('//course/@name')[0]
|
||||
|
||||
chapters = dom.xpath('//course[@name=$name]/chapter', name=name)
|
||||
ch = list()
|
||||
for c in chapters:
|
||||
if c.get('name') == 'hidden':
|
||||
continue
|
||||
sections = list()
|
||||
for s in dom.xpath('//course[@name=$name]/chapter[@name=$chname]/section',
|
||||
name=name, chname=c.get('name')):
|
||||
|
||||
format = s.get("subtitle") if s.get("subtitle") else s.get("format") or ""
|
||||
active = (c.get("name") == active_chapter and
|
||||
s.get("name") == active_section)
|
||||
|
||||
sections.append({'name': s.get("name") or "",
|
||||
'format': format,
|
||||
'due': s.get("due") or "",
|
||||
'active': active})
|
||||
|
||||
ch.append({'name': c.get("name"),
|
||||
'sections': sections,
|
||||
'active': c.get("name") == active_chapter})
|
||||
return ch
|
||||
def process_includes_dir(tree, dir):
|
||||
"""
|
||||
Process tree to replace all <include file=""> tags
|
||||
with the contents of the file specified, relative to dir
|
||||
"""
|
||||
includes = tree.findall('.//include')
|
||||
for inc in includes:
|
||||
file = inc.get('file')
|
||||
if file is not None:
|
||||
try:
|
||||
ifp = open(os.path.join(dir, file))
|
||||
except Exception:
|
||||
log.exception('Error in problem xml include: %s' % (etree.tostring(inc, 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(inc, pretty_print=True)))
|
||||
log.exception('Cannot parse XML in %s' % (file))
|
||||
raise
|
||||
# insert new XML into tree in place of inlcude
|
||||
parent = inc.getparent()
|
||||
parent.insert(parent.index(inc), incxml)
|
||||
parent.remove(inc)
|
||||
|
||||
|
||||
def replace_custom_tags_dir(tree, dir):
|
||||
@@ -181,78 +171,6 @@ def parse_course_file(filename, options, namespace):
|
||||
return course_xml_process(xml)
|
||||
|
||||
|
||||
def get_section(section, options, dirname):
|
||||
'''
|
||||
Given the name of a section, an options dict containing keys
|
||||
'dev_content' and 'groups', and a directory to look in,
|
||||
returns the xml tree for the section, or None if there's no
|
||||
such section.
|
||||
'''
|
||||
filename = section + ".xml"
|
||||
|
||||
if filename not in os.listdir(dirname):
|
||||
log.error(filename + " not in " + str(os.listdir(dirname)))
|
||||
return None
|
||||
|
||||
tree = parse_course_file(filename, options, namespace='sections')
|
||||
return tree
|
||||
|
||||
|
||||
def get_module(tree, module, id_tag, module_id, sections_dirname, options):
|
||||
'''
|
||||
Given the xml tree of the course, get the xml string for a module
|
||||
with the specified module type, id_tag, module_id. Looks in
|
||||
sections_dirname for sections.
|
||||
|
||||
id_tag -- use id_tag if the place the module stores its id is not 'id'
|
||||
'''
|
||||
# Sanitize input
|
||||
if not module.isalnum():
|
||||
raise Exception("Module is not alphanumeric")
|
||||
|
||||
if not module_id.isalnum():
|
||||
raise Exception("Module ID is not alphanumeric")
|
||||
|
||||
# Generate search
|
||||
xpath_search='//{module}[(@{id_tag} = "{id}") or (@id = "{id}")]'.format(
|
||||
module=module,
|
||||
id_tag=id_tag,
|
||||
id=module_id)
|
||||
|
||||
|
||||
result_set = tree.xpath(xpath_search)
|
||||
if len(result_set) < 1:
|
||||
# Not found in main tree. Let's look in the section files.
|
||||
section_list = (s[:-4] for s in os.listdir(sections_dirname) if s.endswith('.xml'))
|
||||
for section in section_list:
|
||||
try:
|
||||
s = get_section(section, options, sections_dirname)
|
||||
except etree.XMLSyntaxError:
|
||||
ex = sys.exc_info()
|
||||
raise ContentException("Malformed XML in " + section +
|
||||
"(" + str(ex[1].msg) + ")")
|
||||
result_set = s.xpath(xpath_search)
|
||||
if len(result_set) != 0:
|
||||
break
|
||||
|
||||
if len(result_set) > 1:
|
||||
log.error("WARNING: Potentially malformed course file", module, module_id)
|
||||
|
||||
if len(result_set)==0:
|
||||
log.error('[content_parser.get_module] cannot find %s in course.xml tree',
|
||||
xpath_search)
|
||||
log.error('tree = %s' % etree.tostring(tree, pretty_print=True))
|
||||
return None
|
||||
|
||||
# log.debug('[courseware.content_parser.module_xml] found %s' % result_set)
|
||||
|
||||
return etree.tostring(result_set[0])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# ==== All Django-specific code below =============================================
|
||||
|
||||
def user_groups(user):
|
||||
@@ -278,6 +196,11 @@ def get_options(user):
|
||||
'groups': user_groups(user)}
|
||||
|
||||
|
||||
def process_includes(tree):
|
||||
'''Replace <include> tags with the contents from the course directory'''
|
||||
process_includes_dir(tree, settings.DATA_DIR)
|
||||
|
||||
|
||||
def replace_custom_tags(tree):
|
||||
'''Replace custom tags defined in our custom_tags dir'''
|
||||
replace_custom_tags_dir(tree, settings.DATA_DIR+'/custom_tags')
|
||||
@@ -337,29 +260,3 @@ def sections_dir(coursename=None):
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename)
|
||||
|
||||
return settings.DATA_DIR + xp + '/sections/'
|
||||
|
||||
|
||||
|
||||
def section_file(user, section, coursename=None):
|
||||
'''
|
||||
Given a user and the name of a section, return that section.
|
||||
This is done specific to each course.
|
||||
|
||||
Returns the xml tree for the section, or None if there's no such section.
|
||||
'''
|
||||
dirname = sections_dir(coursename)
|
||||
|
||||
|
||||
return get_section(section, options, dirname)
|
||||
|
||||
|
||||
def module_xml(user, module, id_tag, module_id, coursename=None):
|
||||
''' Get XML for a module based on module and module_id. Assumes
|
||||
module occurs once in courseware XML file or hidden section.
|
||||
'''
|
||||
tree = course_file(user, coursename)
|
||||
sdirname = sections_dir(coursename)
|
||||
options = get_options(user)
|
||||
|
||||
return get_module(tree, module, id_tag, module_id, sdirname, options)
|
||||
|
||||
|
||||
@@ -81,12 +81,12 @@ def grade_sheet(student,coursename=None):
|
||||
course = dom.xpath('//course/@name')[0]
|
||||
xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course)
|
||||
|
||||
responses=StudentModule.objects.filter(student=student)
|
||||
responses = StudentModule.objects.filter(student=student)
|
||||
response_by_id = {}
|
||||
for response in responses:
|
||||
response_by_id[response.module_id] = response
|
||||
|
||||
|
||||
response_by_id[response.module_state_key] = response
|
||||
|
||||
|
||||
totaled_scores = {}
|
||||
chapters=[]
|
||||
for c in xmlChapters:
|
||||
@@ -147,27 +147,39 @@ def grade_sheet(student,coursename=None):
|
||||
'grade_summary' : grade_summary}
|
||||
|
||||
def get_score(user, problem, cache, coursename=None):
|
||||
"""
|
||||
Return the score for a user on a problem
|
||||
|
||||
user: a Student object
|
||||
problem: the xml for the problem
|
||||
cache: a dictionary mapping module_state_key tuples to instantiated StudentModules
|
||||
module_state_key is either the problem_id, or a key used by the problem
|
||||
to share state across instances
|
||||
"""
|
||||
## HACK: assumes max score is fixed per problem
|
||||
id = problem.get('id')
|
||||
module_type = problem.tag
|
||||
module_class = xmodule.get_module_class(module_type)
|
||||
module_id = problem.get('id')
|
||||
module_state_key = problem.get(module_class.state_key, module_id)
|
||||
correct = 0.0
|
||||
|
||||
|
||||
# If the ID is not in the cache, add the item
|
||||
if id not in cache:
|
||||
module = StudentModule(module_type = 'problem', # TODO: Move into StudentModule.__init__?
|
||||
module_id = id,
|
||||
student = user,
|
||||
state = None,
|
||||
grade = 0,
|
||||
max_grade = None,
|
||||
done = 'i')
|
||||
cache[id] = module
|
||||
if module_state_key not in cache:
|
||||
module = StudentModule(module_type='problem', # TODO: Move into StudentModule.__init__?
|
||||
module_state_key=id,
|
||||
student=user,
|
||||
state=None,
|
||||
grade=0,
|
||||
max_grade=None,
|
||||
done='i')
|
||||
cache[module_id] = module
|
||||
|
||||
# Grab the # correct from cache
|
||||
if id in cache:
|
||||
response = cache[id]
|
||||
if response.grade!=None:
|
||||
correct=float(response.grade)
|
||||
|
||||
if response.grade != None:
|
||||
correct = float(response.grade)
|
||||
|
||||
# Grab max grade from cache, or if it doesn't exist, compute and save to DB
|
||||
if id in cache and response.max_grade is not None:
|
||||
total = response.max_grade
|
||||
|
||||
@@ -13,7 +13,6 @@ ASSUMPTIONS: modules have unique IDs, even across different module_types
|
||||
|
||||
"""
|
||||
from django.db import models
|
||||
from django.db.models.signals import post_save, post_delete
|
||||
#from django.core.cache import cache
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
@@ -21,72 +20,97 @@ from django.contrib.auth.models import User
|
||||
|
||||
#CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours
|
||||
|
||||
|
||||
class StudentModule(models.Model):
|
||||
# For a homework problem, contains a JSON
|
||||
# object consisting of state
|
||||
MODULE_TYPES = (('problem','problem'),
|
||||
('video','video'),
|
||||
('html','html'),
|
||||
MODULE_TYPES = (('problem', 'problem'),
|
||||
('video', 'video'),
|
||||
('html', 'html'),
|
||||
)
|
||||
## These three are the key for the object
|
||||
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True)
|
||||
module_id = models.CharField(max_length=255, db_index=True) # Filename for homeworks, etc.
|
||||
|
||||
# Key used to share state. By default, this is the module_id,
|
||||
# but for abtests and the like, this can be set to a shared value
|
||||
# for many instances of the module.
|
||||
# Filename for homeworks, etc.
|
||||
module_state_key = models.CharField(max_length=255, db_index=True, db_column='module_id')
|
||||
student = models.ForeignKey(User, db_index=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = (('student', 'module_id'),)
|
||||
unique_together = (('student', 'module_state_key'),)
|
||||
|
||||
## Internal state of the object
|
||||
state = models.TextField(null=True, blank=True)
|
||||
|
||||
## Grade, and are we done?
|
||||
## Grade, and are we done?
|
||||
grade = models.FloatField(null=True, blank=True, db_index=True)
|
||||
max_grade = models.FloatField(null=True, blank=True)
|
||||
DONE_TYPES = (('na','NOT_APPLICABLE'),
|
||||
('f','FINISHED'),
|
||||
('i','INCOMPLETE'),
|
||||
DONE_TYPES = (('na', 'NOT_APPLICABLE'),
|
||||
('f', 'FINISHED'),
|
||||
('i', 'INCOMPLETE'),
|
||||
)
|
||||
done = models.CharField(max_length=8, choices=DONE_TYPES, default='na', db_index=True)
|
||||
|
||||
# DONE_TYPES = (('done','DONE'), # Finished
|
||||
# ('incomplete','NOTDONE'), # Not finished
|
||||
# ('na','NA')) # Not applicable (e.g. vertical)
|
||||
# done = models.CharField(max_length=16, choices=DONE_TYPES)
|
||||
|
||||
created = models.DateTimeField(auto_now_add=True, db_index=True)
|
||||
modified = models.DateTimeField(auto_now=True, db_index=True)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.module_type+'/'+self.student.username+"/"+self.module_id+'/'+str(self.state)[:20]
|
||||
|
||||
# @classmethod
|
||||
# def get_with_caching(cls, student, module_id):
|
||||
# k = cls.key_for(student, module_id)
|
||||
# student_module = cache.get(k)
|
||||
# if student_module is None:
|
||||
# student_module = StudentModule.objects.filter(student=student,
|
||||
# module_id=module_id)[0]
|
||||
# # It's possible it really doesn't exist...
|
||||
# if student_module is not None:
|
||||
# cache.set(k, student_module, CACHE_TIMEOUT)
|
||||
|
||||
# return student_module
|
||||
|
||||
@classmethod
|
||||
def key_for(cls, student, module_id):
|
||||
return "StudentModule-student_id:{0};module_id:{1}".format(student.id, module_id)
|
||||
return '/'.join([self.module_type, self.student.username, self.module_state_key, str(self.state)[:20]])
|
||||
|
||||
|
||||
# def clear_cache_by_student_and_module_id(sender, instance, *args, **kwargs):
|
||||
# k = sender.key_for(instance.student, instance.module_id)
|
||||
# cache.delete(k)
|
||||
|
||||
# def update_cache_by_student_and_module_id(sender, instance, *args, **kwargs):
|
||||
# k = sender.key_for(instance.student, instance.module_id)
|
||||
# cache.set(k, instance, CACHE_TIMEOUT)
|
||||
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
|
||||
|
||||
|
||||
#post_save.connect(update_cache_by_student_and_module_id, sender=StudentModule, weak=False)
|
||||
#post_delete.connect(clear_cache_by_student_and_module_id, sender=StudentModule, weak=False)
|
||||
|
||||
#cache_model(StudentModule)
|
||||
class StudentModuleCache(object):
|
||||
"""
|
||||
A cache of StudentModules for a specific student
|
||||
"""
|
||||
def __init__(self, user, descriptor, depth=None):
|
||||
'''
|
||||
Find any StudentModule objects that are needed by any child modules of the
|
||||
supplied descriptor. Avoids making multiple queries to the database
|
||||
'''
|
||||
if user.is_authenticated():
|
||||
module_ids = self._get_module_state_keys(descriptor, depth)
|
||||
self.cache = list(StudentModule.objects.filter(student=user,
|
||||
module_state_key__in=module_ids))
|
||||
else:
|
||||
self.cache = []
|
||||
|
||||
def _get_module_state_keys(self, descriptor, depth):
|
||||
'''
|
||||
Get a list of the state_keys needed for StudentModules
|
||||
required for this chunk of module xml
|
||||
'''
|
||||
keys = [descriptor.url]
|
||||
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
keys.append(shared_state_key)
|
||||
|
||||
if depth is None or depth > 0:
|
||||
new_depth = depth - 1 if depth is not None else depth
|
||||
|
||||
for child in descriptor.get_children():
|
||||
keys.extend(self._get_module_state_keys(child, new_depth))
|
||||
|
||||
return keys
|
||||
|
||||
def lookup(self, module_type, module_state_key):
|
||||
'''
|
||||
Look for a student module with the given type and id in the cache.
|
||||
|
||||
cache -- list of student modules
|
||||
|
||||
returns first found object, or None
|
||||
'''
|
||||
for o in self.cache:
|
||||
if o.module_type == module_type and o.module_state_key == module_state_key:
|
||||
return o
|
||||
return None
|
||||
|
||||
def append(self, student_module):
|
||||
self.cache.append(student_module)
|
||||
|
||||
@@ -12,19 +12,21 @@ from fs.osfs import OSFS
|
||||
from django.conf import settings
|
||||
from mitxmako.shortcuts import render_to_string, render_to_response
|
||||
|
||||
from models import StudentModule
|
||||
from models import StudentModule, StudentModuleCache
|
||||
from multicourse import multicourse_settings
|
||||
from util.views import accepts
|
||||
|
||||
import courseware.content_parser as content_parser
|
||||
import xmodule
|
||||
from keystore.django import keystore
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
class I4xSystem(object):
|
||||
'''
|
||||
This is an abstraction such that x_modules can function independent
|
||||
of the courseware (e.g. import into other types of courseware, LMS,
|
||||
This is an abstraction such that x_modules can function independent
|
||||
of the courseware (e.g. import into other types of courseware, LMS,
|
||||
or if we want to have a sandbox server for user-contributed content)
|
||||
|
||||
I4xSystem objects are passed to x_modules to provide access to system
|
||||
@@ -34,7 +36,7 @@ class I4xSystem(object):
|
||||
and user, or other environment-specific info.
|
||||
'''
|
||||
def __init__(self, ajax_url, track_function, render_function,
|
||||
module_from_xml, render_template, request=None,
|
||||
get_module, render_template, request=None,
|
||||
filestore=None):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
@@ -44,7 +46,7 @@ class I4xSystem(object):
|
||||
or otherwise tracking the event.
|
||||
TODO: Not used, and has inconsistent args in different
|
||||
files. Update or remove.
|
||||
module_from_xml - function that takes (module_xml) and returns a corresponding
|
||||
get_module - function that takes (location) and returns a corresponding
|
||||
module instance object.
|
||||
render_function - function that takes (module_xml) and renders it,
|
||||
returning a dictionary with a context for rendering the
|
||||
@@ -58,14 +60,14 @@ class I4xSystem(object):
|
||||
'''
|
||||
self.ajax_url = ajax_url
|
||||
self.track_function = track_function
|
||||
if not filestore:
|
||||
if not filestore:
|
||||
self.filestore = OSFS(settings.DATA_DIR)
|
||||
else:
|
||||
self.filestore = filestore
|
||||
if settings.DEBUG:
|
||||
log.info("[courseware.module_render.I4xSystem] filestore path = %s",
|
||||
filestore)
|
||||
self.module_from_xml = module_from_xml
|
||||
self.get_module = get_module
|
||||
self.render_function = render_function
|
||||
self.render_template = render_template
|
||||
self.exception404 = Http404
|
||||
@@ -75,8 +77,8 @@ class I4xSystem(object):
|
||||
def get(self, attr):
|
||||
''' provide uniform access to attributes (like etree).'''
|
||||
return self.__dict__.get(attr)
|
||||
|
||||
def set(self,attr,val):
|
||||
|
||||
def set(self, attr, val):
|
||||
'''provide uniform access to attributes (like etree)'''
|
||||
self.__dict__[attr] = val
|
||||
|
||||
@@ -86,21 +88,11 @@ class I4xSystem(object):
|
||||
def __str__(self):
|
||||
return str(self.__dict__)
|
||||
|
||||
def smod_cache_lookup(cache, module_type, module_id):
|
||||
'''
|
||||
Look for a student module with the given type and id in the cache.
|
||||
|
||||
cache -- list of student modules
|
||||
|
||||
returns first found object, or None
|
||||
'''
|
||||
for o in cache:
|
||||
if o.module_type == module_type and o.module_id == module_id:
|
||||
return o
|
||||
return None
|
||||
|
||||
def make_track_function(request):
|
||||
'''
|
||||
'''
|
||||
Make a tracking function that logs what happened.
|
||||
For use in I4xSystem.
|
||||
'''
|
||||
@@ -110,8 +102,9 @@ def make_track_function(request):
|
||||
return track.views.server_track(request, event_type, event, page='x_module')
|
||||
return f
|
||||
|
||||
|
||||
def grade_histogram(module_id):
|
||||
''' Print out a histogram of grades on a given problem.
|
||||
''' Print out a histogram of grades on a given problem.
|
||||
Part of staff member debug info.
|
||||
'''
|
||||
from django.db import connection
|
||||
@@ -137,13 +130,87 @@ def make_module_from_xml_fn(user, request, student_module_cache, position):
|
||||
def module_from_xml(xml):
|
||||
'''Modules need a way to convert xml to instance objects.
|
||||
Pass the rest of the context through.'''
|
||||
(instance, sm, module_type) = get_module(
|
||||
(instance, _, _, _) = get_module(
|
||||
user, request, xml, student_module_cache, position)
|
||||
return instance
|
||||
return module_from_xml
|
||||
|
||||
|
||||
def get_module(user, request, module_xml, student_module_cache, position=None):
|
||||
def toc_for_course(user, request, course_location, active_chapter, active_section):
|
||||
'''
|
||||
Create a table of contents from the module store
|
||||
|
||||
Return format:
|
||||
[ {'name': name, 'sections': SECTIONS, 'active': bool}, ... ]
|
||||
|
||||
where SECTIONS is a list
|
||||
[ {'name': name, 'format': format, 'due': due, 'active' : bool}, ...]
|
||||
|
||||
active is set for the section and chapter corresponding to the passed
|
||||
parameters. Everything else comes from the xml, or defaults to "".
|
||||
|
||||
chapters with name 'hidden' are skipped.
|
||||
'''
|
||||
|
||||
student_module_cache = StudentModuleCache(user, keystore().get_item(course_location), depth=2)
|
||||
(course, _, _, _) = get_module(user, request, course_location, student_module_cache)
|
||||
|
||||
chapters = list()
|
||||
for chapter in course.get_display_items():
|
||||
sections = list()
|
||||
for section in chapter.get_display_items():
|
||||
|
||||
active = (chapter.display_name == active_chapter and
|
||||
section.display_name == active_section)
|
||||
|
||||
sections.append({'name': section.display_name,
|
||||
'format': getattr(section, 'format', ''),
|
||||
'due': getattr(section, 'due', ''),
|
||||
'active': active})
|
||||
|
||||
chapters.append({'name': chapter.display_name,
|
||||
'sections': sections,
|
||||
'active': chapter.display_name == active_chapter})
|
||||
return chapters
|
||||
|
||||
|
||||
def get_section(course, chapter, section):
|
||||
"""
|
||||
Returns the xmodule descriptor for the name course > chapter > section,
|
||||
or None if this doesn't specify a valid section
|
||||
|
||||
course: Course url
|
||||
chapter: Chapter name
|
||||
section: Section name
|
||||
"""
|
||||
try:
|
||||
course_module = keystore().get_item(course)
|
||||
except:
|
||||
log.exception("Unable to load course_module")
|
||||
return None
|
||||
|
||||
if course_module is None:
|
||||
return
|
||||
|
||||
chapter_module = None
|
||||
for _chapter in course_module.get_children():
|
||||
if _chapter.display_name == chapter:
|
||||
chapter_module = _chapter
|
||||
break
|
||||
|
||||
if chapter_module is None:
|
||||
return
|
||||
|
||||
section_module = None
|
||||
for _section in chapter_module.get_children():
|
||||
if _section.display_name == section:
|
||||
section_module = _section
|
||||
break
|
||||
|
||||
return section_module
|
||||
|
||||
|
||||
def get_module(user, request, location, student_module_cache, position=None):
|
||||
''' Get an instance of the xmodule class corresponding to module_xml,
|
||||
setting the state based on an existing StudentModule, or creating one if none
|
||||
exists.
|
||||
@@ -152,65 +219,73 @@ def get_module(user, request, module_xml, student_module_cache, position=None):
|
||||
- user : current django User
|
||||
- request : current django HTTPrequest
|
||||
- module_xml : lxml etree of xml subtree for the requested module
|
||||
- student_module_cache : list of StudentModule objects, one of which may
|
||||
match this module type and id
|
||||
- position : extra information from URL for user-specified
|
||||
- student_module_cache : a StudentModuleCache
|
||||
- position : extra information from URL for user-specified
|
||||
position within module
|
||||
|
||||
Returns:
|
||||
- a tuple (xmodule instance, student module, module type).
|
||||
- a tuple (xmodule instance, instance_module, shared_module, module type).
|
||||
instance_module is a StudentModule specific to this module for this student
|
||||
shared_module is a StudentModule specific to all modules with the same 'shared_state_key' attribute, or None if the module doesn't elect to share state
|
||||
'''
|
||||
module_type = module_xml.tag
|
||||
module_class = xmodule.get_module_class(module_type)
|
||||
module_id = module_xml.get('id')
|
||||
descriptor = keystore().get_item(location)
|
||||
|
||||
# Grab xmodule state from StudentModule cache
|
||||
smod = smod_cache_lookup(student_module_cache, module_type, module_id)
|
||||
state = smod.state if smod else None
|
||||
|
||||
# get coursename if present in request
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
|
||||
if coursename and settings.ENABLE_MULTICOURSE:
|
||||
# path to XML for the course
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename)
|
||||
data_root = settings.DATA_DIR + xp
|
||||
instance_module = student_module_cache.lookup(descriptor.type, descriptor.url)
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
shared_module = student_module_cache.lookup(descriptor.type, shared_state_key)
|
||||
else:
|
||||
data_root = settings.DATA_DIR
|
||||
shared_module = None
|
||||
|
||||
instance_state = instance_module.state if instance_module is not None else None
|
||||
shared_state = shared_module.state if shared_module is not None else None
|
||||
|
||||
# Setup system context for module instance
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/' + module_type + '/' + module_id + '/'
|
||||
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.type + '/' + descriptor.url + '/'
|
||||
|
||||
module_from_xml = make_module_from_xml_fn(
|
||||
user, request, student_module_cache, position)
|
||||
|
||||
system = I4xSystem(track_function = make_track_function(request),
|
||||
render_function = lambda xml: render_x_module(
|
||||
def _get_module(location):
|
||||
(module, _, _, _) = get_module(user, request, location, student_module_cache, position)
|
||||
return module
|
||||
|
||||
system = I4xSystem(track_function=make_track_function(request),
|
||||
render_function=lambda xml: render_x_module(
|
||||
user, request, xml, student_module_cache, position),
|
||||
render_template = render_to_string,
|
||||
ajax_url = ajax_url,
|
||||
request = request,
|
||||
filestore = OSFS(data_root),
|
||||
module_from_xml = module_from_xml,
|
||||
render_template=render_to_string,
|
||||
ajax_url=ajax_url,
|
||||
request=request,
|
||||
# TODO (cpennington): Figure out how to share info between systems
|
||||
filestore=descriptor.system.resources_fs,
|
||||
get_module=_get_module,
|
||||
)
|
||||
# pass position specified in URL to module through I4xSystem
|
||||
system.set('position', position)
|
||||
instance = module_class(system,
|
||||
etree.tostring(module_xml),
|
||||
module_id,
|
||||
state=state)
|
||||
system.set('position', position)
|
||||
|
||||
module = descriptor.xmodule_constructor(system)(instance_state, shared_state)
|
||||
|
||||
# If StudentModule for this instance wasn't already in the database,
|
||||
# and this isn't a guest user, create it.
|
||||
if not smod and user.is_authenticated():
|
||||
smod = StudentModule(student=user, module_type = module_type,
|
||||
module_id=module_id, state=instance.get_state())
|
||||
smod.save()
|
||||
# Add to cache. The caller and the system context have references
|
||||
# to it, so the change persists past the return
|
||||
student_module_cache.append(smod)
|
||||
if user.is_authenticated():
|
||||
if not instance_module:
|
||||
instance_module = StudentModule(
|
||||
student=user,
|
||||
module_type=descriptor.type,
|
||||
module_state_key=module.id,
|
||||
state=module.get_instance_state())
|
||||
instance_module.save()
|
||||
# Add to cache. The caller and the system context have references
|
||||
# to it, so the change persists past the return
|
||||
student_module_cache.append(instance_module)
|
||||
if not shared_module and shared_state_key is not None:
|
||||
shared_module = StudentModule(
|
||||
student=user,
|
||||
module_type=descriptor.type,
|
||||
module_state_key=shared_state_key,
|
||||
state=module.get_shared_state())
|
||||
shared_module.save()
|
||||
student_module_cache.append(shared_module)
|
||||
|
||||
return (module, instance_module, shared_module, descriptor.type)
|
||||
|
||||
return (instance, smod, module_type)
|
||||
|
||||
def render_x_module(user, request, module_xml, student_module_cache, position=None):
|
||||
''' Generic module for extensions. This renders to HTML.
|
||||
@@ -232,20 +307,20 @@ def render_x_module(user, request, module_xml, student_module_cache, position=No
|
||||
- dict which is context for HTML rendering of the specified module. Will have
|
||||
key 'content', and will have 'type' key if passed a valid module.
|
||||
'''
|
||||
if module_xml is None :
|
||||
if module_xml is None:
|
||||
return {"content": ""}
|
||||
|
||||
(instance, smod, module_type) = get_module(
|
||||
(instance, _, _, module_type) = get_module(
|
||||
user, request, module_xml, student_module_cache, position)
|
||||
|
||||
content = instance.get_html()
|
||||
|
||||
# special extra information about each problem, only for users who are staff
|
||||
# special extra information about each problem, only for users who are staff
|
||||
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
|
||||
module_id = module_xml.get('id')
|
||||
histogram = grade_histogram(module_id)
|
||||
render_histogram = len(histogram) > 0
|
||||
staff_context = {'xml': etree.tostring(module_xml),
|
||||
staff_context = {'xml': etree.tostring(module_xml),
|
||||
'module_id': module_id,
|
||||
'histogram': json.dumps(histogram),
|
||||
'render_histogram': render_histogram}
|
||||
@@ -254,6 +329,7 @@ def render_x_module(user, request, module_xml, student_module_cache, position=No
|
||||
context = {'content': content, 'type': module_type}
|
||||
return context
|
||||
|
||||
|
||||
def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
''' Generic view for extensions. This is where AJAX calls go.
|
||||
|
||||
@@ -276,24 +352,10 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
error_msg = ("We're sorry, this module is temporarily unavailable. "
|
||||
"Our staff is working to fix it as soon as possible")
|
||||
|
||||
|
||||
# Grab the student information for the module from the database
|
||||
s = StudentModule.objects.filter(student=request.user,
|
||||
module_id=id)
|
||||
|
||||
if s is None or len(s) == 0:
|
||||
log.debug("Couldn't find module '%s' for user '%s' and id '%s'",
|
||||
module, request.user, id)
|
||||
raise Http404
|
||||
s = s[0]
|
||||
|
||||
oldgrade = s.grade
|
||||
oldstate = s.state
|
||||
|
||||
# If there are arguments, get rid of them
|
||||
dispatch, _, _ = dispatch.partition('?')
|
||||
|
||||
ajax_url = '{root}/modx/{module}/{id}'.format(root = settings.MITX_ROOT_URL,
|
||||
ajax_url = '{root}/modx/{module}/{id}'.format(root=settings.MITX_ROOT_URL,
|
||||
module=module, id=id)
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
if coursename and settings.ENABLE_MULTICOURSE:
|
||||
@@ -315,26 +377,40 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
response = HttpResponse(json.dumps({'success': error_msg}))
|
||||
return response
|
||||
|
||||
# TODO: This doesn't have a cache of child student modules. Just
|
||||
# passing the current one. If ajax calls end up needing children,
|
||||
# this won't work (but fixing it may cause performance issues...)
|
||||
# Figure out :)
|
||||
module_xml = etree.fromstring(xml)
|
||||
student_module_cache = StudentModuleCache(request.user, module_xml)
|
||||
(instance, instance_state, shared_state, module_type) = get_module(
|
||||
request.user, request, module_xml,
|
||||
student_module_cache, None)
|
||||
|
||||
if instance_state is None:
|
||||
log.debug("Couldn't find module '%s' for user '%s' and id '%s'",
|
||||
module, request.user, id)
|
||||
raise Http404
|
||||
|
||||
oldgrade = instance_state.grade
|
||||
old_instance_state = instance_state.state
|
||||
old_shared_state = shared_state.state if shared_state is not None else None
|
||||
|
||||
module_from_xml = make_module_from_xml_fn(
|
||||
request.user, request, [s], None)
|
||||
request.user, request, student_module_cache, None)
|
||||
|
||||
# Create the module
|
||||
system = I4xSystem(track_function = make_track_function(request),
|
||||
render_function = None,
|
||||
module_from_xml = module_from_xml,
|
||||
render_template = render_to_string,
|
||||
ajax_url = ajax_url,
|
||||
request = request,
|
||||
filestore = OSFS(data_root),
|
||||
system = I4xSystem(track_function=make_track_function(request),
|
||||
render_function=None,
|
||||
module_from_xml=module_from_xml,
|
||||
render_template=render_to_string,
|
||||
ajax_url=ajax_url,
|
||||
request=request,
|
||||
filestore=OSFS(data_root),
|
||||
)
|
||||
|
||||
try:
|
||||
module_class = xmodule.get_module_class(module)
|
||||
instance = module_class(system, xml, id, state=oldstate)
|
||||
instance = module_class(
|
||||
system, xml, id,
|
||||
instance_state=old_instance_state,
|
||||
shared_state=old_shared_state)
|
||||
except:
|
||||
log.exception("Unable to load module instance during ajax call")
|
||||
if accepts(request, 'text/html'):
|
||||
@@ -351,10 +427,16 @@ def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
raise
|
||||
|
||||
# Save the state back to the database
|
||||
s.state = instance.get_state()
|
||||
if instance.get_score():
|
||||
s.grade = instance.get_score()['score']
|
||||
if s.grade != oldgrade or s.state != oldstate:
|
||||
s.save()
|
||||
instance_state.state = instance.get_instance_state()
|
||||
if instance.get_score():
|
||||
instance_state.grade = instance.get_score()['score']
|
||||
if instance_state.grade != oldgrade or instance_state.state != old_instance_state:
|
||||
instance_state.save()
|
||||
|
||||
if shared_state is not None:
|
||||
shared_state.state = instance.get_shared_state()
|
||||
if shared_state.state != old_shared_state:
|
||||
shared_state.save()
|
||||
|
||||
# Return whatever the module wanted to return to the client/caller
|
||||
return HttpResponse(ajax_return)
|
||||
|
||||
@@ -16,11 +16,10 @@ from django.views.decorators.cache import cache_control
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from module_render import render_x_module, make_track_function, I4xSystem
|
||||
from models import StudentModule
|
||||
from module_render import render_x_module, toc_for_course, get_module, get_section
|
||||
from models import StudentModuleCache
|
||||
from student.models import UserProfile
|
||||
from multicourse import multicourse_settings
|
||||
import xmodule
|
||||
|
||||
import courseware.content_parser as content_parser
|
||||
|
||||
@@ -87,23 +86,20 @@ def render_accordion(request, course, chapter, section):
|
||||
If chapter and section are '' or None, renders a default accordion.
|
||||
|
||||
Returns (initialization_javascript, content)'''
|
||||
if not course:
|
||||
course = "6.002 Spring 2012"
|
||||
|
||||
toc = content_parser.toc_from_xml(
|
||||
content_parser.course_file(request.user, course), chapter, section)
|
||||
course_location = multicourse_settings.get_course_location(course)
|
||||
toc = toc_for_course(request.user, request, course_location, chapter, section)
|
||||
|
||||
active_chapter = 1
|
||||
for i in range(len(toc)):
|
||||
if toc[i]['active']:
|
||||
active_chapter = i
|
||||
|
||||
context=dict([('active_chapter', active_chapter),
|
||||
('toc', toc),
|
||||
('course_name', course),
|
||||
('format_url_params', content_parser.format_url_params),
|
||||
('csrf', csrf(request)['csrf_token'])] +
|
||||
template_imports.items())
|
||||
context = dict([('active_chapter', active_chapter),
|
||||
('toc', toc),
|
||||
('course_name', course),
|
||||
('format_url_params', content_parser.format_url_params),
|
||||
('csrf', csrf(request)['csrf_token'])] + template_imports.items())
|
||||
return render_to_string('accordion.html', context)
|
||||
|
||||
|
||||
@@ -125,16 +121,10 @@ def render_section(request, section):
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'accordion': render_accordion(request, '', '', '')
|
||||
'accordion': render_accordion(request, get_course(request), '', '')
|
||||
}
|
||||
|
||||
module_ids = dom.xpath("//@id")
|
||||
|
||||
if user.is_authenticated():
|
||||
student_module_cache = list(StudentModule.objects.filter(student=user,
|
||||
module_id__in=module_ids))
|
||||
else:
|
||||
student_module_cache = []
|
||||
student_module_cache = StudentModuleCache(request.user, dom)
|
||||
|
||||
try:
|
||||
module = render_x_module(user, request, dom, student_module_cache)
|
||||
@@ -147,13 +137,13 @@ def render_section(request, section):
|
||||
return render_to_response('courseware.html', context)
|
||||
|
||||
context.update({
|
||||
'init': module.get('init_js', ''),
|
||||
'content': module['content'],
|
||||
})
|
||||
|
||||
result = render_to_response('courseware.html', context)
|
||||
return result
|
||||
|
||||
|
||||
def get_course(request, course):
|
||||
''' Figure out what the correct course is.
|
||||
|
||||
@@ -161,7 +151,7 @@ def get_course(request, course):
|
||||
TODO: Can this go away once multicourse becomes standard?
|
||||
'''
|
||||
|
||||
if course==None:
|
||||
if course == None:
|
||||
if not settings.ENABLE_MULTICOURSE:
|
||||
course = "6.002 Spring 2012"
|
||||
elif 'coursename' in request.session:
|
||||
@@ -170,35 +160,6 @@ def get_course(request, course):
|
||||
course = settings.COURSE_DEFAULT
|
||||
return course
|
||||
|
||||
def get_module_xml(user, course, chapter, section):
|
||||
''' Look up the module xml for the given course/chapter/section path.
|
||||
|
||||
Takes the user to look up the course file.
|
||||
|
||||
Returns None if there was a problem, or the lxml etree for the module.
|
||||
'''
|
||||
try:
|
||||
# this is the course.xml etree
|
||||
dom = content_parser.course_file(user, course)
|
||||
except:
|
||||
log.exception("Unable to parse courseware xml")
|
||||
return None
|
||||
|
||||
# this is the module's parent's etree
|
||||
path = "//course[@name=$course]/chapter[@name=$chapter]//section[@name=$section]"
|
||||
dom_module = dom.xpath(path, course=course, chapter=chapter, section=section)
|
||||
|
||||
module_wrapper = dom_module[0] if len(dom_module) > 0 else None
|
||||
if module_wrapper is None:
|
||||
module = None
|
||||
elif module_wrapper.get("src"):
|
||||
module = content_parser.section_file(
|
||||
user=user, section=module_wrapper.get("src"), coursename=course)
|
||||
else:
|
||||
# Copy the element out of the module's etree
|
||||
module = etree.XML(etree.tostring(module_wrapper[0]))
|
||||
return module
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@@ -228,55 +189,6 @@ def index(request, course=None, chapter=None, section=None,
|
||||
'''
|
||||
return s.replace('_', ' ') if s is not None else None
|
||||
|
||||
def get_submodule_ids(module_xml):
|
||||
'''
|
||||
Get a list with ids of the modules within this module.
|
||||
'''
|
||||
return module_xml.xpath("//@id")
|
||||
|
||||
def preload_student_modules(module_xml):
|
||||
'''
|
||||
Find any StudentModule objects for this user that match
|
||||
one of the given module_ids. Used as a cache to avoid having
|
||||
each rendered module hit the db separately.
|
||||
|
||||
Returns the list, or None on error.
|
||||
'''
|
||||
if request.user.is_authenticated():
|
||||
module_ids = get_submodule_ids(module_xml)
|
||||
return list(StudentModule.objects.filter(student=request.user,
|
||||
module_id__in=module_ids))
|
||||
else:
|
||||
return []
|
||||
|
||||
def get_module_context():
|
||||
'''
|
||||
Look up the module object and render it. If all goes well, returns
|
||||
{'init': module-init-js, 'content': module-rendered-content}
|
||||
|
||||
If there's an error, returns
|
||||
{'content': module-error message}
|
||||
'''
|
||||
user = request.user
|
||||
|
||||
module_xml = get_module_xml(user, course, chapter, section)
|
||||
if module_xml is None:
|
||||
log.exception("couldn't get module_xml: course/chapter/section: '%s/%s/%s'",
|
||||
course, chapter, section)
|
||||
return {'content' : render_to_string("module-error.html", {})}
|
||||
|
||||
student_module_cache = preload_student_modules(module_xml)
|
||||
|
||||
try:
|
||||
module_context = render_x_module(user, request, module_xml,
|
||||
student_module_cache, position)
|
||||
except:
|
||||
log.exception("Unable to load module")
|
||||
return {'content' : render_to_string("module-error.html", {})}
|
||||
|
||||
return {'init': module_context.get('init_js', ''),
|
||||
'content': module_context['content']}
|
||||
|
||||
if not settings.COURSEWARE_ENABLED:
|
||||
return redirect('/')
|
||||
|
||||
@@ -300,11 +212,16 @@ def index(request, course=None, chapter=None, section=None,
|
||||
|
||||
look_for_module = chapter is not None and section is not None
|
||||
if look_for_module:
|
||||
context.update(get_module_context())
|
||||
course_location = multicourse_settings.get_course_location(course)
|
||||
section = get_section(course_location, chapter, section)
|
||||
student_module_cache = StudentModuleCache(request.user, section)
|
||||
module, _, _, _ = get_module(request.user, request, section.url, student_module_cache)
|
||||
context['content'] = module.get_html()
|
||||
|
||||
result = render_to_response('courseware.html', context)
|
||||
return result
|
||||
|
||||
|
||||
def jump_to(request, probname=None):
|
||||
'''
|
||||
Jump to viewing a specific problem. The problem is specified by a
|
||||
|
||||
@@ -31,11 +31,13 @@ if hasattr(settings,'COURSE_SETTINGS'): # in the future, this could be repla
|
||||
elif hasattr(settings,'COURSE_NAME'): # backward compatibility
|
||||
COURSE_SETTINGS = {settings.COURSE_NAME: {'number': settings.COURSE_NUMBER,
|
||||
'title': settings.COURSE_TITLE,
|
||||
'location': settings.COURSE_LOCATION,
|
||||
},
|
||||
}
|
||||
else: # default to 6.002_Spring_2012
|
||||
COURSE_SETTINGS = {'6.002_Spring_2012': {'number': '6.002x',
|
||||
'title': 'Circuits and Electronics',
|
||||
'location': 'i4x://edx/6002xs12/course/6.002 Spring 2012',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,31 +53,47 @@ def get_coursename_from_request(request):
|
||||
|
||||
def get_course_settings(coursename):
|
||||
if not coursename:
|
||||
if hasattr(settings,'COURSE_DEFAULT'):
|
||||
if hasattr(settings, 'COURSE_DEFAULT'):
|
||||
coursename = settings.COURSE_DEFAULT
|
||||
else:
|
||||
coursename = '6.002_Spring_2012'
|
||||
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename]
|
||||
coursename = coursename.replace(' ','_')
|
||||
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename]
|
||||
if coursename in COURSE_SETTINGS:
|
||||
return COURSE_SETTINGS[coursename]
|
||||
coursename = coursename.replace(' ', '_')
|
||||
if coursename in COURSE_SETTINGS:
|
||||
return COURSE_SETTINGS[coursename]
|
||||
return None
|
||||
|
||||
|
||||
def is_valid_course(coursename):
|
||||
return get_course_settings(coursename) != None
|
||||
|
||||
def get_course_property(coursename,property):
|
||||
|
||||
def get_course_property(coursename, property):
|
||||
cs = get_course_settings(coursename)
|
||||
if not cs: return '' # raise exception instead?
|
||||
if property in cs: return cs[property]
|
||||
return '' # default
|
||||
|
||||
# raise exception instead?
|
||||
if not cs:
|
||||
return ''
|
||||
|
||||
if property in cs:
|
||||
return cs[property]
|
||||
|
||||
# default
|
||||
return ''
|
||||
|
||||
|
||||
def get_course_xmlpath(coursename):
|
||||
return get_course_property(coursename,'xmlpath')
|
||||
return get_course_property(coursename, 'xmlpath')
|
||||
|
||||
|
||||
def get_course_title(coursename):
|
||||
return get_course_property(coursename,'title')
|
||||
return get_course_property(coursename, 'title')
|
||||
|
||||
|
||||
def get_course_number(coursename):
|
||||
return get_course_property(coursename,'number')
|
||||
|
||||
return get_course_property(coursename, 'number')
|
||||
|
||||
|
||||
def get_course_location(coursename):
|
||||
return get_course_property(coursename, 'location')
|
||||
|
||||
@@ -132,10 +132,25 @@ COURSE_DEFAULT = '6.002_Spring_2012'
|
||||
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
|
||||
'title' : 'Circuits and Electronics',
|
||||
'xmlpath': '6002x/',
|
||||
'location': 'i4x://edx/6002xs12/course/6_002_Spring_2012',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
############################### XModule Store ##################################
|
||||
KEYSTORE = {
|
||||
'default': {
|
||||
'ENGINE': 'keystore.xml.XMLModuleStore',
|
||||
'OPTIONS': {
|
||||
'org': 'edx',
|
||||
'course': '6002xs12',
|
||||
'data_dir': DATA_DIR,
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
############################### DJANGO BUILT-INS ###############################
|
||||
# Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here
|
||||
DEBUG = False
|
||||
|
||||
@@ -11,7 +11,7 @@ from .common import *
|
||||
from .logsettings import get_logger_config
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = True
|
||||
TEMPLATE_DEBUG = False
|
||||
|
||||
LOGGING = get_logger_config(ENV_ROOT / "log",
|
||||
logging_env="dev",
|
||||
|
||||
@@ -184,29 +184,29 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None):
|
||||
filestore = OSFS(settings.DATA_DIR + xp),
|
||||
#role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this
|
||||
)
|
||||
instance=xmodule.get_module_class(module)(system,
|
||||
xml,
|
||||
instance = xmodule.get_module_class(module)(system,
|
||||
xml,
|
||||
id,
|
||||
state=None)
|
||||
log.info('ajax_url = ' + instance.ajax_url)
|
||||
|
||||
# create empty student state for this problem, if not previously existing
|
||||
s = StudentModule.objects.filter(student=request.user,
|
||||
module_id=id)
|
||||
s = StudentModule.objects.filter(student=request.user,
|
||||
module_state_key=id)
|
||||
if len(s) == 0 or s is None:
|
||||
smod=StudentModule(student=request.user,
|
||||
module_type = 'problem',
|
||||
module_id=id,
|
||||
state=instance.get_state())
|
||||
smod = StudentModule(student=request.user,
|
||||
module_type='problem',
|
||||
module_state_key=id,
|
||||
state=instance.get_instance_state())
|
||||
smod.save()
|
||||
|
||||
lcp = instance.lcp
|
||||
pxml = lcp.tree
|
||||
pxmls = etree.tostring(pxml,pretty_print=True)
|
||||
pxmls = etree.tostring(pxml, pretty_print=True)
|
||||
|
||||
return instance, pxmls
|
||||
|
||||
instance, pxmls = get_lcp(coursename,id)
|
||||
instance, pxmls = get_lcp(coursename, id)
|
||||
|
||||
# if there was a POST, then process it
|
||||
msg = ''
|
||||
@@ -246,8 +246,6 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None):
|
||||
# get the rendered problem HTML
|
||||
phtml = instance.get_html()
|
||||
# phtml = instance.get_problem_html()
|
||||
# init_js = instance.get_init_js()
|
||||
# destory_js = instance.get_destroy_js()
|
||||
|
||||
context = {'id':id,
|
||||
'msg' : msg,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class @Sequence
|
||||
constructor: (@id, @elements, @tag, position) ->
|
||||
@element = $("#sequence_#{@id}")
|
||||
constructor: (@id, @element_id, @elements, @tag, position) ->
|
||||
@element = $("#sequence_#{@element_id}")
|
||||
@buildNavigation()
|
||||
@initProgress()
|
||||
@bind()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="sequence_${id}" class="sequence">
|
||||
<div id="sequence_${element_id}" class="sequence">
|
||||
<nav aria-label="Section Navigation" class="sequence-nav">
|
||||
<ol id="sequence-list">
|
||||
</ol>
|
||||
@@ -22,7 +22,7 @@
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
new Sequence('${id}', ${items}, '${tag}', ${position});
|
||||
new Sequence('${item_id}', '${element_id}', ${items}, '${tag}', ${position});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ol class="vert-mod">
|
||||
% for t in items:
|
||||
<li id="vert-${items.index(t)}">
|
||||
${t[1]['content']}
|
||||
% for idx, item in enumerate(items):
|
||||
<li id="vert-${idx}">
|
||||
${item}
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
|
||||
Reference in New Issue
Block a user