diff --git a/common/lib/keystore/xml.py b/common/lib/keystore/xml.py
new file mode 100644
index 0000000000..d5baefd787
--- /dev/null
+++ b/common/lib/keystore/xml.py
@@ -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")
diff --git a/common/lib/xmodule/__init__.py b/common/lib/xmodule/__init__.py
index 307b544b79..e69de29bb2 100644
--- a/common/lib/xmodule/__init__.py
+++ b/common/lib/xmodule/__init__.py
@@ -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))
-
diff --git a/common/lib/xmodule/abtest_module.py b/common/lib/xmodule/abtest_module.py
new file mode 100644
index 0000000000..dda6a58c99
--- /dev/null
+++ b/common/lib/xmodule/abtest_module.py
@@ -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:
+
+
+
+
+
+ """
+
+ 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())
diff --git a/common/lib/xmodule/capa_module.py b/common/lib/xmodule/capa_module.py
index b59bc9de56..5047b94832 100644
--- a/common/lib/xmodule/capa_module.py
+++ b/common/lib/xmodule/capa_module.py
@@ -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 file %s has an error:%s' % (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
diff --git a/common/lib/xmodule/hidden_module.py b/common/lib/xmodule/hidden_module.py
new file mode 100644
index 0000000000..d4f2a0fa33
--- /dev/null
+++ b/common/lib/xmodule/hidden_module.py
@@ -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
diff --git a/common/lib/xmodule/html_module.py b/common/lib/xmodule/html_module.py
index b35549d971..32963600cd 100644
--- a/common/lib/xmodule/html_module.py
+++ b/common/lib/xmodule/html_module.py
@@ -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)}}
diff --git a/common/lib/xmodule/raw_module.py b/common/lib/xmodule/raw_module.py
index 7bb94c9b63..43a92303ad 100644
--- a/common/lib/xmodule/raw_module.py
+++ b/common/lib/xmodule/raw_module.py
@@ -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)}
diff --git a/common/lib/xmodule/schematic_module.py b/common/lib/xmodule/schematic_module.py
index 30175c16a8..f95729d4ab 100644
--- a/common/lib/xmodule/schematic_module.py
+++ b/common/lib/xmodule/schematic_module.py
@@ -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 ''.format(item_id=self.item_id)
-
- def __init__(self, system, xml, item_id, state=None):
- XModule.__init__(self, system, xml, item_id, state)
-
diff --git a/common/lib/xmodule/seq_module.py b/common/lib/xmodule/seq_module.py
index e3a19c3d60..b60f0e4656 100644
--- a/common/lib/xmodule/seq_module.py
+++ b/common/lib/xmodule/seq_module.py
@@ -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 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>'),
- 'id': self.item_id,
- 'position': self.position,
- 'titles': titles,
- 'tag': self.xmltree.tag}
+ params = {'items': json.dumps(contents).replace('', '<"+"/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')]
- )
+ ]}
diff --git a/common/lib/xmodule/setup.py b/common/lib/xmodule/setup.py
index 17d7af50db..3e3e33805f 100644
--- a/common/lib/xmodule/setup.py
+++ b/common/lib/xmodule/setup.py
@@ -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",
]
}
)
diff --git a/common/lib/xmodule/template_module.py b/common/lib/xmodule/template_module.py
index 51f9447c06..ae276737e6 100644
--- a/common/lib/xmodule/template_module.py
+++ b/common/lib/xmodule/template_module.py
@@ -31,18 +31,11 @@ class Module(XModule):
Renders to::
More information given in the text
"""
- 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())
diff --git a/common/lib/xmodule/vertical_module.py b/common/lib/xmodule/vertical_module.py
index b3feec8bae..d3f4cd6ad3 100644
--- a/common/lib/xmodule/vertical_module.py
+++ b/common/lib/xmodule/vertical_module.py
@@ -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
diff --git a/common/lib/xmodule/video_module.py b/common/lib/xmodule/video_module.py
index f3d615fd3d..1585944cc9 100644
--- a/common/lib/xmodule/video_module.py
+++ b/common/lib/xmodule/video_module.py
@@ -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
diff --git a/common/lib/xmodule/x_module.py b/common/lib/xmodule/x_module.py
index aad4dd94dc..3787a76752 100644
--- a/common/lib/xmodule/x_module.py
+++ b/common/lib/xmodule/x_module.py
@@ -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):
diff --git a/common/lib/xmodule/xml_module.py b/common/lib/xmodule/xml_module.py
new file mode 100644
index 0000000000..34881a4d61
--- /dev/null
+++ b/common/lib/xmodule/xml_module.py
@@ -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')
+ )
diff --git a/lms/djangoapps/courseware/content_parser.py b/lms/djangoapps/courseware/content_parser.py
index 95c3afed8c..70e5eeeeb6 100644
--- a/lms/djangoapps/courseware/content_parser.py
+++ b/lms/djangoapps/courseware/content_parser.py
@@ -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 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 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)
-
diff --git a/lms/djangoapps/courseware/grades.py b/lms/djangoapps/courseware/grades.py
index 00bdffb697..3c2b654682 100644
--- a/lms/djangoapps/courseware/grades.py
+++ b/lms/djangoapps/courseware/grades.py
@@ -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
diff --git a/lms/djangoapps/courseware/models.py b/lms/djangoapps/courseware/models.py
index a97b09ae2b..6ca67a84e7 100644
--- a/lms/djangoapps/courseware/models.py
+++ b/lms/djangoapps/courseware/models.py
@@ -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)
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 3a6fcbfb45..d05bdcefab 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -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)
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 5cbbe18d7d..6e8eb1ab9e 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -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
diff --git a/lms/djangoapps/multicourse/multicourse_settings.py b/lms/djangoapps/multicourse/multicourse_settings.py
index 05b05c8ec9..4d568d55a1 100644
--- a/lms/djangoapps/multicourse/multicourse_settings.py
+++ b/lms/djangoapps/multicourse/multicourse_settings.py
@@ -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')
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 60834f9d91..8c82b2e8c1 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -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
diff --git a/lms/envs/dev.py b/lms/envs/dev.py
index decd92d136..f175ca1f53 100644
--- a/lms/envs/dev.py
+++ b/lms/envs/dev.py
@@ -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",
diff --git a/lms/lib/dogfood/views.py b/lms/lib/dogfood/views.py
index 17096afc70..a91314d228 100644
--- a/lms/lib/dogfood/views.py
+++ b/lms/lib/dogfood/views.py
@@ -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,
diff --git a/lms/static/coffee/src/modules/sequence.coffee b/lms/static/coffee/src/modules/sequence.coffee
index 32a90f51a5..a4a80e3407 100644
--- a/lms/static/coffee/src/modules/sequence.coffee
+++ b/lms/static/coffee/src/modules/sequence.coffee
@@ -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()
diff --git a/lms/templates/seq_module.html b/lms/templates/seq_module.html
index ab903457dc..00221a4951 100644
--- a/lms/templates/seq_module.html
+++ b/lms/templates/seq_module.html
@@ -1,4 +1,4 @@
-