Commit 829e502b by ichuang

Merge pull request #153 from MITx/cpennington/lms-descriptors

These changes make the LMS read from the XML course files using the ModuleStore interface. 

Note that stable-edx4edx will now no longer be a clean merge.  But this code has priority.
parents f64d953d 5dd412bd
...@@ -4,13 +4,8 @@ ...@@ -4,13 +4,8 @@
from django.core.management.base import BaseCommand, CommandError from django.core.management.base import BaseCommand, CommandError
from keystore.django import keystore from keystore.django import keystore
from raw_module import RawDescriptor
from lxml import etree from lxml import etree
from fs.osfs import OSFS from keystore.xml import XMLModuleStore
from mako.lookup import TemplateLookup
from path import path
from x_module import XModuleDescriptor, XMLParsingSystem
unnamed_modules = 0 unnamed_modules = 0
...@@ -27,33 +22,11 @@ class Command(BaseCommand): ...@@ -27,33 +22,11 @@ class Command(BaseCommand):
raise CommandError("import requires 3 arguments: <org> <course> <data directory>") raise CommandError("import requires 3 arguments: <org> <course> <data directory>")
org, course, data_dir = args org, course, data_dir = args
data_dir = path(data_dir)
module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor')
class ImportSystem(XMLParsingSystem): for module in module_store.modules.itervalues():
def __init__(self): keystore().create_item(module.location)
self.load_item = keystore().get_item if 'data' in module.definition:
self.fs = OSFS(data_dir) keystore().update_item(module.location, module.definition['data'])
if 'children' in module.definition:
def process_xml(self, xml): keystore().update_children(module.location, module.definition['children'])
try:
xml_data = etree.fromstring(xml)
except:
raise CommandError("Unable to parse xml: " + xml)
if not xml_data.get('name'):
global unnamed_modules
unnamed_modules += 1
xml_data.set('name', '{tag}_{count}'.format(tag=xml_data.tag, count=unnamed_modules))
module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, RawDescriptor)
keystore().create_item(module.url)
if 'data' in module.definition:
keystore().update_item(module.url, module.definition['data'])
if 'children' in module.definition:
keystore().update_children(module.url, module.definition['children'])
return module
lookup = TemplateLookup(directories=[data_dir])
template = lookup.get_template("course.xml")
course_string = template.render(groups=[])
ImportSystem().process_xml(course_string)
...@@ -10,7 +10,7 @@ def index(request): ...@@ -10,7 +10,7 @@ def index(request):
# TODO (cpennington): These need to be read in from the active user # TODO (cpennington): These need to be read in from the active user
org = 'mit.edu' org = 'mit.edu'
course = '6002xs12' course = '6002xs12'
name = '6.002 Spring 2012' name = '6.002_Spring_2012'
course = keystore().get_item(['i4x', org, course, 'course', name]) course = keystore().get_item(['i4x', org, course, 'course', name])
weeks = course.get_children() weeks = course.get_children()
return render_to_response('index.html', {'weeks': weeks}) return render_to_response('index.html', {'weeks': weeks})
...@@ -22,7 +22,7 @@ def edit_item(request): ...@@ -22,7 +22,7 @@ def edit_item(request):
return render_to_response('unit.html', { return render_to_response('unit.html', {
'contents': item.get_html(), 'contents': item.get_html(),
'js_module': item.js_module_name(), 'js_module': item.js_module_name(),
'type': item.type, 'category': item.category,
'name': item.name, 'name': item.name,
}) })
......
...@@ -8,9 +8,13 @@ TEMPLATE_DEBUG = DEBUG ...@@ -8,9 +8,13 @@ TEMPLATE_DEBUG = DEBUG
KEYSTORE = { KEYSTORE = {
'default': { 'default': {
'host': 'localhost', 'ENGINE': 'keystore.mongo.MongoModuleStore',
'db': 'mongo_base', 'OPTIONS': {
'collection': 'key_store', 'default_class': 'xmodule.raw_module.RawDescriptor',
'host': 'localhost',
'db': 'mongo_base',
'collection': 'key_store',
}
} }
} }
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<header> <header>
<section> <section>
<h1 class="editable">${name}</h1> <h1 class="editable">${name}</h1>
<p>${type}</p> <p>${category}</p>
</section> </section>
<div class="actions"> <div class="actions">
......
...@@ -33,8 +33,8 @@ ...@@ -33,8 +33,8 @@
</section> </section>
</section> </section>
<textarea name="" class="edit-box" rows="8" cols="40">${module.definition['data']['text']}</textarea> <textarea name="" class="edit-box" rows="8" cols="40">${module.definition['data']}</textarea>
<div class="preview">${module.definition['data']['text']}</div> <div class="preview">${module.definition['data']}</div>
<div class="actions wip"> <div class="actions wip">
<a href="" class="save-update">Save &amp; Update</a> <a href="" class="save-update">Save &amp; Update</a>
......
...@@ -38,10 +38,10 @@ ...@@ -38,10 +38,10 @@
% for week in weeks: % for week in weeks:
<li> <li>
<header> <header>
<h1><a href="#" class="module-edit" id="${week.url}">${week.name}</a></h1> <h1><a href="#" class="module-edit" id="${week.location.url()}">${week.name}</a></h1>
<ul> <ul>
% if week.goals: % if 'goals' in week.metadata:
% for goal in week.goals: % for goal in week.metadata['goals']:
<li class="goal editable">${goal}</li> <li class="goal editable">${goal}</li>
% endfor % endfor
% else: % else:
...@@ -52,8 +52,8 @@ ...@@ -52,8 +52,8 @@
<ul> <ul>
% for module in week.get_children(): % for module in week.get_children():
<li class="${module.type}"> <li class="${module.category}">
<a href="#" class="module-edit" id="${module.url}">${module.name}</a> <a href="#" class="module-edit" id="${module.location.url()}">${module.name}</a>
<a href="#" class="draggable">handle</a> <a href="#" class="draggable">handle</a>
</li> </li>
% endfor % endfor
......
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
<ol> <ol>
% for child in module.get_children(): % for child in module.get_children():
<li> <li>
<a href="#" class="module-edit" id="${child.url}">${child.name}</a> <a href="#" class="module-edit" id="${child.location.url()}">${child.name}</a>
<a href="#" class="draggable">handle</a> <a href="#" class="draggable">handle</a>
</li> </li>
%endfor %endfor
......
...@@ -4,6 +4,7 @@ that are stored in a database an accessible using their Location as an identifie ...@@ -4,6 +4,7 @@ that are stored in a database an accessible using their Location as an identifie
""" """
import re import re
from collections import namedtuple
from .exceptions import InvalidLocationError from .exceptions import InvalidLocationError
URL_RE = re.compile(""" URL_RE = re.compile("""
...@@ -15,8 +16,10 @@ URL_RE = re.compile(""" ...@@ -15,8 +16,10 @@ URL_RE = re.compile("""
(/(?P<revision>[^/]+))? (/(?P<revision>[^/]+))?
""", re.VERBOSE) """, re.VERBOSE)
INVALID_CHARS = re.compile(r"[^\w.-]")
class Location(object): _LocationBase = namedtuple('LocationBase', 'tag org course category name revision')
class Location(_LocationBase):
''' '''
Encodes a location. Encodes a location.
...@@ -26,7 +29,16 @@ class Location(object): ...@@ -26,7 +29,16 @@ class Location(object):
However, they can also be represented a dictionaries (specifying each component), However, they can also be represented a dictionaries (specifying each component),
tuples or list (specified in order), or as strings of the url tuples or list (specified in order), or as strings of the url
''' '''
def __init__(self, location): __slots__ = ()
@classmethod
def clean(cls, value):
"""
Return value, made into a form legal for locations
"""
return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
def __new__(_cls, loc_or_tag, org=None, course=None, category=None, name=None, revision=None):
""" """
Create a new location that is a clone of the specifed one. Create a new location that is a clone of the specifed one.
...@@ -45,55 +57,55 @@ class Location(object): ...@@ -45,55 +57,55 @@ class Location(object):
In both the dict and list forms, the revision is optional, and can be ommitted. In both the dict and list forms, the revision is optional, and can be ommitted.
None of the components of a location may contain the '/' character Components must be composed of alphanumeric characters, or the characters '_', '-', and '.'
Components may be set to None, which may be interpreted by some contexts to mean Components may be set to None, which may be interpreted by some contexts to mean
wildcard selection wildcard selection
""" """
self.update(location)
def update(self, location): if org is None and course is None and category is None and name is None and revision is None:
""" location = loc_or_tag
Update this instance with data from another Location object. else:
location = (loc_or_tag, org, course, category, name, revision)
location: can take the same forms as specified by `__init__` def check_dict(dict_):
""" check_list(dict_.values())
self.tag = self.org = self.course = self.category = self.name = self.revision = None
def check_list(list_):
for val in list_:
if val is not None and INVALID_CHARS.search(val) is not None:
raise InvalidLocationError(location)
if isinstance(location, basestring): if isinstance(location, basestring):
match = URL_RE.match(location) match = URL_RE.match(location)
if match is None: if match is None:
raise InvalidLocationError(location) raise InvalidLocationError(location)
else: else:
self.update(match.groupdict()) groups = match.groupdict()
elif isinstance(location, list): check_dict(groups)
return _LocationBase.__new__(_cls, **groups)
elif isinstance(location, (list, tuple)):
if len(location) not in (5, 6): if len(location) not in (5, 6):
raise InvalidLocationError(location) raise InvalidLocationError(location)
(self.tag, self.org, self.course, self.category, self.name) = location[0:5] if len(location) == 5:
self.revision = location[5] if len(location) == 6 else None args = tuple(location) + (None, )
else:
args = tuple(location)
check_list(args)
return _LocationBase.__new__(_cls, *args)
elif isinstance(location, dict): elif isinstance(location, dict):
try: kwargs = dict(location)
self.tag = location['tag'] kwargs.setdefault('revision', None)
self.org = location['org']
self.course = location['course'] check_dict(kwargs)
self.category = location['category'] return _LocationBase.__new__(_cls, **kwargs)
self.name = location['name']
except KeyError:
raise InvalidLocationError(location)
self.revision = location.get('revision')
elif isinstance(location, Location): elif isinstance(location, Location):
self.update(location.list()) return _LocationBase.__new__(_cls, location)
else: else:
raise InvalidLocationError(location) raise InvalidLocationError(location)
for val in self.list():
if val is not None and '/' in val:
raise InvalidLocationError(location)
def __str__(self):
return self.url()
def url(self): def url(self):
""" """
Return a string containing the URL for this location Return a string containing the URL for this location
...@@ -103,22 +115,23 @@ class Location(object): ...@@ -103,22 +115,23 @@ class Location(object):
url += "/" + self.revision url += "/" + self.revision
return url return url
def list(self): def html_id(self):
""" """
Return a list representing this location Return a string with a version of the location that is safe for use in html id attributes
""" """
return [self.tag, self.org, self.course, self.category, self.name, self.revision] return "-".join(str(v) for v in self if v is not None)
def dict(self): def dict(self):
""" return self.__dict__
Return a dictionary representing this location
""" def list(self):
return {'tag': self.tag, return list(self)
'org': self.org,
'course': self.course, def __str__(self):
'category': self.category, return self.url()
'name': self.name,
'revision': self.revision} def __repr__(self):
return "Location%s" % repr(tuple(self))
class ModuleStore(object): class ModuleStore(object):
......
...@@ -6,9 +6,9 @@ Passes settings.KEYSTORE as kwargs to MongoModuleStore ...@@ -6,9 +6,9 @@ Passes settings.KEYSTORE as kwargs to MongoModuleStore
from __future__ import absolute_import from __future__ import absolute_import
from importlib import import_module
from django.conf import settings from django.conf import settings
from .mongo import MongoModuleStore
from raw_module import RawDescriptor
_KEYSTORES = {} _KEYSTORES = {}
...@@ -17,9 +17,10 @@ def keystore(name='default'): ...@@ -17,9 +17,10 @@ def keystore(name='default'):
global _KEYSTORES global _KEYSTORES
if name not in _KEYSTORES: if name not in _KEYSTORES:
# TODO (cpennington): Load the default class from a string class_path = settings.KEYSTORE[name]['ENGINE']
_KEYSTORES[name] = MongoModuleStore( module_path, _, class_name = class_path.rpartition('.')
default_class=RawDescriptor, class_ = getattr(import_module(module_path), class_name)
**settings.KEYSTORE[name]) _KEYSTORES[name] = class_(
**settings.KEYSTORE[name]['OPTIONS'])
return _KEYSTORES[name] return _KEYSTORES[name]
import pymongo import pymongo
from importlib import import_module
from xmodule.x_module import XModuleDescriptor, DescriptorSystem
from . import ModuleStore, Location from . import ModuleStore, Location
from .exceptions import ItemNotFoundError, InsufficientSpecificationError from .exceptions import ItemNotFoundError, InsufficientSpecificationError
from xmodule.x_module import XModuleDescriptor, DescriptorSystem
class MongoModuleStore(ModuleStore): class MongoModuleStore(ModuleStore):
...@@ -16,7 +18,10 @@ class MongoModuleStore(ModuleStore): ...@@ -16,7 +18,10 @@ class MongoModuleStore(ModuleStore):
# Force mongo to report errors, at the expense of performance # Force mongo to report errors, at the expense of performance
self.collection.safe = True self.collection.safe = True
self.default_class = default_class
module_path, _, class_name = default_class.rpartition('.')
class_ = getattr(import_module(module_path), class_name)
self.default_class = class_
def get_item(self, location): def get_item(self, location):
""" """
...@@ -29,8 +34,6 @@ class MongoModuleStore(ModuleStore): ...@@ -29,8 +34,6 @@ class MongoModuleStore(ModuleStore):
If no object is found at that location, raises keystore.exceptions.ItemNotFoundError If no object is found at that location, raises keystore.exceptions.ItemNotFoundError
location: Something that can be passed to Location location: Something that can be passed to Location
default_class: An XModuleDescriptor subclass to use if no plugin matching the
location is found
""" """
query = {} query = {}
...@@ -48,8 +51,9 @@ class MongoModuleStore(ModuleStore): ...@@ -48,8 +51,9 @@ class MongoModuleStore(ModuleStore):
if item is None: if item is None:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
# TODO (cpennington): Pass a proper resources_fs to the system
return XModuleDescriptor.load_from_json( return XModuleDescriptor.load_from_json(
item, DescriptorSystem(self.get_item), self.default_class) item, DescriptorSystem(self.get_item, None), self.default_class)
def create_item(self, location): def create_item(self, location):
""" """
......
from nose.tools import assert_equals, assert_raises from nose.tools import assert_equals, assert_raises, assert_not_equals
from keystore import Location from keystore import Location
from keystore.exceptions import InvalidLocationError from keystore.exceptions import InvalidLocationError
...@@ -11,7 +11,6 @@ def check_string_roundtrip(url): ...@@ -11,7 +11,6 @@ def check_string_roundtrip(url):
def test_string_roundtrip(): def test_string_roundtrip():
check_string_roundtrip("tag://org/course/category/name") check_string_roundtrip("tag://org/course/category/name")
check_string_roundtrip("tag://org/course/category/name/revision") check_string_roundtrip("tag://org/course/category/name/revision")
check_string_roundtrip("tag://org/course/category/name with spaces/revision")
def test_dict(): def test_dict():
...@@ -50,3 +49,15 @@ def test_invalid_locations(): ...@@ -50,3 +49,15 @@ def test_invalid_locations():
assert_raises(InvalidLocationError, Location, ["foo", "bar"]) assert_raises(InvalidLocationError, Location, ["foo", "bar"])
assert_raises(InvalidLocationError, Location, ["foo", "bar", "baz", "blat", "foo/bar"]) assert_raises(InvalidLocationError, Location, ["foo", "bar", "baz", "blat", "foo/bar"])
assert_raises(InvalidLocationError, Location, None) assert_raises(InvalidLocationError, Location, None)
assert_raises(InvalidLocationError, Location, "tag://org/course/category/name with spaces/revision")
def test_equality():
assert_equals(
Location('tag', 'org', 'course', 'category', 'name'),
Location('tag', 'org', 'course', 'category', 'name')
)
assert_not_equals(
Location('tag', 'org', 'course', 'category', 'name1'),
Location('tag', 'org', 'course', 'category', 'name')
)
import logging
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
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True))
log = logging.getLogger(__name__)
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(self.data_dir / "course.xml") as course_file:
class ImportSystem(XMLParsingSystem):
def __init__(self, modulestore):
"""
modulestore: the XMLModuleStore to store the loaded modules in
"""
self.unnamed_modules = 0
def process_xml(xml):
try:
xml_data = etree.fromstring(xml)
except:
log.exception("Unable to parse xml: {xml}".format(xml=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, modulestore.default_class)
modulestore.modules[module.location] = module
return module
XMLParsingSystem.__init__(self, modulestore.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]
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 xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
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 ABTestModule(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, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
target_groups = self.definition['data'].keys()
if shared_state is None:
self.group = group_from_value(
self.definition['data']['group_portions'],
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):
print self.group
return json.dumps({'group': self.group})
def displayable_items(self):
return [self.system.get_module(child)
for child
in self.definition['data']['group_content'][self.group]]
class ABTestDescriptor(RawDescriptor, XmlDescriptor):
module_class = ABTestModule
def __init__(self, system, definition=None, **kwargs):
kwargs['shared_state_key'] = definition['data']['experiment']
RawDescriptor.__init__(self, system, definition, **kwargs)
@classmethod
def definition_from_xml(cls, xml_object, system):
experiment = xml_object.get('experiment')
if experiment is None:
raise InvalidDefinitionError("ABTests must specify an experiment. Not found in:\n{xml}".format(xml=etree.tostring(xml_object, pretty_print=True)))
definition = {
'data': {
'experiment': experiment,
'group_portions': [],
'group_content': {None: []},
},
'children': []}
for group in xml_object:
if group.tag == 'default':
name = None
else:
name = group.get('name')
definition['data']['group_portions'].append(
(name, float(group.get('portion', 0)))
)
child_content_urls = [
system.process_xml(etree.tostring(child)).location.url()
for child in group
]
definition['data']['group_content'][name] = child_content_urls
definition['children'].extend(child_content_urls)
default_portion = 1 - sum(portion for (name, portion) in definition['data']['group_portions'])
if default_portion < 0:
raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1")
definition['data']['group_portions'].append((None, default_portion))
return definition
class InvalidDefinitionError(Exception):
pass
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,31 +14,32 @@ log = logging.getLogger("mitx.common.lib.seq_module") ...@@ -13,31 +14,32 @@ 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_state(self): def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
return json.dumps({ 'position':self.position }) XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
self.position = 1
@classmethod if instance_state is not None:
def get_xml_tags(c): state = json.loads(instance_state)
obsolete_tags = ["sequential", 'tab'] if 'position' in state:
modern_tags = ["videosequence"] self.position = int(state['position'])
return obsolete_tags + modern_tags
def get_html(self):
self.render()
return self.content
def get_init_js(self): # if position is specified in system, then use that instead
self.render() if system.get('position'):
return self.init_js self.position = int(system.get('position'))
self.rendered = False
def get_destroy_js(self): def get_instance_state(self):
return json.dumps({'position': self.position})
def get_html(self):
self.render() self.render()
return self.destroy_js return self.content
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.
...@@ -60,78 +62,49 @@ class Module(XModule): ...@@ -60,78 +62,49 @@ 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.metadata['display_name'].strip()
for grand_child in child.get_children()
self.contents = self.rendered_children() if 'display_name' in grand_child.metadata
),
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):
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': self.location.html_id(),
'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)
if self.xmltree.tag == 'tab':
self.content = self.system.render_template('tab_module.html', params)
log.debug("rendered content: %s", content)
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)
self.position = 1
if state is not None:
state = json.loads(state)
if 'position' in state: self.position = int(state['position'])
# if position is specified in system, then use that instead self.content = self.system.render_template('seq_module.html', params)
if system.get('position'): self.rendered = True
self.position = int(system.get('position'))
self.rendered = False 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
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': [
system.process_xml(etree.tostring(child_module)).location.url()
children = [
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,23 @@ setup( ...@@ -13,14 +13,23 @@ 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", "abtest = xmodule.abtest_module:ABTestDescriptor",
"course = seq_module:SequenceDescriptor", "book = xmodule.translation_module:TranslateCustomTagDescriptor",
"html = html_module:HtmlModuleDescriptor", "chapter = xmodule.seq_module:SequenceDescriptor",
"section = translation_module:SemanticSectionDescriptor", "course = xmodule.seq_module:SequenceDescriptor",
"sequential = seq_module:SequenceDescriptor", "customtag = xmodule.template_module:CustomTagDescriptor",
"vertical = seq_module:SequenceDescriptor", "discuss = xmodule.translation_module:TranslateCustomTagDescriptor",
"problemset = seq_module:SequenceDescriptor", "html = xmodule.html_module:HtmlDescriptor",
"videosequence = seq_module:SequenceDescriptor", "image = xmodule.translation_module:TranslateCustomTagDescriptor",
"problem = xmodule.capa_module:CapaDescriptor",
"problemset = xmodule.vertical_module:VerticalDescriptor",
"section = xmodule.translation_module:SemanticSectionDescriptor",
"sequential = xmodule.seq_module:SequenceDescriptor",
"slides = xmodule.translation_module:TranslateCustomTagDescriptor",
"vertical = xmodule.vertical_module:VerticalDescriptor",
"video = xmodule.video_module:VideoDescriptor",
"videodev = xmodule.translation_module:TranslateCustomTagDescriptor",
"videosequence = xmodule.seq_module:SequenceDescriptor",
] ]
} }
) )
import json from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from x_module import XModule, XModuleDescriptor
from lxml import etree from lxml import etree
class ModuleDescriptor(XModuleDescriptor): class CustomTagModule(XModule):
pass
class Module(XModule):
""" """
This module supports tags of the form This module supports tags of the form
<customtag option="val" option2="val2"> <customtag option="val" option2="val2">
...@@ -31,19 +26,16 @@ class Module(XModule): ...@@ -31,19 +26,16 @@ 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 __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
def get_xml_tags(c): XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
return ['customtag'] xmltree = etree.fromstring(self.definition['data'])
filename = xmltree.find('impl').text
params = dict(xmltree.items())
self.html = self.system.render_template(filename, params, namespace='custom_tags')
def get_html(self): def get_html(self):
return self.html return self.html
def __init__(self, system, xml, item_id, state=None): class CustomTagDescriptor(RawDescriptor):
XModule.__init__(self, system, xml, item_id, state) module_class = CustomTagModule
xmltree = etree.fromstring(xml)
filename = xmltree.find('impl').text
params = dict(xmltree.items())
self.html = self.system.render_template(filename, params, namespace='custom_tags')
...@@ -23,7 +23,7 @@ def process_includes(fn): ...@@ -23,7 +23,7 @@ def process_includes(fn):
file = next_include.get('file') file = next_include.get('file')
if file is not None: if file is not None:
try: try:
ifp = system.fs.open(file) ifp = system.resources_fs.open(file)
except Exception: except Exception:
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True))) log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True)))
log.exception('Cannot find file %s in %s' % (file, dir)) log.exception('Cannot find file %s in %s' % (file, dir))
...@@ -57,11 +57,26 @@ class SemanticSectionDescriptor(XModuleDescriptor): ...@@ -57,11 +57,26 @@ class SemanticSectionDescriptor(XModuleDescriptor):
if len(xml_object) == 1: if len(xml_object) == 1:
for (key, val) in xml_object.items(): for (key, val) in xml_object.items():
if key == 'format':
continue
xml_object[0].set(key, val) xml_object[0].set(key, val)
return system.process_xml(etree.tostring(xml_object[0])) return system.process_xml(etree.tostring(xml_object[0]))
else: else:
xml_object.tag = 'sequence' xml_object.tag = 'sequence'
return system.process_xml(etree.tostring(xml_object)) return system.process_xml(etree.tostring(xml_object))
class TranslateCustomTagDescriptor(XModuleDescriptor):
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Transforms the xml_data from <$custom_tag attr="" attr=""/> to
<customtag attr="" attr=""><impl>$custom_tag</impl></customtag>
"""
xml_object = etree.fromstring(xml_data)
tag = xml_object.tag
xml_object.tag = 'customtag'
impl = etree.SubElement(xml_object, 'impl')
impl.text = tag
return system.process_xml(etree.tostring(xml_object))
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): # HACK: This shouldn't be hard-coded to two types
pass # OBSOLETE: This obsoletes 'type'
class_priority = ['video', 'problem']
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): def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
return json.dumps({ }) XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
self.contents = None
@classmethod
def get_xml_tags(c):
return ["vertical", "problemset"]
def get_html(self): def get_html(self):
if self.contents is None:
self.contents = [child.get_html() for child in self.get_display_items()]
return self.system.render_template('vert_module.html', { return self.system.render_template('vert_module.html', {
'items': self.contents 'items': self.contents
}) })
...@@ -30,8 +29,14 @@ class Module(XModule): ...@@ -30,8 +29,14 @@ 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 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())
xmltree=etree.fromstring(xml) new_class = 'other'
self.contents=[(e.get("name"),self.render_function(e)) \ for c in class_priority:
for e in xmltree] if c in child_classes:
new_class = c
return new_class
class VerticalDescriptor(SequenceDescriptor):
module_class = VerticalModule
...@@ -3,17 +3,27 @@ import logging ...@@ -3,17 +3,27 @@ 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
icon_class = 'video'
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 instance_state is not None:
state = json.loads(instance_state)
if 'position' in state:
self.position = int(float(state['position']))
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
''' '''
...@@ -39,14 +49,9 @@ class Module(XModule): ...@@ -39,14 +49,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 +59,11 @@ class Module(XModule): ...@@ -54,27 +59,11 @@ 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.location.html_id(),
'position': self.position, 'position': self.position,
'name': self.name, '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)
self.youtube = xmltree.get('youtube')
self.name = xmltree.get('name')
self.position = 0
if state is not None:
state = json.loads(state)
if 'position' in state:
self.position = int(float(state['position']))
self.annotations=[(e.get("name"),self.render_function(e)) \
for e in xmltree]
class VideoSegmentDescriptor(XModuleDescriptor): class VideoDescriptor(RawDescriptor):
pass module_class = VideoModule
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)
metadata = {}
for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'):
from_xml = xml_object.get(attr)
if from_xml is not None:
metadata[attr] = from_xml
if xml_object.get('graded') is not None:
metadata['graded'] = xml_object.get('graded') == 'true'
if xml_object.get('name') is not None:
metadata['display_name'] = xml_object.get('name')
return cls(
system,
cls.definition_from_xml(xml_object, system),
location=['i4x',
org,
course,
xml_object.tag,
xml_object.get('slug')],
metadata=metadata,
)
from lxml import etree
import random import random
import imp
import logging import logging
import sys
import types
from django.conf import settings from django.conf import settings
...@@ -11,134 +7,119 @@ from courseware.course_settings import course_settings ...@@ -11,134 +7,119 @@ from courseware.course_settings import course_settings
from xmodule import graders from xmodule import graders
from xmodule.graders import Score from xmodule.graders import Score
from models import StudentModule from models import StudentModule
import courseware.content_parser as content_parser
import xmodule
_log = logging.getLogger("mitx.courseware") _log = logging.getLogger("mitx.courseware")
def grade_sheet(student,coursename=None): def grade_sheet(student, course, student_module_cache):
""" """
This pulls a summary of all problems in the course. It returns a dictionary with two datastructures: This pulls a summary of all problems in the course. It returns a dictionary with two datastructures:
- courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters, - courseware_summary is a summary of all sections with problems in the course. It is organized as an array of chapters,
each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded each containing an array of sections, each containing an array of scores. This contains information for graded and ungraded
problems, and is good for displaying a course summary with due dates, etc. problems, and is good for displaying a course summary with due dates, etc.
- grade_summary is the output from the course grader. More information on the format is in the docstring for CourseGrader. - grade_summary is the output from the course grader. More information on the format is in the docstring for CourseGrader.
Arguments:
student: A User object for the student to grade
course: An XModule containing the course to grade
student_module_cache: A StudentModuleCache initialized with all instance_modules for the student
""" """
dom=content_parser.course_file(student,coursename)
course = dom.xpath('//course/@name')[0]
xmlChapters = dom.xpath('//course[@name=$course]/chapter', course=course)
responses=StudentModule.objects.filter(student=student)
response_by_id = {}
for response in responses:
response_by_id[response.module_id] = response
totaled_scores = {} totaled_scores = {}
chapters=[] chapters = []
for c in xmlChapters: for c in course.get_children():
sections = [] sections = []
chname=c.get('name') for s in c.get_children():
def yield_descendents(module):
yield module
for s in dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section', for child in module.get_display_items():
course=course, chname=chname): for module in yield_descendents(child):
problems=dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section[@name=$section]//problem', yield module
course=course, chname=chname, section=s.get('name'))
graded = s.metadata.get('graded', False)
graded = True if s.get('graded') == "true" else False scores = []
scores=[] for module in yield_descendents(s):
if len(problems)>0: (correct, total) = get_score(student, module, student_module_cache)
for p in problems:
(correct,total) = get_score(student, p, response_by_id, coursename=coursename) if correct is None and total is None:
continue
if settings.GENERATE_PROFILE_SCORES:
if total > 1: if settings.GENERATE_PROFILE_SCORES:
correct = random.randrange( max(total-2, 1) , total + 1 ) if total > 1:
else: correct = random.randrange(max(total - 2, 1), total + 1)
correct = total else:
correct = total
if not total > 0:
#We simply cannot grade a problem that is 12/0, because we might need it as a percentage if not total > 0:
graded = False #We simply cannot grade a problem that is 12/0, because we might need it as a percentage
scores.append( Score(correct,total, graded, p.get("name")) ) graded = False
section_total, graded_total = graders.aggregate_scores(scores, s.get("name")) scores.append(Score(correct, total, graded, module.metadata.get('display_name')))
#Add the graded total to totaled_scores
format = s.get('format', "") section_total, graded_total = graders.aggregate_scores(scores, s.metadata.get('display_name'))
subtitle = s.get('subtitle', format) #Add the graded total to totaled_scores
if format and graded_total[1] > 0: format = s.metadata.get('format', "")
format_scores = totaled_scores.get(format, []) if format and graded_total.possible > 0:
format_scores.append( graded_total ) format_scores = totaled_scores.get(format, [])
totaled_scores[ format ] = format_scores format_scores.append(graded_total)
totaled_scores[format] = format_scores
section_score={'section':s.get("name"),
'scores':scores, sections.append({
'section_total' : section_total, 'section': s.metadata.get('display_name'),
'format' : format, 'scores': scores,
'subtitle' : subtitle, 'section_total': section_total,
'due' : s.get("due") or "", 'format': format,
'graded' : graded, 'due': s.metadata.get("due", ""),
} 'graded': graded,
sections.append(section_score) })
chapters.append({'course':course, chapters.append({'course': course.metadata.get('display_name'),
'chapter' : c.get("name"), 'chapter': c.metadata.get('display_name'),
'sections' : sections,}) 'sections': sections})
grader = course_settings.GRADER grader = course_settings.GRADER
grade_summary = grader.grade(totaled_scores) grade_summary = grader.grade(totaled_scores)
return {'courseware_summary' : chapters,
'grade_summary' : grade_summary}
def get_score(user, problem, cache, coursename=None): return {'courseware_summary': chapters,
## HACK: assumes max score is fixed per problem 'grade_summary': grade_summary}
id = problem.get('id')
def get_score(user, problem, cache):
"""
Return the score for a user on a problem
user: a Student object
problem: an XModule
cache: A StudentModuleCache
"""
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: instance_module = cache.lookup(problem.category, problem.id)
module = StudentModule(module_type = 'problem', # TODO: Move into StudentModule.__init__? if instance_module is None:
module_id = id, instance_module = StudentModule(module_type=problem.category,
student = user, module_state_key=problem.id,
state = None, student=user,
grade = 0, state=None,
max_grade = None, grade=0,
done = 'i') max_grade=problem.max_score(),
cache[id] = module done='i')
cache.append(instance_module)
# Grab the # correct from cache instance_module.save()
if id in cache:
response = cache[id] # If this problem is ungraded/ungradable, bail
if response.grade!=None: if instance_module.max_grade is None:
correct=float(response.grade) return (None, None)
# Grab max grade from cache, or if it doesn't exist, compute and save to DB correct = instance_module.grade if instance_module.grade is not None else 0
if id in cache and response.max_grade is not None: total = instance_module.max_grade
total = response.max_grade
else: if correct is not None and total is not None:
## HACK 1: We shouldn't specifically reference capa_module #Now we re-weight the problem, if specified
## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system weight = getattr(problem, 'weight', 1)
# TODO: These are no longer correct params for I4xSystem -- figure out what this code if weight != 1:
# does, clean it up. correct = correct * weight / total
# from module_render import I4xSystem total = weight
# system = I4xSystem(None, None, None, coursename=coursename)
# total=float(xmodule.capa_module.Module(system, etree.tostring(problem), "id").max_score())
# response.max_grade = total
# response.save()
total = 1
# For a temporary fix, we just assume a problem is worth 1 point if we haven't seen it before. This is totally incorrect
#Now we re-weight the problem, if specified
weight = problem.get("weight", None)
if weight:
weight = float(weight)
correct = correct * weight / total
total = weight
return (correct, total) return (correct, total)
...@@ -6,50 +6,36 @@ from django.core.management.base import BaseCommand ...@@ -6,50 +6,36 @@ from django.core.management.base import BaseCommand
from django.conf import settings from django.conf import settings
from django.contrib.auth.models import User from django.contrib.auth.models import User
from courseware.content_parser import course_file
import courseware.module_render
import xmodule import xmodule
import mitxmako.middleware as middleware import mitxmako.middleware as middleware
middleware.MakoMiddleware() middleware.MakoMiddleware()
from keystore.django import keystore
from courseware.models import StudentModuleCache
from courseware.module_render import get_module
def check_names(user, course):
'''
Complain if any problems have non alphanumeric names.
TODO (vshnayder): there are some in 6.002x that don't. Is that actually a problem?
'''
all_ok = True
print "Confirming all problems have alphanumeric names"
for problem in course.xpath('//problem'):
filename = problem.get('filename')
if not filename.isalnum():
print "==============> Invalid (non-alphanumeric) filename", filename
all_ok = False
return all_ok
def check_rendering(user, course): def check_rendering(module):
'''Check that all modules render''' '''Check that all modules render'''
all_ok = True all_ok = True
print "Confirming all modules render. Nothing should print during this step. " print "Confirming all modules render. Nothing should print during this step. "
for module in course.xpath('//problem|//html|//video|//vertical|//sequential|/tab'):
module_class = xmodule.modx_modules[module.tag] def _check_module(module):
# TODO: Abstract this out in render_module.py try:
try: module.get_html()
module_class(etree.tostring(module),
module.get('id'),
ajax_url='',
state=None,
track_function = lambda x,y,z:None,
render_function = lambda x: {'content':'','type':'video'})
except Exception as ex: except Exception as ex:
print "==============> Error in ", etree.tostring(module) print "==============> Error in ", module.id
print "" print ""
print ex print ex
all_ok = False all_ok = False
for child in module.get_children():
_check_module(child)
_check_module(module)
print "Module render check finished" print "Module render check finished"
return all_ok return all_ok
def check_sections(user, course):
def check_sections(course):
all_ok = True all_ok = True
sections_dir = settings.DATA_DIR + "/sections" sections_dir = settings.DATA_DIR + "/sections"
print "Checking that all sections exist and parse properly" print "Checking that all sections exist and parse properly"
...@@ -69,11 +55,13 @@ def check_sections(user, course): ...@@ -69,11 +55,13 @@ def check_sections(user, course):
all_ok = False all_ok = False
print "checked all sections" print "checked all sections"
else: else:
print "Skipping check of include files -- no section includes dir ("+sections_dir+")" print "Skipping check of include files -- no section includes dir (" + sections_dir + ")"
return all_ok return all_ok
class Command(BaseCommand): class Command(BaseCommand):
help = "Does basic validity tests on course.xml." help = "Does basic validity tests on course.xml."
def handle(self, *args, **options): def handle(self, *args, **options):
all_ok = True all_ok = True
...@@ -86,22 +74,25 @@ class Command(BaseCommand): ...@@ -86,22 +74,25 @@ class Command(BaseCommand):
sample_user = User.objects.all()[0] sample_user = User.objects.all()[0]
print "Attempting to load courseware" print "Attempting to load courseware"
course = course_file(sample_user)
# TODO (cpennington): Get coursename in a legitimate way
to_run = [check_names, course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
# TODO (vshnayder) : make check_rendering work (use module_render.py), student_module_cache = StudentModuleCache(sample_user, keystore().get_item(course_location))
# turn it on (course, _, _, _) = get_module(sample_user, None, course_location, student_module_cache)
# check_rendering,
check_sections, to_run = [
] #TODO (vshnayder) : make check_rendering work (use module_render.py),
# turn it on
check_rendering,
check_sections,
]
for check in to_run: for check in to_run:
all_ok = check(sample_user, course) and all_ok all_ok = check(course) and all_ok
# TODO: print "Checking course properly annotated with preprocess.py" # TODO: print "Checking course properly annotated with preprocess.py"
if all_ok: if all_ok:
print 'Courseware passes all checks!' print 'Courseware passes all checks!'
else: else:
print "Courseware fails some checks" print "Courseware fails some checks"
...@@ -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,106 @@ from django.contrib.auth.models import User ...@@ -21,72 +20,106 @@ 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
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)
# This works around a limitation in sqlite3 on the number of parameters
# that can be put into a single query
self.cache = []
chunk_size = 500
for id_chunk in [module_ids[i:i+chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
self.cache.extend(StudentModule.objects.filter(
student=user,
module_state_key__in=id_chunk)
)
# @classmethod else:
# def get_with_caching(cls, student, module_id): self.cache = []
# 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 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.location.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)
...@@ -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",
......
...@@ -174,7 +174,7 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None): ...@@ -174,7 +174,7 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None):
module = 'problem' module = 'problem'
xml = content_parser.module_xml(request.user, module, 'id', id, coursename) xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
ajax_url = settings.MITX_ROOT_URL + '/modx/'+module+'/'+id+'/' ajax_url = settings.MITX_ROOT_URL + '/modx/'+id+'/'
# Create the module (instance of capa_module.Module) # Create the module (instance of capa_module.Module)
system = I4xSystem(track_function = make_track_function(request), system = I4xSystem(track_function = make_track_function(request),
...@@ -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,
......
...@@ -20,8 +20,8 @@ class @Courseware ...@@ -20,8 +20,8 @@ class @Courseware
id = $(this).attr('id').replace(/video_/, '') id = $(this).attr('id').replace(/video_/, '')
new Video id, $(this).data('streams') new Video id, $(this).data('streams')
$('.course-content .problems-wrapper').each -> $('.course-content .problems-wrapper').each ->
id = $(this).attr('id').replace(/problem_/, '') id = $(this).attr('problem-id')
new Problem id, $(this).data('url') new Problem id, $(this).attr('id'), $(this).data('url')
$('.course-content .histogram').each -> $('.course-content .histogram').each ->
id = $(this).attr('id').replace(/histogram_/, '') id = $(this).attr('id').replace(/histogram_/, '')
new Histogram id, $(this).data('histogram') new Histogram id, $(this).data('histogram')
class @Problem class @Problem
constructor: (@id, url) -> constructor: (@id, @element_id, url) ->
@element = $("#problem_#{id}") @element = $("##{element_id}")
@render() @render()
$: (selector) -> $: (selector) ->
...@@ -26,13 +26,13 @@ class @Problem ...@@ -26,13 +26,13 @@ class @Problem
@element.html(content) @element.html(content)
@bind() @bind()
else else
$.postWithPrefix "/modx/problem/#{@id}/problem_get", (response) => $.postWithPrefix "/modx/#{@id}/problem_get", (response) =>
@element.html(response.html) @element.html(response.html)
@bind() @bind()
check: => check: =>
Logger.log 'problem_check', @answers Logger.log 'problem_check', @answers
$.postWithPrefix "/modx/problem/#{@id}/problem_check", @answers, (response) => $.postWithPrefix "/modx/#{@id}/problem_check", @answers, (response) =>
switch response.success switch response.success
when 'incorrect', 'correct' when 'incorrect', 'correct'
@render(response.contents) @render(response.contents)
...@@ -42,14 +42,14 @@ class @Problem ...@@ -42,14 +42,14 @@ class @Problem
reset: => reset: =>
Logger.log 'problem_reset', @answers Logger.log 'problem_reset', @answers
$.postWithPrefix "/modx/problem/#{@id}/problem_reset", id: @id, (response) => $.postWithPrefix "/modx/#{@id}/problem_reset", id: @id, (response) =>
@render(response.html) @render(response.html)
@updateProgress response @updateProgress response
show: => show: =>
if !@element.hasClass 'showed' if !@element.hasClass 'showed'
Logger.log 'problem_show', problem: @id Logger.log 'problem_show', problem: @id
$.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) => $.postWithPrefix "/modx/#{@id}/problem_show", (response) =>
answers = response.answers answers = response.answers
$.each answers, (key, value) => $.each answers, (key, value) =>
if $.isArray(value) if $.isArray(value)
...@@ -69,7 +69,7 @@ class @Problem ...@@ -69,7 +69,7 @@ class @Problem
save: => save: =>
Logger.log 'problem_save', @answers Logger.log 'problem_save', @answers
$.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) => $.postWithPrefix "/modx/#{@id}/problem_save", @answers, (response) =>
if response.success if response.success
alert 'Saved' alert 'Saved'
@updateProgress response @updateProgress response
...@@ -94,4 +94,4 @@ class @Problem ...@@ -94,4 +94,4 @@ class @Problem
element.schematic.update_value() element.schematic.update_value()
@$(".CodeMirror").each (index, element) -> @$(".CodeMirror").each (index, element) ->
element.CodeMirror.save() if element.CodeMirror.save element.CodeMirror.save() if element.CodeMirror.save
@answers = @$("[id^=input_#{@id}_]").serialize() @answers = @$("[id^=input_#{@element_id.replace(/problem_/, '')}_]").serialize()
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()
...@@ -88,7 +88,7 @@ class @Sequence ...@@ -88,7 +88,7 @@ class @Sequence
if @position != new_position if @position != new_position
if @position != undefined if @position != undefined
@mark_visited @position @mark_visited @position
$.postWithPrefix "/modx/#{@tag}/#{@id}/goto_position", position: new_position $.postWithPrefix "/modx/#{@id}/goto_position", position: new_position
@mark_active new_position @mark_active new_position
@$('#seq_content').html @elements[new_position - 1].content @$('#seq_content').html @elements[new_position - 1].content
......
<section id="problem_${id}" class="problems-wrapper" data-url="${ajax_url}"></section> <section id="problem_${element_id}" class="problems-wrapper" problem-id="${id}" data-url="${ajax_url}"></section>
...@@ -156,7 +156,7 @@ $(function() { ...@@ -156,7 +156,7 @@ $(function() {
<h3><a href="${reverse('courseware_section', args=format_url_params([chapter['course'], chapter['chapter'], section['section']])) }"> <h3><a href="${reverse('courseware_section', args=format_url_params([chapter['course'], chapter['chapter'], section['section']])) }">
${ section['section'] }</a> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</h3> ${ section['section'] }</a> ${"({0:.3n}/{1:.3n}) {2}".format( float(earned), float(total), percentageString )}</h3>
${section['subtitle']} ${section['format']}
%if 'due' in section and section['due']!="": %if 'due' in section and section['due']!="":
due ${section['due']} due ${section['due']}
%endif %endif
......
<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>
${module_content}
<div class="staff_info"> <div class="staff_info">
${xml | h} definition = ${definition | h}
metadata = ${metadata | h}
</div> </div>
%if render_histogram: %if render_histogram:
<div id="histogram_${module_id}" class="histogram" data-histogram="${histogram}"></div> <div id="histogram_${element_id}" class="histogram" data-histogram="${histogram}"></div>
%endif %endif
<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>
...@@ -13,11 +13,3 @@ ...@@ -13,11 +13,3 @@
</article> </article>
</div> </div>
</div> </div>
<ol class="video-mod">
% for t in annotations:
<li id="video-${annotations.index(t)}">
${t[1]['content']}
</li>
% endfor
</ol>
...@@ -56,8 +56,7 @@ if settings.COURSEWARE_ENABLED: ...@@ -56,8 +56,7 @@ if settings.COURSEWARE_ENABLED:
url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/$', 'courseware.views.index', name="courseware_chapter"), url(r'^courseware/(?P<course>[^/]*)/(?P<chapter>[^/]*)/$', 'courseware.views.index', name="courseware_chapter"),
url(r'^courseware/(?P<course>[^/]*)/$', 'courseware.views.index', name="courseware_course"), url(r'^courseware/(?P<course>[^/]*)/$', 'courseware.views.index', name="courseware_course"),
url(r'^jumpto/(?P<probname>[^/]+)/$', 'courseware.views.jump_to'), url(r'^jumpto/(?P<probname>[^/]+)/$', 'courseware.views.jump_to'),
url(r'^section/(?P<section>[^/]*)/$', 'courseware.views.render_section'), url(r'^modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
url(r'^modx/(?P<module>[^/]*)/(?P<id>[^/]*)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
url(r'^profile$', 'courseware.views.profile'), url(r'^profile$', 'courseware.views.profile'),
url(r'^profile/(?P<student_id>[^/]*)/$', 'courseware.views.profile'), url(r'^profile/(?P<student_id>[^/]*)/$', 'courseware.views.profile'),
url(r'^change_setting$', 'student.views.change_setting'), url(r'^change_setting$', 'student.views.change_setting'),
......
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