Commit 34d68348 by Calen Pennington

Begin using a Keystore for XML parsing. Still broken: sequence icons, custom…

Begin using a Keystore for XML parsing. Still broken: sequence icons, custom tags, problems, video js
parent 0a2fd0a1
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")
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))
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): ...@@ -81,14 +81,7 @@ class Module(XModule):
reset. reset.
''' '''
id_attribute = "filename" def get_instance_state(self):
@classmethod
def get_xml_tags(c):
return ["problem"]
def get_state(self):
state = self.lcp.get_state() state = self.lcp.get_state()
state['attempts'] = self.attempts state['attempts'] = self.attempts
return json.dumps(state) return json.dumps(state)
...@@ -191,8 +184,8 @@ class Module(XModule): ...@@ -191,8 +184,8 @@ class Module(XModule):
return html return html
def __init__(self, system, xml, item_id, state=None): def __init__(self, system, xml, item_id, instance_state=None, shared_state=None):
XModule.__init__(self, system, xml, item_id, state) XModule.__init__(self, system, xml, item_id, instance_state, shared_state)
self.attempts = 0 self.attempts = 0
self.max_attempts = None self.max_attempts = None
...@@ -232,19 +225,19 @@ class Module(XModule): ...@@ -232,19 +225,19 @@ class Module(XModule):
self.show_answer = "closed" self.show_answer = "closed"
self.rerandomize = only_one(dom2.xpath('/problem/@rerandomize')) self.rerandomize = only_one(dom2.xpath('/problem/@rerandomize'))
if self.rerandomize == "" or self.rerandomize=="always" or self.rerandomize=="true": if self.rerandomize == "" or self.rerandomize == "always" or self.rerandomize == "true":
self.rerandomize="always" self.rerandomize = "always"
elif self.rerandomize=="false" or self.rerandomize=="per_student": elif self.rerandomize == "false" or self.rerandomize == "per_student":
self.rerandomize="per_student" self.rerandomize = "per_student"
elif self.rerandomize=="never": elif self.rerandomize == "never":
self.rerandomize="never" self.rerandomize = "never"
else: else:
raise Exception("Invalid rerandomize attribute "+self.rerandomize) raise Exception("Invalid rerandomize attribute " + self.rerandomize)
if state!=None: if instance_state != None:
state=json.loads(state) instance_state = json.loads(instance_state)
if state!=None and 'attempts' in state: if instance_state != None and 'attempts' in instance_state:
self.attempts=state['attempts'] self.attempts = instance_state['attempts']
# TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename')) # TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename'))
self.filename= "problems/"+only_one(dom2.xpath('/problem/@filename'))+".xml" self.filename= "problems/"+only_one(dom2.xpath('/problem/@filename'))+".xml"
...@@ -267,7 +260,7 @@ class Module(XModule): ...@@ -267,7 +260,7 @@ class Module(XModule):
else: else:
raise raise
try: 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: except Exception,err:
msg = '[courseware.capa.capa_module.Module.init] error %s: cannot create LoncapaProblem %s' % (err,self.filename) msg = '[courseware.capa.capa_module.Module.init] error %s: cannot create LoncapaProblem %s' % (err,self.filename)
log.exception(msg) log.exception(msg)
...@@ -277,7 +270,7 @@ class Module(XModule): ...@@ -277,7 +270,7 @@ class Module(XModule):
# create a dummy problem with error message instead of failing # 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 = 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" 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: else:
raise raise
......
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
class HiddenModule(XModule):
pass
class HiddenDescriptor(RawDescriptor):
module_class = HiddenModule
import json import json
import logging import logging
from x_module import XModule from xmodule.x_module import XModule
from mako_module import MakoModuleDescriptor from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor
from lxml import etree from lxml import etree
from pkg_resources import resource_string from pkg_resources import resource_string
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
#----------------------------------------------------------------------------- class HtmlModule(XModule):
class HtmlModuleDescriptor(MakoModuleDescriptor): 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 Module for putting raw html in a course
""" """
mako_template = "widgets/html-edit.html" mako_template = "widgets/html-edit.html"
module_class = HtmlModule
js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]} js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]}
js_module = 'HTML' js_module = 'HTML'
@classmethod @classmethod
def from_xml(cls, xml_data, system, org=None, course=None): def definition_from_xml(cls, xml_object, system):
""" return {'data': {'text': etree.tostring(xml_object)}}
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])
from pkg_resources import resource_string from pkg_resources import resource_string
from mako_module import MakoModuleDescriptor
from lxml import etree 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 Module that provides a raw editing view of it's data and children
""" """
...@@ -18,24 +19,5 @@ class RawDescriptor(MakoModuleDescriptor): ...@@ -18,24 +19,5 @@ class RawDescriptor(MakoModuleDescriptor):
} }
@classmethod @classmethod
def from_xml(cls, xml_data, system, org=None, course=None): def definition_from_xml(cls, xml_object, system):
""" return {'data': etree.tostring(xml_object)}
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')]
)
...@@ -6,18 +6,5 @@ class ModuleDescriptor(XModuleDescriptor): ...@@ -6,18 +6,5 @@ class ModuleDescriptor(XModuleDescriptor):
pass pass
class Module(XModule): 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): def get_html(self):
return '<input type="hidden" class="schematic" name="{item_id}" height="480" width="640">'.format(item_id=self.item_id) 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 ...@@ -3,8 +3,9 @@ import logging
from lxml import etree from lxml import etree
from x_module import XModule from xmodule.mako_module import MakoModuleDescriptor
from mako_module import MakoModuleDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.x_module import XModule
from xmodule.progress import Progress from xmodule.progress import Progress
log = logging.getLogger("mitx.common.lib.seq_module") log = logging.getLogger("mitx.common.lib.seq_module")
...@@ -13,32 +14,17 @@ log = logging.getLogger("mitx.common.lib.seq_module") ...@@ -13,32 +14,17 @@ log = logging.getLogger("mitx.common.lib.seq_module")
# OBSOLETE: This obsoletes 'type' # OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem'] class_priority = ['video', 'problem']
class Module(XModule):
class SequenceModule(XModule):
''' Layout module which lays out content in a temporal sequence ''' 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): def get_html(self):
self.render() self.render()
return self.content 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): def get_progress(self):
''' Return the total progress, adding total done and total available. ''' Return the total progress, adding total done and total available.
(assumes that each submodule uses the same "units" for progress.) (assumes that each submodule uses the same "units" for progress.)
...@@ -60,53 +46,51 @@ class Module(XModule): ...@@ -60,53 +46,51 @@ class Module(XModule):
if self.rendered: if self.rendered:
return return
## Returns a set of all types of all sub-children ## 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():
titles = ["\n".join([i.get("name").strip() for i in e.iter() if i.get("name") is not None]) \ progress = child.get_progress()
for e in self.xmltree] contents.append({
'content': child.get_html(),
children = self.get_children() 'title': "\n".join(
progresses = [child.get_progress() for child in children] grand_child.name.strip()
for grand_child in child.get_children()
self.contents = self.rendered_children() if grand_child.name is not None
),
for contents, title, progress in zip(self.contents, titles, progresses): 'progress_status': Progress.to_js_status_str(progress),
contents['title'] = title 'progress_detail': Progress.to_js_detail_str(progress),
contents['progress_status'] = Progress.to_js_status_str(progress) 'type': child.get_icon_class(),
contents['progress_detail'] = Progress.to_js_detail_str(progress) })
for (content, element_class) in zip(self.contents, child_classes): print json.dumps(contents, indent=4)
new_class = 'other'
for c in class_priority:
if c in element_class:
new_class = c
content['type'] = new_class
# Split </script> tags -- browsers handle this as end # Split </script> tags -- browsers handle this as end
# of script, even if it occurs mid-string. Do this after json.dumps()ing # 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 # so that we can be sure of the quotations being used
params={'items': json.dumps(self.contents).replace('</script>', '<"+"/script>'), params = {'items': json.dumps(contents).replace('</script>', '<"+"/script>'),
'id': self.item_id, 'element_id': "-".join(str(v) for v in self.location.list()),
'position': self.position, 'item_id': self.id,
'titles': titles, 'position': self.position,
'tag': self.xmltree.tag} 'tag': self.location.category}
if self.xmltree.tag in ['sequential', 'videosequence']: self.content = self.system.render_template('seq_module.html', params)
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.rendered = True self.rendered = True
def __init__(self, system, xml, item_id, state=None): def get_icon_class(self):
XModule.__init__(self, system, xml, item_id, state) child_classes = set(child.get_icon_class() for child in self.get_children())
self.xmltree = etree.fromstring(xml) 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 self.position = 1
if state is not None: if instance_state is not None:
state = json.loads(state) state = json.loads(instance_state)
if 'position' in state: self.position = int(state['position']) if 'position' in state:
self.position = int(state['position'])
# if position is specified in system, then use that instead # if position is specified in system, then use that instead
if system.get('position'): if system.get('position'):
...@@ -115,23 +99,13 @@ class Module(XModule): ...@@ -115,23 +99,13 @@ class Module(XModule):
self.rendered = False self.rendered = False
class SequenceDescriptor(MakoModuleDescriptor): class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
mako_template = 'widgets/sequence-edit.html' mako_template = 'widgets/sequence-edit.html'
module_class = SequenceModule
@classmethod @classmethod
def from_xml(cls, xml_data, system, org=None, course=None): def definition_from_xml(cls, xml_object, system):
xml_object = etree.fromstring(xml_data) return {'children': [
children = [
system.process_xml(etree.tostring(child_module)).url system.process_xml(etree.tostring(child_module)).url
for child_module in xml_object 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( ...@@ -13,14 +13,15 @@ setup(
# for a description of entry_points # for a description of entry_points
entry_points={ entry_points={
'xmodule.v1': [ 'xmodule.v1': [
"chapter = seq_module:SequenceDescriptor", "chapter = xmodule.seq_module:SequenceDescriptor",
"course = seq_module:SequenceDescriptor", "course = xmodule.seq_module:SequenceDescriptor",
"html = html_module:HtmlModuleDescriptor", "html = xmodule.html_module:HtmlDescriptor",
"section = translation_module:SemanticSectionDescriptor", "section = xmodule.translation_module:SemanticSectionDescriptor",
"sequential = seq_module:SequenceDescriptor", "sequential = xmodule.seq_module:SequenceDescriptor",
"vertical = seq_module:SequenceDescriptor", "vertical = xmodule.vertical_module:VerticalDescriptor",
"problemset = seq_module:SequenceDescriptor", "problemset = xmodule.seq_module:SequenceDescriptor",
"videosequence = seq_module:SequenceDescriptor", "videosequence = xmodule.seq_module:SequenceDescriptor",
"video = xmodule.video_module:VideoDescriptor",
] ]
} }
) )
...@@ -31,18 +31,11 @@ class Module(XModule): ...@@ -31,18 +31,11 @@ class Module(XModule):
Renders to:: Renders to::
More information given in <a href="/book/234">the text</a> 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): def get_html(self):
return self.html return self.html
def __init__(self, system, xml, item_id, state=None): def __init__(self, system, xml, item_id, instance_state=None, shared_state=None):
XModule.__init__(self, system, xml, item_id, state) XModule.__init__(self, system, xml, item_id, instance_state, shared_state)
xmltree = etree.fromstring(xml) xmltree = etree.fromstring(xml)
filename = xmltree.find('impl').text filename = xmltree.find('impl').text
params = dict(xmltree.items()) params = dict(xmltree.items())
......
import json from xmodule.x_module import XModule
from xmodule.seq_module import SequenceDescriptor
from x_module import XModule, XModuleDescriptor
from xmodule.progress import Progress 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.''' ''' 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): def get_html(self):
return self.system.render_template('vert_module.html', { return self.system.render_template('vert_module.html', {
'items': self.contents 'items': self.contents
...@@ -30,8 +17,10 @@ class Module(XModule): ...@@ -30,8 +17,10 @@ class Module(XModule):
progress = reduce(Progress.add_counts, progresses) progress = reduce(Progress.add_counts, progresses)
return progress return progress
def __init__(self, system, xml, item_id, state=None): def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, xml, item_id, state) XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
xmltree=etree.fromstring(xml) self.contents = [child.get_html() for child in self.get_display_items()]
self.contents=[(e.get("name"),self.render_function(e)) \
for e in xmltree]
class VerticalDescriptor(SequenceDescriptor):
module_class = VerticalModule
...@@ -3,16 +3,13 @@ import logging ...@@ -3,16 +3,13 @@ import logging
from lxml import etree from lxml import etree
from x_module import XModule, XModuleDescriptor from xmodule.x_module import XModule
from progress import Progress from xmodule.raw_module import RawDescriptor
log = logging.getLogger("mitx.courseware.modules") log = logging.getLogger(__name__)
class ModuleDescriptor(XModuleDescriptor):
pass
class Module(XModule): class VideoModule(XModule):
id_attribute = 'youtube'
video_time = 0 video_time = 0
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
...@@ -39,14 +36,9 @@ class Module(XModule): ...@@ -39,14 +36,9 @@ class Module(XModule):
''' '''
return None return None
def get_state(self): def get_instance_state(self):
log.debug(u"STATE POSITION {0}".format(self.position)) log.debug(u"STATE POSITION {0}".format(self.position))
return json.dumps({ 'position': self.position }) 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"]
def video_list(self): def video_list(self):
return self.youtube return self.youtube
...@@ -54,27 +46,27 @@ class Module(XModule): ...@@ -54,27 +46,27 @@ class Module(XModule):
def get_html(self): def get_html(self):
return self.system.render_template('video.html', { return self.system.render_template('video.html', {
'streams': self.video_list(), 'streams': self.video_list(),
'id': self.item_id, 'id': self.id,
'position': self.position, 'position': self.position,
'name': self.name, 'name': self.name,
'annotations': self.annotations, 'annotations': self.annotations,
}) })
def __init__(self, system, xml, item_id, state=None): def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, xml, item_id, state) XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(xml) xmltree = etree.fromstring(self.definition['data'])
self.youtube = xmltree.get('youtube') self.youtube = xmltree.get('youtube')
self.name = xmltree.get('name') self.name = xmltree.get('name')
self.position = 0 self.position = 0
if state is not None: if instance_state is not None:
state = json.loads(state) state = json.loads(instance_state)
if 'position' in state: if 'position' in state:
self.position = int(float(state['position'])) 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] for e in xmltree]
class VideoSegmentDescriptor(XModuleDescriptor): class VideoDescriptor(RawDescriptor):
pass module_class = VideoModule
...@@ -3,6 +3,7 @@ import pkg_resources ...@@ -3,6 +3,7 @@ import pkg_resources
import logging import logging
from keystore import Location from keystore import Location
from functools import partial
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
...@@ -56,85 +57,87 @@ class Plugin(object): ...@@ -56,85 +57,87 @@ class Plugin(object):
class XModule(object): class XModule(object):
''' Implements a generic learning module. ''' Implements a generic learning module.
Initialized on access with __init__, first time with state=None, and Initialized on access with __init__, first time with instance_state=None, and
then with state 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 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 system: An I4xSystem allowing access to external resources
def get_xml_tags(c): location: Something Location-like that identifies this xmodule
''' Tags in the courseware file guaranteed to correspond to the module ''' definition: A dictionary containing 'data' and 'children'. Both are optional
return [] '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
@classmethod '''
def get_usage_tags(c): self.system = system
''' We should convert to a real module system self.location = Location(location)
For now, this tells us whether we use this as an xmodule, a CAPA response type self.definition = definition
or a CAPA input type ''' self.instance_state = instance_state
return ['xmodule'] 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): def get_name(self):
name = self.__xmltree.get('name') name = self.__xmltree.get('name')
if name: if name:
return name return name
else: else:
raise "We should iterate through children and find a default name" raise "We should iterate through children and find a default name"
def get_children(self): def get_children(self):
''' '''
Return module instances for all the children of this module. Return module instances for all the children of this module.
''' '''
children = [self.module_from_xml(e) for e in self.__xmltree] if self._loaded_children is None:
return children 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. Returns a list of descendent module instances that will display immediately
This really ought to return a list of xmodules, instead of dictionaries inside this module
''' '''
children = [self.render_function(e) for e in self.__xmltree] items = []
return children 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): return items
''' In most cases, you must pass state or xml'''
if not item_id: def displayable_items(self):
raise ValueError("Missing Index") '''
if not xml and not json: Returns list of displayable modules contained by this module. If this module
raise ValueError("xml or json required") is visible, should return [self]
if not system: '''
raise ValueError("System context required") return [self]
self.xml = xml def get_icon_class(self):
self.json = json '''
self.item_id = item_id Return a class identifying this module in the context of an icon
self.state = state '''
self.DEBUG = False return 'other'
self.__xmltree = etree.fromstring(xml) # PRIVATE
if system:
## These are temporary; we really should go
## through self.system.
self.ajax_url = system.ajax_url
self.tracker = system.track_function
self.filestore = system.filestore
self.render_function = system.render_function
self.module_from_xml = system.module_from_xml
self.DEBUG = system.DEBUG
self.system = system
### Functions used in the LMS ### Functions used in the LMS
def get_state(self): def get_instance_state(self):
''' State of the object, as stored in the database ''' 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): def get_score(self):
''' Score the student received on the problem. ''' Score the student received on the problem.
''' '''
return None return None
...@@ -281,6 +284,7 @@ class XModuleDescriptor(Plugin): ...@@ -281,6 +284,7 @@ class XModuleDescriptor(Plugin):
self.name = Location(kwargs.get('location')).name self.name = Location(kwargs.get('location')).name
self.type = Location(kwargs.get('location')).category self.type = Location(kwargs.get('location')).category
self.url = Location(kwargs.get('location')).url() 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 # 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 # is one of the things that we are going to be iterating on heavily
...@@ -302,33 +306,13 @@ class XModuleDescriptor(Plugin): ...@@ -302,33 +306,13 @@ class XModuleDescriptor(Plugin):
""" """
raise NotImplementedError("get_html() must be provided by specific modules") raise NotImplementedError("get_html() must be provided by specific modules")
def get_xml(self): def xmodule_constructor(self, system):
''' For conversions between JSON and legacy XML representations. """
''' Returns a constructor for an XModule. This constructor takes two arguments:
if self.xml: instance_state and shared_state, and returns a fully nstantiated XModule
return self.xml """
else: return partial(self.module_class, system, self.url, self.definition,
raise NotImplementedError("JSON->XML Translation not implemented") display_name=self.display_name)
def get_json(self):
''' For conversions between JSON and legacy XML representations.
'''
if self.json:
raise NotImplementedError
return self.json # TODO: Return context as well -- files, etc.
else:
raise NotImplementedError("XML->JSON Translation not implemented")
#def handle_cms_json(self):
# raise NotImplementedError
#def render(self, size):
# ''' Size: [thumbnail, small, full]
# Small ==> what we drag around
# Full ==> what we edit
# '''
# raise NotImplementedError
class DescriptorSystem(object): class DescriptorSystem(object):
def __init__(self, load_item, resources_fs): def __init__(self, load_item, resources_fs):
......
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 ...@@ -19,10 +19,12 @@ from django.conf import settings
from student.models import UserProfile from student.models import UserProfile
from student.models import UserTestGroup from student.models import UserTestGroup
from courseware.models import StudentModuleCache
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from util.cache import cache from util.cache import cache
from multicourse import multicourse_settings from multicourse import multicourse_settings
import xmodule import xmodule
from keystore.django import keystore
''' This file will eventually form an abstraction layer between the ''' This file will eventually form an abstraction layer between the
course XML file and the rest of the system. course XML file and the rest of the system.
...@@ -103,6 +105,7 @@ def course_xml_process(tree): ...@@ -103,6 +105,7 @@ def course_xml_process(tree):
items without. Propagate due dates, grace periods, etc. to child items without. Propagate due dates, grace periods, etc. to child
items. items.
''' '''
process_includes(tree)
replace_custom_tags(tree) replace_custom_tags(tree)
id_tag(tree) id_tag(tree)
propogate_downward_tag(tree, "due") propogate_downward_tag(tree, "due")
...@@ -113,45 +116,32 @@ def course_xml_process(tree): ...@@ -113,45 +116,32 @@ def course_xml_process(tree):
return tree return tree
def toc_from_xml(dom, active_chapter, active_section): def process_includes_dir(tree, dir):
''' """
Create a table of contents from the course xml. Process tree to replace all <include file=""> tags
with the contents of the file specified, relative to dir
Return format: """
[ {'name': name, 'sections': SECTIONS, 'active': bool}, ... ] includes = tree.findall('.//include')
for inc in includes:
where SECTIONS is a list file = inc.get('file')
[ {'name': name, 'format': format, 'due': due, 'active' : bool}, ...] if file is not None:
try:
active is set for the section and chapter corresponding to the passed ifp = open(os.path.join(dir, file))
parameters. Everything else comes from the xml, or defaults to "". except Exception:
log.exception('Error in problem xml include: %s' % (etree.tostring(inc, pretty_print=True)))
chapters with name 'hidden' are skipped. log.exception('Cannot find file %s in %s' % (file, dir))
''' raise
name = dom.xpath('//course/@name')[0] try:
# read in and convert to XML
chapters = dom.xpath('//course[@name=$name]/chapter', name=name) incxml = etree.XML(ifp.read())
ch = list() except Exception:
for c in chapters: log.exception('Error in problem xml include: %s' % (etree.tostring(inc, pretty_print=True)))
if c.get('name') == 'hidden': log.exception('Cannot parse XML in %s' % (file))
continue raise
sections = list() # insert new XML into tree in place of inlcude
for s in dom.xpath('//course[@name=$name]/chapter[@name=$chname]/section', parent = inc.getparent()
name=name, chname=c.get('name')): parent.insert(parent.index(inc), incxml)
parent.remove(inc)
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 replace_custom_tags_dir(tree, dir): def replace_custom_tags_dir(tree, dir):
...@@ -181,78 +171,6 @@ def parse_course_file(filename, options, namespace): ...@@ -181,78 +171,6 @@ def parse_course_file(filename, options, namespace):
return course_xml_process(xml) 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 ============================================= # ==== All Django-specific code below =============================================
def user_groups(user): def user_groups(user):
...@@ -278,6 +196,11 @@ def get_options(user): ...@@ -278,6 +196,11 @@ def get_options(user):
'groups': user_groups(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): def replace_custom_tags(tree):
'''Replace custom tags defined in our custom_tags dir''' '''Replace custom tags defined in our custom_tags dir'''
replace_custom_tags_dir(tree, settings.DATA_DIR+'/custom_tags') replace_custom_tags_dir(tree, settings.DATA_DIR+'/custom_tags')
...@@ -337,29 +260,3 @@ def sections_dir(coursename=None): ...@@ -337,29 +260,3 @@ def sections_dir(coursename=None):
xp = multicourse_settings.get_course_xmlpath(coursename) xp = multicourse_settings.get_course_xmlpath(coursename)
return settings.DATA_DIR + xp + '/sections/' 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): ...@@ -81,12 +81,12 @@ def grade_sheet(student,coursename=None):
course = dom.xpath('//course/@name')[0] course = dom.xpath('//course/@name')[0]
xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course) xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course)
responses=StudentModule.objects.filter(student=student) responses = StudentModule.objects.filter(student=student)
response_by_id = {} response_by_id = {}
for response in responses: for response in responses:
response_by_id[response.module_id] = response response_by_id[response.module_state_key] = response
totaled_scores = {} totaled_scores = {}
chapters=[] chapters=[]
for c in xmlChapters: for c in xmlChapters:
...@@ -147,27 +147,39 @@ def grade_sheet(student,coursename=None): ...@@ -147,27 +147,39 @@ def grade_sheet(student,coursename=None):
'grade_summary' : grade_summary} 'grade_summary' : grade_summary}
def get_score(user, problem, cache, coursename=None): 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 ## 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 correct = 0.0
# If the ID is not in the cache, add the item # If the ID is not in the cache, add the item
if id not in cache: if module_state_key not in cache:
module = StudentModule(module_type = 'problem', # TODO: Move into StudentModule.__init__? module = StudentModule(module_type='problem', # TODO: Move into StudentModule.__init__?
module_id = id, module_state_key=id,
student = user, student=user,
state = None, state=None,
grade = 0, grade=0,
max_grade = None, max_grade=None,
done = 'i') done='i')
cache[id] = module cache[module_id] = module
# Grab the # correct from cache # Grab the # correct from cache
if id in cache: if id in cache:
response = cache[id] response = cache[id]
if response.grade!=None: if response.grade != None:
correct=float(response.grade) correct = float(response.grade)
# Grab max grade from cache, or if it doesn't exist, compute and save to DB # 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: if id in cache and response.max_grade is not None:
total = response.max_grade total = response.max_grade
......
...@@ -13,7 +13,6 @@ ASSUMPTIONS: modules have unique IDs, even across different module_types ...@@ -13,7 +13,6 @@ ASSUMPTIONS: modules have unique IDs, even across different module_types
""" """
from django.db import models from django.db import models
from django.db.models.signals import post_save, post_delete
#from django.core.cache import cache #from django.core.cache import cache
from django.contrib.auth.models import User from django.contrib.auth.models import User
...@@ -21,72 +20,97 @@ 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 #CACHE_TIMEOUT = 60 * 60 * 4 # Set the cache timeout to be four hours
class StudentModule(models.Model): class StudentModule(models.Model):
# For a homework problem, contains a JSON # For a homework problem, contains a JSON
# object consisting of state # object consisting of state
MODULE_TYPES = (('problem','problem'), MODULE_TYPES = (('problem', 'problem'),
('video','video'), ('video', 'video'),
('html','html'), ('html', 'html'),
) )
## These three are the key for the object ## These three are the key for the object
module_type = models.CharField(max_length=32, choices=MODULE_TYPES, default='problem', db_index=True) 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) student = models.ForeignKey(User, db_index=True)
class Meta: class Meta:
unique_together = (('student', 'module_id'),) unique_together = (('student', 'module_state_key'),)
## Internal state of the object ## Internal state of the object
state = models.TextField(null=True, blank=True) 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) grade = models.FloatField(null=True, blank=True, db_index=True)
max_grade = models.FloatField(null=True, blank=True) max_grade = models.FloatField(null=True, blank=True)
DONE_TYPES = (('na','NOT_APPLICABLE'), DONE_TYPES = (('na', 'NOT_APPLICABLE'),
('f','FINISHED'), ('f', 'FINISHED'),
('i','INCOMPLETE'), ('i', 'INCOMPLETE'),
) )
done = models.CharField(max_length=8, choices=DONE_TYPES, default='na', db_index=True) 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) created = models.DateTimeField(auto_now_add=True, db_index=True)
modified = models.DateTimeField(auto_now=True, db_index=True) modified = models.DateTimeField(auto_now=True, db_index=True)
def __unicode__(self): def __unicode__(self):
return self.module_type+'/'+self.student.username+"/"+self.module_id+'/'+str(self.state)[:20] return '/'.join([self.module_type, self.student.username, self.module_state_key, str(self.state)[:20]])
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
# @classmethod class StudentModuleCache(object):
# def get_with_caching(cls, student, module_id): """
# k = cls.key_for(student, module_id) A cache of StudentModules for a specific student
# student_module = cache.get(k) """
# if student_module is None: def __init__(self, user, descriptor, depth=None):
# student_module = StudentModule.objects.filter(student=student, '''
# module_id=module_id)[0] Find any StudentModule objects that are needed by any child modules of the
# # It's possible it really doesn't exist... supplied descriptor. Avoids making multiple queries to the database
# if student_module is not None: '''
# cache.set(k, student_module, CACHE_TIMEOUT) 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 = []
# return student_module 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]
@classmethod shared_state_key = getattr(descriptor, 'shared_state_key', None)
def key_for(cls, student, module_id): if shared_state_key is not None:
return "StudentModule-student_id:{0};module_id:{1}".format(student.id, module_id) keys.append(shared_state_key)
if depth is None or depth > 0:
new_depth = depth - 1 if depth is not None else depth
# def clear_cache_by_student_and_module_id(sender, instance, *args, **kwargs): for child in descriptor.get_children():
# k = sender.key_for(instance.student, instance.module_id) keys.extend(self._get_module_state_keys(child, new_depth))
# cache.delete(k)
# def update_cache_by_student_and_module_id(sender, instance, *args, **kwargs): return keys
# k = sender.key_for(instance.student, instance.module_id)
# cache.set(k, instance, CACHE_TIMEOUT)
def lookup(self, module_type, module_state_key):
'''
Look for a student module with the given type and id in the cache.
#post_save.connect(update_cache_by_student_and_module_id, sender=StudentModule, weak=False) cache -- list of student modules
#post_delete.connect(clear_cache_by_student_and_module_id, sender=StudentModule, weak=False)
#cache_model(StudentModule) 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)
...@@ -16,11 +16,10 @@ from django.views.decorators.cache import cache_control ...@@ -16,11 +16,10 @@ from django.views.decorators.cache import cache_control
from lxml import etree from lxml import etree
from module_render import render_x_module, make_track_function, I4xSystem from module_render import render_x_module, toc_for_course, get_module, get_section
from models import StudentModule from models import StudentModuleCache
from student.models import UserProfile from student.models import UserProfile
from multicourse import multicourse_settings from multicourse import multicourse_settings
import xmodule
import courseware.content_parser as content_parser import courseware.content_parser as content_parser
...@@ -87,23 +86,20 @@ def render_accordion(request, course, chapter, section): ...@@ -87,23 +86,20 @@ def render_accordion(request, course, chapter, section):
If chapter and section are '' or None, renders a default accordion. If chapter and section are '' or None, renders a default accordion.
Returns (initialization_javascript, content)''' Returns (initialization_javascript, content)'''
if not course:
course = "6.002 Spring 2012"
toc = content_parser.toc_from_xml( course_location = multicourse_settings.get_course_location(course)
content_parser.course_file(request.user, course), chapter, section) toc = toc_for_course(request.user, request, course_location, chapter, section)
active_chapter = 1 active_chapter = 1
for i in range(len(toc)): for i in range(len(toc)):
if toc[i]['active']: if toc[i]['active']:
active_chapter = i active_chapter = i
context=dict([('active_chapter', active_chapter), context = dict([('active_chapter', active_chapter),
('toc', toc), ('toc', toc),
('course_name', course), ('course_name', course),
('format_url_params', content_parser.format_url_params), ('format_url_params', content_parser.format_url_params),
('csrf', csrf(request)['csrf_token'])] + ('csrf', csrf(request)['csrf_token'])] + template_imports.items())
template_imports.items())
return render_to_string('accordion.html', context) return render_to_string('accordion.html', context)
...@@ -125,16 +121,10 @@ def render_section(request, section): ...@@ -125,16 +121,10 @@ def render_section(request, section):
context = { context = {
'csrf': csrf(request)['csrf_token'], 'csrf': csrf(request)['csrf_token'],
'accordion': render_accordion(request, '', '', '') 'accordion': render_accordion(request, get_course(request), '', '')
} }
module_ids = dom.xpath("//@id") student_module_cache = StudentModuleCache(request.user, dom)
if user.is_authenticated():
student_module_cache = list(StudentModule.objects.filter(student=user,
module_id__in=module_ids))
else:
student_module_cache = []
try: try:
module = render_x_module(user, request, dom, student_module_cache) module = render_x_module(user, request, dom, student_module_cache)
...@@ -147,13 +137,13 @@ def render_section(request, section): ...@@ -147,13 +137,13 @@ def render_section(request, section):
return render_to_response('courseware.html', context) return render_to_response('courseware.html', context)
context.update({ context.update({
'init': module.get('init_js', ''),
'content': module['content'], 'content': module['content'],
}) })
result = render_to_response('courseware.html', context) result = render_to_response('courseware.html', context)
return result return result
def get_course(request, course): def get_course(request, course):
''' Figure out what the correct course is. ''' Figure out what the correct course is.
...@@ -161,7 +151,7 @@ def get_course(request, course): ...@@ -161,7 +151,7 @@ def get_course(request, course):
TODO: Can this go away once multicourse becomes standard? TODO: Can this go away once multicourse becomes standard?
''' '''
if course==None: if course == None:
if not settings.ENABLE_MULTICOURSE: if not settings.ENABLE_MULTICOURSE:
course = "6.002 Spring 2012" course = "6.002 Spring 2012"
elif 'coursename' in request.session: elif 'coursename' in request.session:
...@@ -170,35 +160,6 @@ def get_course(request, course): ...@@ -170,35 +160,6 @@ def get_course(request, course):
course = settings.COURSE_DEFAULT course = settings.COURSE_DEFAULT
return course 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 @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @cache_control(no_cache=True, no_store=True, must_revalidate=True)
...@@ -228,55 +189,6 @@ def index(request, course=None, chapter=None, section=None, ...@@ -228,55 +189,6 @@ def index(request, course=None, chapter=None, section=None,
''' '''
return s.replace('_', ' ') if s is not None else 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: if not settings.COURSEWARE_ENABLED:
return redirect('/') return redirect('/')
...@@ -300,11 +212,16 @@ def index(request, course=None, chapter=None, section=None, ...@@ -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 look_for_module = chapter is not None and section is not None
if look_for_module: 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) result = render_to_response('courseware.html', context)
return result return result
def jump_to(request, probname=None): def jump_to(request, probname=None):
''' '''
Jump to viewing a specific problem. The problem is specified by a 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 ...@@ -31,11 +31,13 @@ if hasattr(settings,'COURSE_SETTINGS'): # in the future, this could be repla
elif hasattr(settings,'COURSE_NAME'): # backward compatibility elif hasattr(settings,'COURSE_NAME'): # backward compatibility
COURSE_SETTINGS = {settings.COURSE_NAME: {'number': settings.COURSE_NUMBER, COURSE_SETTINGS = {settings.COURSE_NAME: {'number': settings.COURSE_NUMBER,
'title': settings.COURSE_TITLE, 'title': settings.COURSE_TITLE,
'location': settings.COURSE_LOCATION,
}, },
} }
else: # default to 6.002_Spring_2012 else: # default to 6.002_Spring_2012
COURSE_SETTINGS = {'6.002_Spring_2012': {'number': '6.002x', COURSE_SETTINGS = {'6.002_Spring_2012': {'number': '6.002x',
'title': 'Circuits and Electronics', 'title': 'Circuits and Electronics',
'location': 'i4x://edx/6002xs12/course/6.002 Spring 2012',
}, },
} }
...@@ -51,31 +53,47 @@ def get_coursename_from_request(request): ...@@ -51,31 +53,47 @@ def get_coursename_from_request(request):
def get_course_settings(coursename): def get_course_settings(coursename):
if not coursename: if not coursename:
if hasattr(settings,'COURSE_DEFAULT'): if hasattr(settings, 'COURSE_DEFAULT'):
coursename = settings.COURSE_DEFAULT coursename = settings.COURSE_DEFAULT
else: else:
coursename = '6.002_Spring_2012' coursename = '6.002_Spring_2012'
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename] if coursename in COURSE_SETTINGS:
coursename = coursename.replace(' ','_') 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 return None
def is_valid_course(coursename): def is_valid_course(coursename):
return get_course_settings(coursename) != None return get_course_settings(coursename) != None
def get_course_property(coursename,property):
def get_course_property(coursename, property):
cs = get_course_settings(coursename) cs = get_course_settings(coursename)
if not cs: return '' # raise exception instead?
if property in cs: return cs[property] # raise exception instead?
return '' # default if not cs:
return ''
if property in cs:
return cs[property]
# default
return ''
def get_course_xmlpath(coursename): def get_course_xmlpath(coursename):
return get_course_property(coursename,'xmlpath') return get_course_property(coursename, 'xmlpath')
def get_course_title(coursename): def get_course_title(coursename):
return get_course_property(coursename,'title') return get_course_property(coursename, 'title')
def get_course_number(coursename): 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' ...@@ -132,10 +132,25 @@ COURSE_DEFAULT = '6.002_Spring_2012'
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x', COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
'title' : 'Circuits and Electronics', 'title' : 'Circuits and Electronics',
'xmlpath': '6002x/', '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 ############################### ############################### DJANGO BUILT-INS ###############################
# Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here # Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here
DEBUG = False DEBUG = False
......
...@@ -11,7 +11,7 @@ from .common import * ...@@ -11,7 +11,7 @@ from .common import *
from .logsettings import get_logger_config from .logsettings import get_logger_config
DEBUG = True DEBUG = True
TEMPLATE_DEBUG = True TEMPLATE_DEBUG = False
LOGGING = get_logger_config(ENV_ROOT / "log", LOGGING = get_logger_config(ENV_ROOT / "log",
logging_env="dev", logging_env="dev",
......
...@@ -184,29 +184,29 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): ...@@ -184,29 +184,29 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None):
filestore = OSFS(settings.DATA_DIR + xp), filestore = OSFS(settings.DATA_DIR + xp),
#role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this #role = 'staff' if request.user.is_staff else 'student', # TODO: generalize this
) )
instance=xmodule.get_module_class(module)(system, instance = xmodule.get_module_class(module)(system,
xml, xml,
id, id,
state=None) state=None)
log.info('ajax_url = ' + instance.ajax_url) log.info('ajax_url = ' + instance.ajax_url)
# create empty student state for this problem, if not previously existing # create empty student state for this problem, if not previously existing
s = StudentModule.objects.filter(student=request.user, s = StudentModule.objects.filter(student=request.user,
module_id=id) module_state_key=id)
if len(s) == 0 or s is None: if len(s) == 0 or s is None:
smod=StudentModule(student=request.user, smod = StudentModule(student=request.user,
module_type = 'problem', module_type='problem',
module_id=id, module_state_key=id,
state=instance.get_state()) state=instance.get_instance_state())
smod.save() smod.save()
lcp = instance.lcp lcp = instance.lcp
pxml = lcp.tree pxml = lcp.tree
pxmls = etree.tostring(pxml,pretty_print=True) pxmls = etree.tostring(pxml, pretty_print=True)
return instance, pxmls return instance, pxmls
instance, pxmls = get_lcp(coursename,id) instance, pxmls = get_lcp(coursename, id)
# if there was a POST, then process it # if there was a POST, then process it
msg = '' msg = ''
...@@ -246,8 +246,6 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): ...@@ -246,8 +246,6 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None):
# get the rendered problem HTML # get the rendered problem HTML
phtml = instance.get_html() phtml = instance.get_html()
# phtml = instance.get_problem_html() # phtml = instance.get_problem_html()
# init_js = instance.get_init_js()
# destory_js = instance.get_destroy_js()
context = {'id':id, context = {'id':id,
'msg' : msg, 'msg' : msg,
......
class @Sequence class @Sequence
constructor: (@id, @elements, @tag, position) -> constructor: (@id, @element_id, @elements, @tag, position) ->
@element = $("#sequence_#{@id}") @element = $("#sequence_#{@element_id}")
@buildNavigation() @buildNavigation()
@initProgress() @initProgress()
@bind() @bind()
......
<div id="sequence_${id}" class="sequence"> <div id="sequence_${element_id}" class="sequence">
<nav aria-label="Section Navigation" class="sequence-nav"> <nav aria-label="Section Navigation" class="sequence-nav">
<ol id="sequence-list"> <ol id="sequence-list">
</ol> </ol>
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<%block name="js_extra"> <%block name="js_extra">
<script type="text/javascript"> <script type="text/javascript">
$(function(){ $(function(){
new Sequence('${id}', ${items}, '${tag}', ${position}); new Sequence('${item_id}', '${element_id}', ${items}, '${tag}', ${position});
}); });
</script> </script>
</%block> </%block>
<ol class="vert-mod"> <ol class="vert-mod">
% for t in items: % for idx, item in enumerate(items):
<li id="vert-${items.index(t)}"> <li id="vert-${idx}">
${t[1]['content']} ${item}
</li> </li>
% endfor % endfor
</ol> </ol>
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