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.
This commit is contained in:
@@ -4,13 +4,8 @@
|
||||
|
||||
from django.core.management.base import BaseCommand, CommandError
|
||||
from keystore.django import keystore
|
||||
from raw_module import RawDescriptor
|
||||
from lxml import etree
|
||||
from fs.osfs import OSFS
|
||||
from mako.lookup import TemplateLookup
|
||||
|
||||
from path import path
|
||||
from x_module import XModuleDescriptor, XMLParsingSystem
|
||||
from keystore.xml import XMLModuleStore
|
||||
|
||||
unnamed_modules = 0
|
||||
|
||||
@@ -27,33 +22,11 @@ class Command(BaseCommand):
|
||||
raise CommandError("import requires 3 arguments: <org> <course> <data directory>")
|
||||
|
||||
org, course, data_dir = args
|
||||
data_dir = path(data_dir)
|
||||
|
||||
class ImportSystem(XMLParsingSystem):
|
||||
def __init__(self):
|
||||
self.load_item = keystore().get_item
|
||||
self.fs = OSFS(data_dir)
|
||||
|
||||
def process_xml(self, xml):
|
||||
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)
|
||||
module_store = XMLModuleStore(org, course, data_dir, 'xmodule.raw_module.RawDescriptor')
|
||||
for module in module_store.modules.itervalues():
|
||||
keystore().create_item(module.location)
|
||||
if 'data' in module.definition:
|
||||
keystore().update_item(module.location, module.definition['data'])
|
||||
if 'children' in module.definition:
|
||||
keystore().update_children(module.location, module.definition['children'])
|
||||
|
||||
@@ -10,7 +10,7 @@ def index(request):
|
||||
# TODO (cpennington): These need to be read in from the active user
|
||||
org = 'mit.edu'
|
||||
course = '6002xs12'
|
||||
name = '6.002 Spring 2012'
|
||||
name = '6.002_Spring_2012'
|
||||
course = keystore().get_item(['i4x', org, course, 'course', name])
|
||||
weeks = course.get_children()
|
||||
return render_to_response('index.html', {'weeks': weeks})
|
||||
@@ -22,7 +22,7 @@ def edit_item(request):
|
||||
return render_to_response('unit.html', {
|
||||
'contents': item.get_html(),
|
||||
'js_module': item.js_module_name(),
|
||||
'type': item.type,
|
||||
'category': item.category,
|
||||
'name': item.name,
|
||||
})
|
||||
|
||||
|
||||
@@ -8,9 +8,13 @@ TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
KEYSTORE = {
|
||||
'default': {
|
||||
'host': 'localhost',
|
||||
'db': 'mongo_base',
|
||||
'collection': 'key_store',
|
||||
'ENGINE': 'keystore.mongo.MongoModuleStore',
|
||||
'OPTIONS': {
|
||||
'default_class': 'xmodule.raw_module.RawDescriptor',
|
||||
'host': 'localhost',
|
||||
'db': 'mongo_base',
|
||||
'collection': 'key_store',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<header>
|
||||
<section>
|
||||
<h1 class="editable">${name}</h1>
|
||||
<p>${type}</p>
|
||||
<p>${category}</p>
|
||||
</section>
|
||||
|
||||
<div class="actions">
|
||||
|
||||
@@ -33,8 +33,8 @@
|
||||
</section>
|
||||
</section>
|
||||
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${module.definition['data']['text']}</textarea>
|
||||
<div class="preview">${module.definition['data']['text']}</div>
|
||||
<textarea name="" class="edit-box" rows="8" cols="40">${module.definition['data']}</textarea>
|
||||
<div class="preview">${module.definition['data']}</div>
|
||||
|
||||
<div class="actions wip">
|
||||
<a href="" class="save-update">Save & Update</a>
|
||||
|
||||
@@ -38,10 +38,10 @@
|
||||
% for week in weeks:
|
||||
<li>
|
||||
<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>
|
||||
% if week.goals:
|
||||
% for goal in week.goals:
|
||||
% if 'goals' in week.metadata:
|
||||
% for goal in week.metadata['goals']:
|
||||
<li class="goal editable">${goal}</li>
|
||||
% endfor
|
||||
% else:
|
||||
@@ -52,8 +52,8 @@
|
||||
|
||||
<ul>
|
||||
% for module in week.get_children():
|
||||
<li class="${module.type}">
|
||||
<a href="#" class="module-edit" id="${module.url}">${module.name}</a>
|
||||
<li class="${module.category}">
|
||||
<a href="#" class="module-edit" id="${module.location.url()}">${module.name}</a>
|
||||
<a href="#" class="draggable">handle</a>
|
||||
</li>
|
||||
% endfor
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<ol>
|
||||
% for child in module.get_children():
|
||||
<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>
|
||||
</li>
|
||||
%endfor
|
||||
|
||||
@@ -4,6 +4,7 @@ that are stored in a database an accessible using their Location as an identifie
|
||||
"""
|
||||
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from .exceptions import InvalidLocationError
|
||||
|
||||
URL_RE = re.compile("""
|
||||
@@ -15,8 +16,10 @@ URL_RE = re.compile("""
|
||||
(/(?P<revision>[^/]+))?
|
||||
""", 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.
|
||||
|
||||
@@ -26,7 +29,16 @@ class Location(object):
|
||||
However, they can also be represented a dictionaries (specifying each component),
|
||||
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.
|
||||
|
||||
@@ -45,55 +57,55 @@ class Location(object):
|
||||
|
||||
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
|
||||
wildcard selection
|
||||
"""
|
||||
self.update(location)
|
||||
|
||||
def update(self, location):
|
||||
"""
|
||||
Update this instance with data from another Location object.
|
||||
if org is None and course is None and category is None and name is None and revision is None:
|
||||
location = loc_or_tag
|
||||
else:
|
||||
location = (loc_or_tag, org, course, category, name, revision)
|
||||
|
||||
location: can take the same forms as specified by `__init__`
|
||||
"""
|
||||
self.tag = self.org = self.course = self.category = self.name = self.revision = None
|
||||
def check_dict(dict_):
|
||||
check_list(dict_.values())
|
||||
|
||||
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):
|
||||
match = URL_RE.match(location)
|
||||
if match is None:
|
||||
raise InvalidLocationError(location)
|
||||
else:
|
||||
self.update(match.groupdict())
|
||||
elif isinstance(location, list):
|
||||
groups = match.groupdict()
|
||||
check_dict(groups)
|
||||
return _LocationBase.__new__(_cls, **groups)
|
||||
elif isinstance(location, (list, tuple)):
|
||||
if len(location) not in (5, 6):
|
||||
raise InvalidLocationError(location)
|
||||
|
||||
(self.tag, self.org, self.course, self.category, self.name) = location[0:5]
|
||||
self.revision = location[5] if len(location) == 6 else None
|
||||
if len(location) == 5:
|
||||
args = tuple(location) + (None, )
|
||||
else:
|
||||
args = tuple(location)
|
||||
|
||||
check_list(args)
|
||||
return _LocationBase.__new__(_cls, *args)
|
||||
elif isinstance(location, dict):
|
||||
try:
|
||||
self.tag = location['tag']
|
||||
self.org = location['org']
|
||||
self.course = location['course']
|
||||
self.category = location['category']
|
||||
self.name = location['name']
|
||||
except KeyError:
|
||||
raise InvalidLocationError(location)
|
||||
self.revision = location.get('revision')
|
||||
kwargs = dict(location)
|
||||
kwargs.setdefault('revision', None)
|
||||
|
||||
check_dict(kwargs)
|
||||
return _LocationBase.__new__(_cls, **kwargs)
|
||||
elif isinstance(location, Location):
|
||||
self.update(location.list())
|
||||
return _LocationBase.__new__(_cls, location)
|
||||
else:
|
||||
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):
|
||||
"""
|
||||
Return a string containing the URL for this location
|
||||
@@ -103,22 +115,23 @@ class Location(object):
|
||||
url += "/" + self.revision
|
||||
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):
|
||||
"""
|
||||
Return a dictionary representing this location
|
||||
"""
|
||||
return {'tag': self.tag,
|
||||
'org': self.org,
|
||||
'course': self.course,
|
||||
'category': self.category,
|
||||
'name': self.name,
|
||||
'revision': self.revision}
|
||||
return self.__dict__
|
||||
|
||||
def list(self):
|
||||
return list(self)
|
||||
|
||||
def __str__(self):
|
||||
return self.url()
|
||||
|
||||
def __repr__(self):
|
||||
return "Location%s" % repr(tuple(self))
|
||||
|
||||
|
||||
class ModuleStore(object):
|
||||
|
||||
@@ -6,9 +6,9 @@ Passes settings.KEYSTORE as kwargs to MongoModuleStore
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
from django.conf import settings
|
||||
from .mongo import MongoModuleStore
|
||||
from raw_module import RawDescriptor
|
||||
|
||||
_KEYSTORES = {}
|
||||
|
||||
@@ -17,9 +17,10 @@ def keystore(name='default'):
|
||||
global _KEYSTORES
|
||||
|
||||
if name not in _KEYSTORES:
|
||||
# TODO (cpennington): Load the default class from a string
|
||||
_KEYSTORES[name] = MongoModuleStore(
|
||||
default_class=RawDescriptor,
|
||||
**settings.KEYSTORE[name])
|
||||
class_path = settings.KEYSTORE[name]['ENGINE']
|
||||
module_path, _, class_name = class_path.rpartition('.')
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
_KEYSTORES[name] = class_(
|
||||
**settings.KEYSTORE[name]['OPTIONS'])
|
||||
|
||||
return _KEYSTORES[name]
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import pymongo
|
||||
from importlib import import_module
|
||||
from xmodule.x_module import XModuleDescriptor, DescriptorSystem
|
||||
|
||||
from . import ModuleStore, Location
|
||||
from .exceptions import ItemNotFoundError, InsufficientSpecificationError
|
||||
from xmodule.x_module import XModuleDescriptor, DescriptorSystem
|
||||
|
||||
|
||||
class MongoModuleStore(ModuleStore):
|
||||
@@ -16,7 +18,10 @@ class MongoModuleStore(ModuleStore):
|
||||
|
||||
# Force mongo to report errors, at the expense of performance
|
||||
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):
|
||||
"""
|
||||
@@ -29,8 +34,6 @@ class MongoModuleStore(ModuleStore):
|
||||
If no object is found at that location, raises keystore.exceptions.ItemNotFoundError
|
||||
|
||||
location: Something that can be passed to Location
|
||||
default_class: An XModuleDescriptor subclass to use if no plugin matching the
|
||||
location is found
|
||||
"""
|
||||
|
||||
query = {}
|
||||
@@ -48,8 +51,9 @@ class MongoModuleStore(ModuleStore):
|
||||
if item is None:
|
||||
raise ItemNotFoundError(location)
|
||||
|
||||
# TODO (cpennington): Pass a proper resources_fs to the system
|
||||
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):
|
||||
"""
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
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.exceptions import InvalidLocationError
|
||||
|
||||
@@ -11,7 +11,6 @@ def check_string_roundtrip(url):
|
||||
def test_string_roundtrip():
|
||||
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 with spaces/revision")
|
||||
|
||||
|
||||
def test_dict():
|
||||
@@ -50,3 +49,15 @@ def test_invalid_locations():
|
||||
assert_raises(InvalidLocationError, Location, ["foo", "bar"])
|
||||
assert_raises(InvalidLocationError, Location, ["foo", "bar", "baz", "blat", "foo/bar"])
|
||||
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')
|
||||
)
|
||||
|
||||
96
common/lib/keystore/xml.py
Normal file
96
common/lib/keystore/xml.py
Normal file
@@ -0,0 +1,96 @@
|
||||
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")
|
||||
@@ -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))
|
||||
|
||||
|
||||
118
common/lib/xmodule/abtest_module.py
Normal file
118
common/lib/xmodule/abtest_module.py
Normal file
@@ -0,0 +1,118 @@
|
||||
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
|
||||
@@ -10,8 +10,8 @@ import StringIO
|
||||
from datetime import timedelta
|
||||
from lxml import etree
|
||||
|
||||
from x_module import XModule
|
||||
from mako_module import MakoModuleDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from progress import Progress
|
||||
from capa.capa_problem import LoncapaProblem
|
||||
from capa.responsetypes import StudentInputError
|
||||
@@ -64,44 +64,126 @@ class ComplexEncoder(json.JSONEncoder):
|
||||
return json.JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
class CapaModuleDescriptor(MakoModuleDescriptor):
|
||||
"""
|
||||
Module implementing problems in the LON-CAPA format,
|
||||
as implemented by capa.capa_problem
|
||||
"""
|
||||
|
||||
mako_template = 'widgets/problem-edit.html'
|
||||
|
||||
|
||||
|
||||
class Module(XModule):
|
||||
class CapaModule(XModule):
|
||||
''' Interface between capa_problem and x_module. Originally a hack
|
||||
meant to be refactored out, but it seems to be serving a useful
|
||||
prupose now. We can e.g .destroy and create the capa_problem on a
|
||||
reset.
|
||||
'''
|
||||
icon_class = 'problem'
|
||||
|
||||
id_attribute = "filename"
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ["problem"]
|
||||
self.attempts = 0
|
||||
self.max_attempts = None
|
||||
|
||||
dom2 = etree.fromstring(definition['data'])
|
||||
|
||||
def get_state(self):
|
||||
self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'),
|
||||
default="closed")
|
||||
# TODO: Should be converted to: self.explanation=only_one(dom2.xpath('/problem/@explain'), default="closed")
|
||||
self.explain_available = only_one(dom2.xpath('/problem/@explain_available'))
|
||||
|
||||
display_due_date_string = self.metadata.get('due', None)
|
||||
if display_due_date_string is not None:
|
||||
self.display_due_date = dateutil.parser.parse(display_due_date_string)
|
||||
#log.debug("Parsed " + display_due_date_string + " to " + str(self.display_due_date))
|
||||
else:
|
||||
self.display_due_date = None
|
||||
|
||||
grace_period_string = self.metadata.get('graceperiod', None)
|
||||
if grace_period_string is not None and self.display_due_date:
|
||||
self.grace_period = parse_timedelta(grace_period_string)
|
||||
self.close_date = self.display_due_date + self.grace_period
|
||||
#log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date))
|
||||
else:
|
||||
self.grace_period = None
|
||||
self.close_date = self.display_due_date
|
||||
|
||||
self.max_attempts = only_one(dom2.xpath('/problem/@attempts'))
|
||||
if len(self.max_attempts) > 0:
|
||||
self.max_attempts = int(self.max_attempts)
|
||||
else:
|
||||
self.max_attempts = None
|
||||
|
||||
self.show_answer = self.metadata.get('showanwser', 'closed')
|
||||
|
||||
if self.show_answer == "":
|
||||
self.show_answer = "closed"
|
||||
|
||||
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"
|
||||
self.name = only_one(dom2.xpath('/problem/@name'))
|
||||
|
||||
weight_string = only_one(dom2.xpath('/problem/@weight'))
|
||||
if weight_string:
|
||||
self.weight = float(weight_string)
|
||||
else:
|
||||
self.weight = 1
|
||||
|
||||
if self.rerandomize == 'never':
|
||||
seed = 1
|
||||
elif self.rerandomize == "per_student" and hasattr(system, 'id'):
|
||||
seed = system.id
|
||||
else:
|
||||
seed = None
|
||||
try:
|
||||
fp = self.system.filestore.open(self.filename)
|
||||
except Exception:
|
||||
log.exception('cannot open file %s' % self.filename)
|
||||
if self.system.DEBUG:
|
||||
# create a dummy problem instead of failing
|
||||
fp = StringIO.StringIO('<problem><text><font color="red" size="+2">Problem file %s is missing</font></text></problem>' % self.filename)
|
||||
fp.name = "StringIO"
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
self.lcp = LoncapaProblem(fp, self.location.html_id(), instance_state, seed=seed, system=self.system)
|
||||
except Exception:
|
||||
msg = 'cannot create LoncapaProblem %s' % self.filename
|
||||
log.exception(msg)
|
||||
if self.system.DEBUG:
|
||||
msg = '<p>%s</p>' % msg.replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
# create a dummy problem with error message instead of failing
|
||||
fp = StringIO.StringIO('<problem><text><font color="red" size="+2">Problem file %s has an error:</font>%s</text></problem>' % (self.filename, msg))
|
||||
fp.name = "StringIO"
|
||||
self.lcp = LoncapaProblem(fp, self.location.html_id(), instance_state, seed=seed, system=self.system)
|
||||
else:
|
||||
raise
|
||||
|
||||
@property
|
||||
def rerandomize(self):
|
||||
"""
|
||||
Property accessor that returns self.metadata['rerandomize'] in a canonical form
|
||||
"""
|
||||
rerandomize = self.metadata.get('rerandomize', 'always')
|
||||
if rerandomize in ("", "always", "true"):
|
||||
return "always"
|
||||
elif rerandomize in ("false", "per_student"):
|
||||
return "per_student"
|
||||
elif rerandomize == "never":
|
||||
return "never"
|
||||
else:
|
||||
raise Exception("Invalid rerandomize attribute " + rerandomize)
|
||||
|
||||
def get_instance_state(self):
|
||||
state = self.lcp.get_state()
|
||||
state['attempts'] = self.attempts
|
||||
return json.dumps(state)
|
||||
|
||||
|
||||
def get_score(self):
|
||||
return self.lcp.get_score()
|
||||
|
||||
|
||||
def max_score(self):
|
||||
return self.lcp.get_max_score()
|
||||
|
||||
|
||||
def get_progress(self):
|
||||
''' For now, just return score / max_score
|
||||
'''
|
||||
@@ -112,14 +194,13 @@ class Module(XModule):
|
||||
return Progress(score, total)
|
||||
return None
|
||||
|
||||
|
||||
def get_html(self):
|
||||
return self.system.render_template('problem_ajax.html', {
|
||||
'id': self.item_id,
|
||||
'ajax_url': self.ajax_url,
|
||||
'element_id': self.location.html_id(),
|
||||
'id': self.id,
|
||||
'ajax_url': self.system.ajax_url,
|
||||
})
|
||||
|
||||
|
||||
def get_problem_html(self, encapsulate=True):
|
||||
'''Return html for the problem. Adds check, reset, save buttons
|
||||
as necessary based on the problem config and state.'''
|
||||
@@ -172,12 +253,12 @@ class Module(XModule):
|
||||
explain = False
|
||||
|
||||
context = {'problem': content,
|
||||
'id': self.item_id,
|
||||
'id': self.id,
|
||||
'check_button': check_button,
|
||||
'reset_button': reset_button,
|
||||
'save_button': save_button,
|
||||
'answer_available': self.answer_available(),
|
||||
'ajax_url': self.ajax_url,
|
||||
'ajax_url': self.system.ajax_url,
|
||||
'attempts_used': self.attempts,
|
||||
'attempts_allowed': self.max_attempts,
|
||||
'explain': explain,
|
||||
@@ -187,100 +268,10 @@ class Module(XModule):
|
||||
html = self.system.render_template('problem.html', context)
|
||||
if encapsulate:
|
||||
html = '<div id="problem_{id}" class="problem" data-url="{ajax_url}">'.format(
|
||||
id=self.item_id, ajax_url=self.ajax_url) + html + "</div>"
|
||||
id=self.location.html_id(), ajax_url=self.system.ajax_url) + html + "</div>"
|
||||
|
||||
return html
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
|
||||
self.attempts = 0
|
||||
self.max_attempts = None
|
||||
|
||||
dom2 = etree.fromstring(xml)
|
||||
|
||||
self.explanation = "problems/" + only_one(dom2.xpath('/problem/@explain'),
|
||||
default="closed")
|
||||
# TODO: Should be converted to: self.explanation=only_one(dom2.xpath('/problem/@explain'), default="closed")
|
||||
self.explain_available = only_one(dom2.xpath('/problem/@explain_available'))
|
||||
|
||||
display_due_date_string = only_one(dom2.xpath('/problem/@due'))
|
||||
if len(display_due_date_string) > 0:
|
||||
self.display_due_date = dateutil.parser.parse(display_due_date_string)
|
||||
#log.debug("Parsed " + display_due_date_string + " to " + str(self.display_due_date))
|
||||
else:
|
||||
self.display_due_date = None
|
||||
|
||||
grace_period_string = only_one(dom2.xpath('/problem/@graceperiod'))
|
||||
if len(grace_period_string) >0 and self.display_due_date:
|
||||
self.grace_period = parse_timedelta(grace_period_string)
|
||||
self.close_date = self.display_due_date + self.grace_period
|
||||
#log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date))
|
||||
else:
|
||||
self.grace_period = None
|
||||
self.close_date = self.display_due_date
|
||||
|
||||
self.max_attempts = only_one(dom2.xpath('/problem/@attempts'))
|
||||
if len(self.max_attempts) > 0:
|
||||
self.max_attempts = int(self.max_attempts)
|
||||
else:
|
||||
self.max_attempts = None
|
||||
|
||||
self.show_answer = only_one(dom2.xpath('/problem/@showanswer'))
|
||||
|
||||
if self.show_answer == "":
|
||||
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"
|
||||
else:
|
||||
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']
|
||||
|
||||
# TODO: Should be: self.filename=only_one(dom2.xpath('/problem/@filename'))
|
||||
self.filename= "problems/"+only_one(dom2.xpath('/problem/@filename'))+".xml"
|
||||
self.name=only_one(dom2.xpath('/problem/@name'))
|
||||
self.weight=only_one(dom2.xpath('/problem/@weight'))
|
||||
if self.rerandomize == 'never':
|
||||
seed = 1
|
||||
elif self.rerandomize == "per_student" and hasattr(system, 'id'):
|
||||
seed = system.id
|
||||
else:
|
||||
seed = None
|
||||
try:
|
||||
fp = self.filestore.open(self.filename)
|
||||
except Exception,err:
|
||||
log.exception('[courseware.capa.capa_module.Module.init] error %s: cannot open file %s' % (err,self.filename))
|
||||
if self.DEBUG:
|
||||
# create a dummy problem instead of failing
|
||||
fp = StringIO.StringIO('<problem><text><font color="red" size="+2">Problem file %s is missing</font></text></problem>' % self.filename)
|
||||
fp.name = "StringIO"
|
||||
else:
|
||||
raise
|
||||
try:
|
||||
self.lcp=LoncapaProblem(fp, self.item_id, 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)
|
||||
if self.DEBUG:
|
||||
msg = '<p>%s</p>' % msg.replace('<','<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','<')
|
||||
# create a dummy problem with error message instead of failing
|
||||
fp = StringIO.StringIO('<problem><text><font color="red" size="+2">Problem file %s has an error:</font>%s</text></problem>' % (self.filename,msg))
|
||||
fp.name = "StringIO"
|
||||
self.lcp=LoncapaProblem(fp, self.item_id, state, seed = seed, system=self.system)
|
||||
else:
|
||||
raise
|
||||
|
||||
def handle_ajax(self, dispatch, get):
|
||||
'''
|
||||
This is called by courseware.module_render, to handle an AJAX call.
|
||||
@@ -306,8 +297,8 @@ class Module(XModule):
|
||||
d = handlers[dispatch](get)
|
||||
after = self.get_progress()
|
||||
d.update({
|
||||
'progress_changed' : after != before,
|
||||
'progress_status' : Progress.to_js_status_str(after),
|
||||
'progress_changed': after != before,
|
||||
'progress_status': Progress.to_js_status_str(after),
|
||||
})
|
||||
return json.dumps(d, cls=ComplexEncoder)
|
||||
|
||||
@@ -320,7 +311,6 @@ class Module(XModule):
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def answer_available(self):
|
||||
''' Is the user allowed to see an answer?
|
||||
'''
|
||||
@@ -341,7 +331,8 @@ class Module(XModule):
|
||||
|
||||
if self.show_answer == 'always':
|
||||
return True
|
||||
raise self.system.exception404 #TODO: Not 404
|
||||
#TODO: Not 404
|
||||
raise self.system.exception404
|
||||
|
||||
def get_answer(self, get):
|
||||
'''
|
||||
@@ -355,8 +346,7 @@ class Module(XModule):
|
||||
raise self.system.exception404
|
||||
else:
|
||||
answers = self.lcp.get_question_answers()
|
||||
return {'answers' : answers}
|
||||
|
||||
return {'answers': answers}
|
||||
|
||||
# Figure out if we should move these to capa_problem?
|
||||
def get_problem(self, get):
|
||||
@@ -365,8 +355,8 @@ class Module(XModule):
|
||||
|
||||
Used if we want to reconfirm we have the right thing e.g. after
|
||||
several AJAX calls.
|
||||
'''
|
||||
return {'html' : self.get_problem_html(encapsulate=False)}
|
||||
'''
|
||||
return {'html': self.get_problem_html(encapsulate=False)}
|
||||
|
||||
@staticmethod
|
||||
def make_dict_of_responses(get):
|
||||
@@ -399,7 +389,7 @@ class Module(XModule):
|
||||
# Too late. Cannot submit
|
||||
if self.closed():
|
||||
event_info['failure'] = 'closed'
|
||||
self.tracker('save_problem_check_fail', event_info)
|
||||
self.system.track_function('save_problem_check_fail', event_info)
|
||||
# TODO (vshnayder): probably not 404?
|
||||
raise self.system.exception404
|
||||
|
||||
@@ -407,7 +397,7 @@ class Module(XModule):
|
||||
# again.
|
||||
if self.lcp.done and self.rerandomize == "always":
|
||||
event_info['failure'] = 'unreset'
|
||||
self.tracker('save_problem_check_fail', event_info)
|
||||
self.system.track_function('save_problem_check_fail', event_info)
|
||||
raise self.system.exception404
|
||||
|
||||
try:
|
||||
@@ -416,18 +406,16 @@ class Module(XModule):
|
||||
correct_map = self.lcp.grade_answers(answers)
|
||||
except StudentInputError as inst:
|
||||
# TODO (vshnayder): why is this line here?
|
||||
self.lcp = LoncapaProblem(self.filestore.open(self.filename),
|
||||
self.lcp = LoncapaProblem(self.system.filestore.open(self.filename),
|
||||
id=lcp_id, state=old_state, system=self.system)
|
||||
traceback.print_exc()
|
||||
return {'success': inst.message}
|
||||
except:
|
||||
# TODO: why is this line here?
|
||||
self.lcp = LoncapaProblem(self.filestore.open(self.filename),
|
||||
self.lcp = LoncapaProblem(self.system.filestore.open(self.filename),
|
||||
id=lcp_id, state=old_state, system=self.system)
|
||||
traceback.print_exc()
|
||||
raise Exception,"error in capa_module"
|
||||
# TODO: Dead code... is this a bug, or just old?
|
||||
return {'success':'Unknown Error'}
|
||||
raise Exception("error in capa_module")
|
||||
|
||||
self.attempts = self.attempts + 1
|
||||
self.lcp.done = True
|
||||
@@ -438,21 +426,18 @@ class Module(XModule):
|
||||
if not correct_map.is_correct(answer_id):
|
||||
success = 'incorrect'
|
||||
|
||||
event_info['correct_map'] = correct_map.get_dict() # log this in the tracker
|
||||
# log this in the track_function
|
||||
event_info['correct_map'] = correct_map.get_dict()
|
||||
event_info['success'] = success
|
||||
self.tracker('save_problem_check', event_info)
|
||||
self.system.track_function('save_problem_check', event_info)
|
||||
|
||||
try:
|
||||
html = self.get_problem_html(encapsulate=False) # render problem into HTML
|
||||
except Exception,err:
|
||||
log.error('failed to generate html')
|
||||
raise
|
||||
# render problem into HTML
|
||||
html = self.get_problem_html(encapsulate=False)
|
||||
|
||||
return {'success': success,
|
||||
'contents': html,
|
||||
}
|
||||
|
||||
|
||||
def save_problem(self, get):
|
||||
'''
|
||||
Save the passed in answers.
|
||||
@@ -469,7 +454,7 @@ class Module(XModule):
|
||||
# Too late. Cannot submit
|
||||
if self.closed():
|
||||
event_info['failure'] = 'closed'
|
||||
self.tracker('save_problem_fail', event_info)
|
||||
self.system.track_function('save_problem_fail', event_info)
|
||||
return {'success': False,
|
||||
'error': "Problem is closed"}
|
||||
|
||||
@@ -477,14 +462,14 @@ class Module(XModule):
|
||||
# again.
|
||||
if self.lcp.done and self.rerandomize == "always":
|
||||
event_info['failure'] = 'done'
|
||||
self.tracker('save_problem_fail', event_info)
|
||||
return {'success' : False,
|
||||
'error' : "Problem needs to be reset prior to save."}
|
||||
self.system.track_function('save_problem_fail', event_info)
|
||||
return {'success': False,
|
||||
'error': "Problem needs to be reset prior to save."}
|
||||
|
||||
self.lcp.student_answers = answers
|
||||
|
||||
# TODO: should this be save_problem_fail? Looks like success to me...
|
||||
self.tracker('save_problem_fail', event_info)
|
||||
self.system.track_function('save_problem_fail', event_info)
|
||||
return {'success': True}
|
||||
|
||||
def reset_problem(self, get):
|
||||
@@ -492,30 +477,39 @@ class Module(XModule):
|
||||
and causes problem to rerender itself.
|
||||
|
||||
Returns problem html as { 'html' : html-string }.
|
||||
'''
|
||||
'''
|
||||
event_info = dict()
|
||||
event_info['old_state'] = self.lcp.get_state()
|
||||
event_info['filename'] = self.filename
|
||||
|
||||
if self.closed():
|
||||
event_info['failure'] = 'closed'
|
||||
self.tracker('reset_problem_fail', event_info)
|
||||
self.system.track_function('reset_problem_fail', event_info)
|
||||
return "Problem is closed"
|
||||
|
||||
if not self.lcp.done:
|
||||
event_info['failure'] = 'not_done'
|
||||
self.tracker('reset_problem_fail', event_info)
|
||||
self.system.track_function('reset_problem_fail', event_info)
|
||||
return "Refresh the page and make an attempt before resetting."
|
||||
|
||||
self.lcp.do_reset()
|
||||
if self.rerandomize == "always":
|
||||
# reset random number generator seed (note the self.lcp.get_state() in next line)
|
||||
self.lcp.seed=None
|
||||
|
||||
self.lcp = LoncapaProblem(self.filestore.open(self.filename),
|
||||
self.item_id, self.lcp.get_state(), system=self.system)
|
||||
self.lcp.seed = None
|
||||
|
||||
self.lcp = LoncapaProblem(self.system.filestore.open(self.filename),
|
||||
self.location.html_id(), self.lcp.get_state(), system=self.system)
|
||||
|
||||
event_info['new_state'] = self.lcp.get_state()
|
||||
self.tracker('reset_problem', event_info)
|
||||
self.system.track_function('reset_problem', event_info)
|
||||
|
||||
return {'html' : self.get_problem_html(encapsulate=False)}
|
||||
return {'html': self.get_problem_html(encapsulate=False)}
|
||||
|
||||
|
||||
class CapaDescriptor(RawDescriptor):
|
||||
"""
|
||||
Module implementing problems in the LON-CAPA format,
|
||||
as implemented by capa.capa_problem
|
||||
"""
|
||||
|
||||
module_class = CapaModule
|
||||
|
||||
2
common/lib/xmodule/exceptions.py
Normal file
2
common/lib/xmodule/exceptions.py
Normal file
@@ -0,0 +1,2 @@
|
||||
class InvalidDefinitionError(Exception):
|
||||
pass
|
||||
10
common/lib/xmodule/hidden_module.py
Normal file
10
common/lib/xmodule/hidden_module.py
Normal file
@@ -0,0 +1,10 @@
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
|
||||
|
||||
class HiddenModule(XModule):
|
||||
pass
|
||||
|
||||
|
||||
class HiddenDescriptor(RawDescriptor):
|
||||
module_class = HiddenModule
|
||||
@@ -1,75 +1,34 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from x_module import XModule
|
||||
from mako_module import MakoModuleDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from lxml import etree
|
||||
from pkg_resources import resource_string
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
class HtmlModuleDescriptor(MakoModuleDescriptor):
|
||||
class HtmlModule(XModule):
|
||||
def get_html(self):
|
||||
return self.html
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
|
||||
self.html = self.definition['data']['text']
|
||||
|
||||
|
||||
class HtmlDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
"""
|
||||
Module for putting raw html in a course
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = HtmlModule
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]}
|
||||
js_module = 'HTML'
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
Creates an instance of this descriptor from the supplied xml_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
xml_data: A string of xml that will be translated into data and children for
|
||||
this module
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
"""
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
return cls(
|
||||
system,
|
||||
definition={'data': {'text': xml_data}},
|
||||
location=['i4x',
|
||||
org,
|
||||
course,
|
||||
xml_object.tag,
|
||||
xml_object.get('name')]
|
||||
)
|
||||
|
||||
class Module(XModule):
|
||||
id_attribute = 'filename'
|
||||
|
||||
def get_state(self):
|
||||
return json.dumps({ })
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ["html"]
|
||||
|
||||
def get_html(self):
|
||||
if self.filename==None:
|
||||
xmltree=etree.fromstring(self.xml)
|
||||
textlist=[xmltree.text]+[etree.tostring(i) for i in xmltree]+[xmltree.tail]
|
||||
textlist=[i for i in textlist if type(i)==str]
|
||||
return "".join(textlist)
|
||||
try:
|
||||
filename="html/"+self.filename
|
||||
return self.filestore.open(filename).read()
|
||||
except: # For backwards compatibility. TODO: Remove
|
||||
if self.DEBUG:
|
||||
log.info('[courseware.modules.html_module] filename=%s' % self.filename)
|
||||
return self.system.render_template(self.filename, {'id': self.item_id}, namespace='course')
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
xmltree=etree.fromstring(xml)
|
||||
self.filename = None
|
||||
filename_l=xmltree.xpath("/html/@filename")
|
||||
if len(filename_l)>0:
|
||||
self.filename=str(filename_l[0])
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return {'data': {'text': etree.tostring(xml_object)}}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
from pkg_resources import resource_string
|
||||
from mako_module import MakoModuleDescriptor
|
||||
from lxml import etree
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
|
||||
class RawDescriptor(MakoModuleDescriptor):
|
||||
class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
"""
|
||||
Module that provides a raw editing view of it's data and children
|
||||
"""
|
||||
@@ -18,24 +19,5 @@ class RawDescriptor(MakoModuleDescriptor):
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
Creates an instance of this descriptor from the supplied xml_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
xml_data: A string of xml that will be translated into data and children for
|
||||
this module
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
org and course are optional strings that will be used in the generated modules
|
||||
url identifiers
|
||||
"""
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
return cls(
|
||||
system,
|
||||
definition={'data': xml_data},
|
||||
location=['i4x',
|
||||
org,
|
||||
course,
|
||||
xml_object.tag,
|
||||
xml_object.get('name')]
|
||||
)
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return {'data': etree.tostring(xml_object)}
|
||||
|
||||
@@ -6,18 +6,5 @@ class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
class Module(XModule):
|
||||
id_attribute = 'id'
|
||||
|
||||
def get_state(self):
|
||||
return json.dumps({ })
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ["schematic"]
|
||||
|
||||
def get_html(self):
|
||||
return '<input type="hidden" class="schematic" name="{item_id}" height="480" width="640">'.format(item_id=self.item_id)
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
|
||||
|
||||
@@ -3,8 +3,9 @@ import logging
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from x_module import XModule
|
||||
from mako_module import MakoModuleDescriptor
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.progress import Progress
|
||||
|
||||
log = logging.getLogger("mitx.common.lib.seq_module")
|
||||
@@ -13,32 +14,33 @@ 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_state(self):
|
||||
return json.dumps({ 'position':self.position })
|
||||
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 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'):
|
||||
self.position = int(system.get('position'))
|
||||
|
||||
self.rendered = False
|
||||
|
||||
def get_instance_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,78 +62,49 @@ 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]
|
||||
|
||||
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
|
||||
contents = []
|
||||
for child in self.get_display_items():
|
||||
progress = child.get_progress()
|
||||
contents.append({
|
||||
'content': child.get_html(),
|
||||
'title': "\n".join(
|
||||
grand_child.metadata['display_name'].strip()
|
||||
for grand_child in child.get_children()
|
||||
if 'display_name' in grand_child.metadata
|
||||
),
|
||||
'progress_status': Progress.to_js_status_str(progress),
|
||||
'progress_detail': Progress.to_js_detail_str(progress),
|
||||
'type': child.get_icon_class(),
|
||||
})
|
||||
|
||||
# Split </script> tags -- browsers handle this as end
|
||||
# of script, even if it occurs mid-string. Do this after json.dumps()ing
|
||||
# so that we can be sure of the quotations being used
|
||||
params={'items': json.dumps(self.contents).replace('</script>', '<"+"/script>'),
|
||||
'id': self.item_id,
|
||||
'position': self.position,
|
||||
'titles': titles,
|
||||
'tag': self.xmltree.tag}
|
||||
params = {'items': json.dumps(contents).replace('</script>', '<"+"/script>'),
|
||||
'element_id': self.location.html_id(),
|
||||
'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)
|
||||
|
||||
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
|
||||
if system.get('position'):
|
||||
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'
|
||||
module_class = SequenceModule
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
xml_object = etree.fromstring(xml_data)
|
||||
|
||||
children = [
|
||||
system.process_xml(etree.tostring(child_module)).url
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return {'children': [
|
||||
system.process_xml(etree.tostring(child_module)).location.url()
|
||||
for child_module in xml_object
|
||||
]
|
||||
|
||||
return cls(
|
||||
system, {'children': children},
|
||||
location=['i4x',
|
||||
org,
|
||||
course,
|
||||
xml_object.tag,
|
||||
xml_object.get('name')]
|
||||
)
|
||||
]}
|
||||
|
||||
@@ -13,14 +13,23 @@ 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",
|
||||
"abtest = xmodule.abtest_module:ABTestDescriptor",
|
||||
"book = xmodule.translation_module:TranslateCustomTagDescriptor",
|
||||
"chapter = xmodule.seq_module:SequenceDescriptor",
|
||||
"course = xmodule.seq_module:SequenceDescriptor",
|
||||
"customtag = xmodule.template_module:CustomTagDescriptor",
|
||||
"discuss = xmodule.translation_module:TranslateCustomTagDescriptor",
|
||||
"html = xmodule.html_module:HtmlDescriptor",
|
||||
"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",
|
||||
]
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import json
|
||||
|
||||
from x_module import XModule, XModuleDescriptor
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from lxml import etree
|
||||
|
||||
|
||||
class ModuleDescriptor(XModuleDescriptor):
|
||||
pass
|
||||
|
||||
|
||||
class Module(XModule):
|
||||
class CustomTagModule(XModule):
|
||||
"""
|
||||
This module supports tags of the form
|
||||
<customtag option="val" option2="val2">
|
||||
@@ -31,19 +26,16 @@ class Module(XModule):
|
||||
Renders to::
|
||||
More information given in <a href="/book/234">the text</a>
|
||||
"""
|
||||
def get_state(self):
|
||||
return json.dumps({})
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ['customtag']
|
||||
def __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'])
|
||||
filename = xmltree.find('impl').text
|
||||
params = dict(xmltree.items())
|
||||
self.html = self.system.render_template(filename, params, namespace='custom_tags')
|
||||
|
||||
def get_html(self):
|
||||
return self.html
|
||||
|
||||
def __init__(self, system, xml, item_id, state=None):
|
||||
XModule.__init__(self, system, xml, item_id, state)
|
||||
xmltree = etree.fromstring(xml)
|
||||
filename = xmltree.find('impl').text
|
||||
params = dict(xmltree.items())
|
||||
self.html = self.system.render_template(filename, params, namespace='custom_tags')
|
||||
class CustomTagDescriptor(RawDescriptor):
|
||||
module_class = CustomTagModule
|
||||
|
||||
@@ -23,7 +23,7 @@ def process_includes(fn):
|
||||
file = next_include.get('file')
|
||||
if file is not None:
|
||||
try:
|
||||
ifp = system.fs.open(file)
|
||||
ifp = system.resources_fs.open(file)
|
||||
except Exception:
|
||||
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))
|
||||
@@ -57,11 +57,26 @@ class SemanticSectionDescriptor(XModuleDescriptor):
|
||||
|
||||
if len(xml_object) == 1:
|
||||
for (key, val) in xml_object.items():
|
||||
if key == 'format':
|
||||
continue
|
||||
xml_object[0].set(key, val)
|
||||
|
||||
return system.process_xml(etree.tostring(xml_object[0]))
|
||||
else:
|
||||
xml_object.tag = 'sequence'
|
||||
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))
|
||||
|
||||
@@ -1,24 +1,23 @@
|
||||
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
|
||||
# HACK: This shouldn't be hard-coded to two types
|
||||
# OBSOLETE: This obsoletes 'type'
|
||||
class_priority = ['video', 'problem']
|
||||
|
||||
class Module(XModule):
|
||||
|
||||
class VerticalModule(XModule):
|
||||
''' Layout module for laying out submodules vertically.'''
|
||||
id_attribute = 'id'
|
||||
|
||||
def get_state(self):
|
||||
return json.dumps({ })
|
||||
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 = None
|
||||
|
||||
@classmethod
|
||||
def get_xml_tags(c):
|
||||
return ["vertical", "problemset"]
|
||||
|
||||
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', {
|
||||
'items': self.contents
|
||||
})
|
||||
@@ -30,8 +29,14 @@ 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 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 VerticalDescriptor(SequenceDescriptor):
|
||||
module_class = VerticalModule
|
||||
|
||||
@@ -3,17 +3,27 @@ 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
|
||||
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):
|
||||
'''
|
||||
@@ -39,14 +49,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 +59,11 @@ class Module(XModule):
|
||||
def get_html(self):
|
||||
return self.system.render_template('video.html', {
|
||||
'streams': self.video_list(),
|
||||
'id': self.item_id,
|
||||
'id': self.location.html_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)
|
||||
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):
|
||||
pass
|
||||
class VideoDescriptor(RawDescriptor):
|
||||
module_class = VideoModule
|
||||
|
||||
@@ -3,6 +3,7 @@ import pkg_resources
|
||||
import logging
|
||||
|
||||
from keystore import Location
|
||||
from functools import partial
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
@@ -55,86 +56,107 @@ 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
|
||||
''' Implements a generic learning module.
|
||||
|
||||
See the HTML module for a simple example
|
||||
Subclasses must at a minimum provide a definition for get_html in order to be displayed to users.
|
||||
|
||||
See the HTML module for a simple example.
|
||||
'''
|
||||
id_attribute='id' # An attribute guaranteed to be unique
|
||||
|
||||
@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']
|
||||
# The default implementation of get_icon_class returns the icon_class attribute of the class
|
||||
# This attribute can be overridden by subclasses, and the function can also be overridden
|
||||
# if the icon class depends on the data in the module
|
||||
icon_class = 'other'
|
||||
|
||||
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
|
||||
'''
|
||||
Construct a new 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 JSON-like (string, dictionary, list, bool, or None, optionally nested).
|
||||
This defines all of the data necessary for a problem to display that is intrinsic to the problem.
|
||||
It should not include any data that would vary between two courses using the same problem
|
||||
(due dates, grading policy, randomization, etc.)
|
||||
'children': is a list of Location-like values for child modules that this module depends on
|
||||
instance_state: A string of serialized json that contains the state of this module for
|
||||
current student accessing the system, or None if no state has been saved
|
||||
shared_state: A string of serialized json that contains the state that is shared between
|
||||
this module and any modules of the same type with the same shared_state_key. This
|
||||
state is only shared per-student, not across different students
|
||||
kwargs: Optional arguments. Subclasses should always accept kwargs and pass them
|
||||
to the parent class constructor.
|
||||
Current known uses of kwargs:
|
||||
metadata: A dictionary containing data that specifies information that is particular
|
||||
to a problem in the context of a course
|
||||
'''
|
||||
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.category = self.location.category
|
||||
self.metadata = kwargs.get('metadata', {})
|
||||
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 css class identifying this module in the context of an icon
|
||||
'''
|
||||
return self.icon_class
|
||||
|
||||
### 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
|
||||
|
||||
@@ -149,7 +171,7 @@ class XModule(object):
|
||||
def get_html(self):
|
||||
''' HTML, as shown in the browser. This is the only method that must be implemented
|
||||
'''
|
||||
return "Unimplemented"
|
||||
raise NotImplementedError("get_html must be defined for all XModules that appear on the screen. Not defined in %s" % self.__class__.__name__)
|
||||
|
||||
def get_progress(self):
|
||||
''' Return a progress.Progress object that represents how far the student has gone
|
||||
@@ -180,6 +202,49 @@ class XModuleDescriptor(Plugin):
|
||||
js = {}
|
||||
js_module = None
|
||||
|
||||
# A list of metadata that this module can inherit from its parent module
|
||||
inheritable_metadata = ('graded', 'due', 'graceperiod', 'showanswer', 'rerandomize')
|
||||
|
||||
def __init__(self,
|
||||
system,
|
||||
definition=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Construct a new XModuleDescriptor. The only required arguments are the
|
||||
system, used for interaction with external resources, and the definition,
|
||||
which specifies all the data needed to edit and display the problem (but none
|
||||
of the associated metadata that handles recordkeeping around the problem).
|
||||
|
||||
This allows for maximal flexibility to add to the interface while preserving
|
||||
backwards compatibility.
|
||||
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
definition: A dict containing `data` and `children` representing the problem definition
|
||||
|
||||
Current arguments passed in kwargs:
|
||||
location: A keystore.Location object indicating the name and ownership of this problem
|
||||
shared_state_key: The key to use for sharing StudentModules with other
|
||||
modules of this type
|
||||
metadata: A dictionary containing the following optional keys:
|
||||
goals: A list of strings of learning goals associated with this module
|
||||
display_name: The name to use for displaying this module to the user
|
||||
format: The format of this module ('Homework', 'Lab', etc)
|
||||
graded (bool): Whether this module is should be graded or not
|
||||
due (string): The due date for this module
|
||||
graceperiod (string): The amount of grace period to allow when enforcing the due date
|
||||
showanswer (string): When to show answers for this module
|
||||
rerandomize (string): When to generate a newly randomized instance of the module data
|
||||
"""
|
||||
self.system = system
|
||||
self.definition = definition if definition is not None else {}
|
||||
self.name = Location(kwargs.get('location')).name
|
||||
self.category = Location(kwargs.get('location')).category
|
||||
self.location = Location(kwargs.get('location'))
|
||||
self.metadata = kwargs.get('metadata', {})
|
||||
self.shared_state_key = kwargs.get('shared_state_key')
|
||||
|
||||
self._child_instances = None
|
||||
|
||||
@staticmethod
|
||||
def load_from_json(json_data, system, default_class=None):
|
||||
"""
|
||||
@@ -201,13 +266,18 @@ class XModuleDescriptor(Plugin):
|
||||
Creates an instance of this descriptor from the supplied json_data.
|
||||
This may be overridden by subclasses
|
||||
|
||||
json_data: Json data specifying the data, children, and metadata for the descriptor
|
||||
json_data: A json object specifying the definition and any optional keyword arguments for
|
||||
the XModuleDescriptor
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
"""
|
||||
return cls(system=system, **json_data)
|
||||
|
||||
@staticmethod
|
||||
def load_from_xml(xml_data, system, org=None, course=None, default_class=None):
|
||||
def load_from_xml(xml_data,
|
||||
system,
|
||||
org=None,
|
||||
course=None,
|
||||
default_class=None):
|
||||
"""
|
||||
This method instantiates the correct subclass of XModuleDescriptor based
|
||||
on the contents of xml_data.
|
||||
@@ -256,43 +326,27 @@ class XModuleDescriptor(Plugin):
|
||||
"""
|
||||
return self.js_module
|
||||
|
||||
def __init__(self,
|
||||
system,
|
||||
definition=None,
|
||||
**kwargs):
|
||||
|
||||
def inherit_metadata(self, metadata):
|
||||
"""
|
||||
Construct a new XModuleDescriptor. The only required arguments are the
|
||||
system, used for interaction with external resources, and the definition,
|
||||
which specifies all the data needed to edit and display the problem (but none
|
||||
of the associated metadata that handles recordkeeping around the problem).
|
||||
|
||||
This allows for maximal flexibility to add to the interface while preserving
|
||||
backwards compatibility.
|
||||
|
||||
system: An XModuleSystem for interacting with external resources
|
||||
definition: A dict containing `data` and `children` representing the problem definition
|
||||
|
||||
Current arguments passed in kwargs:
|
||||
location: A keystore.Location object indicating the name and ownership of this problem
|
||||
goals: A list of strings of learning goals associated with this module
|
||||
Updates this module with metadata inherited from a containing module.
|
||||
Only metadata specified in self.inheritable_metadata will
|
||||
be inherited
|
||||
"""
|
||||
self.system = system
|
||||
self.definition = definition if definition is not None else {}
|
||||
self.name = Location(kwargs.get('location')).name
|
||||
self.type = Location(kwargs.get('location')).category
|
||||
self.url = Location(kwargs.get('location')).url()
|
||||
|
||||
# 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
|
||||
# to find the best teaching method
|
||||
self.goals = kwargs.get('goals', [])
|
||||
|
||||
self._child_instances = None
|
||||
# Set all inheritable metadata from kwargs that are
|
||||
# in self.inheritable_metadata and aren't already set in metadata
|
||||
for attr in self.inheritable_metadata:
|
||||
if attr not in self.metadata and attr in metadata:
|
||||
self.metadata[attr] = metadata[attr]
|
||||
|
||||
def get_children(self):
|
||||
"""Returns a list of XModuleDescriptor instances for the children of this module"""
|
||||
if self._child_instances is None:
|
||||
self._child_instances = [self.system.load_item(child) for child in self.definition.get('children', [])]
|
||||
self._child_instances = []
|
||||
for child_loc in self.definition.get('children', []):
|
||||
child = self.system.load_item(child_loc)
|
||||
child.inherit_metadata(self.metadata)
|
||||
self._child_instances.append(child)
|
||||
|
||||
return self._child_instances
|
||||
|
||||
@@ -302,49 +356,36 @@ 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.location,
|
||||
self.definition,
|
||||
metadata=self.metadata
|
||||
)
|
||||
|
||||
|
||||
class DescriptorSystem(object):
|
||||
def __init__(self, load_item):
|
||||
def __init__(self, load_item, resources_fs):
|
||||
"""
|
||||
load_item: Takes a Location and returns an XModuleDescriptor
|
||||
resources_fs: A Filesystem object that contains all of the
|
||||
resources needed for the course
|
||||
"""
|
||||
|
||||
self.load_item = load_item
|
||||
self.resources_fs = resources_fs
|
||||
|
||||
|
||||
class XMLParsingSystem(DescriptorSystem):
|
||||
def __init__(self, load_item, process_xml, fs):
|
||||
def __init__(self, load_item, resources_fs, process_xml):
|
||||
"""
|
||||
process_xml: Takes an xml string, and returns the the XModuleDescriptor created from that xml
|
||||
fs: A Filesystem object that contains all of the xml resources needed to parse
|
||||
the course
|
||||
"""
|
||||
DescriptorSystem.__init__(self, load_item, resources_fs)
|
||||
self.process_xml = process_xml
|
||||
self.fs = fs
|
||||
|
||||
53
common/lib/xmodule/xml_module.py
Normal file
53
common/lib/xmodule/xml_module.py
Normal file
@@ -0,0 +1,53 @@
|
||||
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,
|
||||
)
|
||||
@@ -1,365 +0,0 @@
|
||||
'''
|
||||
courseware/content_parser.py
|
||||
|
||||
This file interfaces between all courseware modules and the top-level course.xml file for a course.
|
||||
|
||||
Does some caching (to be explained).
|
||||
|
||||
'''
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import urllib
|
||||
|
||||
from lxml import etree
|
||||
from util.memcache import fasthash
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
from student.models import UserProfile
|
||||
from student.models import UserTestGroup
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from util.cache import cache
|
||||
from multicourse import multicourse_settings
|
||||
import xmodule
|
||||
|
||||
''' This file will eventually form an abstraction layer between the
|
||||
course XML file and the rest of the system.
|
||||
'''
|
||||
|
||||
# ==== This section has no direct dependencies on django ====================================
|
||||
# NOTE: it does still have some indirect dependencies:
|
||||
# util.memcache.fasthash (which does not depend on memcache at all)
|
||||
#
|
||||
|
||||
class ContentException(Exception):
|
||||
pass
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
def format_url_params(params):
|
||||
return [ urllib.quote(string.replace(' ','_')) for string in params ]
|
||||
|
||||
def xpath_remove(tree, path):
|
||||
''' Remove all items matching path from lxml tree. Works in
|
||||
place.'''
|
||||
items = tree.xpath(path)
|
||||
for item in items:
|
||||
item.getparent().remove(item)
|
||||
return tree
|
||||
|
||||
def id_tag(course):
|
||||
''' Tag all course elements with unique IDs '''
|
||||
default_ids = xmodule.get_default_ids()
|
||||
|
||||
# Tag elements with unique IDs
|
||||
elements = course.xpath("|".join('//' + c for c in default_ids))
|
||||
for elem in elements:
|
||||
if elem.get('id'):
|
||||
pass
|
||||
elif elem.get(default_ids[elem.tag]):
|
||||
new_id = elem.get(default_ids[elem.tag])
|
||||
# Convert to alphanumeric
|
||||
new_id = "".join(a for a in new_id if a.isalnum())
|
||||
|
||||
# Without this, a conflict may occur between an html or youtube id
|
||||
new_id = default_ids[elem.tag] + new_id
|
||||
elem.set('id', new_id)
|
||||
else:
|
||||
elem.set('id', "id" + fasthash(etree.tostring(elem)))
|
||||
|
||||
def propogate_downward_tag(element, attribute_name, parent_attribute = None):
|
||||
''' This call is to pass down an attribute to all children. If an element
|
||||
has this attribute, it will be "inherited" by all of its children. If a
|
||||
child (A) already has that attribute, A will keep the same attribute and
|
||||
all of A's children will inherit A's attribute. This is a recursive call.'''
|
||||
|
||||
if (parent_attribute is None):
|
||||
#This is the entry call. Select all elements with this attribute
|
||||
all_attributed_elements = element.xpath("//*[@" + attribute_name +"]")
|
||||
for attributed_element in all_attributed_elements:
|
||||
attribute_value = attributed_element.get(attribute_name)
|
||||
for child_element in attributed_element:
|
||||
propogate_downward_tag(child_element, attribute_name, attribute_value)
|
||||
else:
|
||||
'''The hack below is because we would get _ContentOnlyELements from the
|
||||
iterator that can't have attributes set. We can't find API for it. If we
|
||||
ever have an element which subclasses BaseElement, we will not tag it'''
|
||||
if not element.get(attribute_name) and type(element) == etree._Element:
|
||||
element.set(attribute_name, parent_attribute)
|
||||
|
||||
for child_element in element:
|
||||
propogate_downward_tag(child_element, attribute_name, parent_attribute)
|
||||
else:
|
||||
#This element would have already been found by Xpath, so we return
|
||||
#for now and trust that this element will get its turn to propogate
|
||||
#to its children later.
|
||||
return
|
||||
|
||||
|
||||
def course_xml_process(tree):
|
||||
''' Do basic pre-processing of an XML tree. Assign IDs to all
|
||||
items without. Propagate due dates, grace periods, etc. to child
|
||||
items.
|
||||
'''
|
||||
replace_custom_tags(tree)
|
||||
id_tag(tree)
|
||||
propogate_downward_tag(tree, "due")
|
||||
propogate_downward_tag(tree, "graded")
|
||||
propogate_downward_tag(tree, "graceperiod")
|
||||
propogate_downward_tag(tree, "showanswer")
|
||||
propogate_downward_tag(tree, "rerandomize")
|
||||
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 replace_custom_tags_dir(tree, dir):
|
||||
'''
|
||||
Process tree to replace all custom tags defined in dir.
|
||||
'''
|
||||
tags = os.listdir(dir)
|
||||
for tag in tags:
|
||||
for element in tree.iter(tag):
|
||||
element.tag = 'customtag'
|
||||
impl = etree.SubElement(element, 'impl')
|
||||
impl.text = tag
|
||||
|
||||
def parse_course_file(filename, options, namespace):
|
||||
'''
|
||||
Parse a course file with the given options, and return the resulting
|
||||
xml tree object.
|
||||
|
||||
Options should be a dictionary including keys
|
||||
'dev_content': bool,
|
||||
'groups' : [list, of, user, groups]
|
||||
|
||||
namespace is used to in searching for the file. Could be e.g. 'course',
|
||||
'sections'.
|
||||
'''
|
||||
xml = etree.XML(render_to_string(filename, options, namespace=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):
|
||||
if not user.is_authenticated():
|
||||
return []
|
||||
|
||||
# TODO: Rewrite in Django
|
||||
key = 'user_group_names_{user.id}'.format(user=user)
|
||||
cache_expiration = 60 * 60 # one hour
|
||||
|
||||
# Kill caching on dev machines -- we switch groups a lot
|
||||
group_names = cache.get(key)
|
||||
|
||||
if group_names is None:
|
||||
group_names = [u.name for u in UserTestGroup.objects.filter(users=user)]
|
||||
cache.set(key, group_names, cache_expiration)
|
||||
|
||||
return group_names
|
||||
|
||||
|
||||
def get_options(user):
|
||||
return {'dev_content': settings.DEV_CONTENT,
|
||||
'groups': user_groups(user)}
|
||||
|
||||
|
||||
def replace_custom_tags(tree):
|
||||
'''Replace custom tags defined in our custom_tags dir'''
|
||||
replace_custom_tags_dir(tree, settings.DATA_DIR+'/custom_tags')
|
||||
|
||||
|
||||
def course_file(user, coursename=None):
|
||||
''' Given a user, return an xml tree object for the course file.
|
||||
|
||||
Handles getting the right file, and processing it depending on the
|
||||
groups the user is in. Does caching of the xml strings.
|
||||
'''
|
||||
|
||||
if user.is_authenticated():
|
||||
# use user.profile_cache.courseware?
|
||||
filename = UserProfile.objects.get(user=user).courseware
|
||||
else:
|
||||
filename = 'guest_course.xml'
|
||||
|
||||
# if a specific course is specified, then use multicourse to get
|
||||
# the right path to the course XML directory
|
||||
if coursename and settings.ENABLE_MULTICOURSE:
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename)
|
||||
filename = xp + filename # prefix the filename with the path
|
||||
|
||||
groups = user_groups(user)
|
||||
options = get_options(user)
|
||||
|
||||
# Try the cache...
|
||||
cache_key = "{0}_processed?dev_content:{1}&groups:{2}".format(
|
||||
filename,
|
||||
options['dev_content'],
|
||||
sorted(groups))
|
||||
|
||||
if "dev" in settings.DEFAULT_GROUPS:
|
||||
tree_string = None
|
||||
else:
|
||||
tree_string = cache.get(cache_key)
|
||||
|
||||
if tree_string:
|
||||
tree = etree.XML(tree_string)
|
||||
else:
|
||||
tree = parse_course_file(filename, options, namespace='course')
|
||||
# Cache it
|
||||
tree_string = etree.tostring(tree)
|
||||
cache.set(cache_key, tree_string, 60)
|
||||
|
||||
return tree
|
||||
|
||||
|
||||
def sections_dir(coursename=None):
|
||||
''' Get directory where sections information is stored.
|
||||
'''
|
||||
# if a specific course is specified, then use multicourse to get the
|
||||
# right path to the course XML directory
|
||||
xp = ''
|
||||
if coursename and settings.ENABLE_MULTICOURSE:
|
||||
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)
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
from lxml import etree
|
||||
import random
|
||||
import imp
|
||||
import logging
|
||||
import sys
|
||||
import types
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
@@ -11,134 +7,119 @@ from courseware.course_settings import course_settings
|
||||
from xmodule import graders
|
||||
from xmodule.graders import Score
|
||||
from models import StudentModule
|
||||
import courseware.content_parser as content_parser
|
||||
import xmodule
|
||||
|
||||
_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:
|
||||
|
||||
|
||||
- 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
|
||||
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.
|
||||
|
||||
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 = {}
|
||||
chapters=[]
|
||||
for c in xmlChapters:
|
||||
chapters = []
|
||||
for c in course.get_children():
|
||||
sections = []
|
||||
chname=c.get('name')
|
||||
|
||||
|
||||
for s in dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section',
|
||||
course=course, chname=chname):
|
||||
problems=dom.xpath('//course[@name=$course]/chapter[@name=$chname]/section[@name=$section]//problem',
|
||||
course=course, chname=chname, section=s.get('name'))
|
||||
for s in c.get_children():
|
||||
def yield_descendents(module):
|
||||
yield module
|
||||
for child in module.get_display_items():
|
||||
for module in yield_descendents(child):
|
||||
yield module
|
||||
|
||||
graded = True if s.get('graded') == "true" else False
|
||||
scores=[]
|
||||
if len(problems)>0:
|
||||
for p in problems:
|
||||
(correct,total) = get_score(student, p, response_by_id, coursename=coursename)
|
||||
|
||||
if settings.GENERATE_PROFILE_SCORES:
|
||||
if total > 1:
|
||||
correct = random.randrange( max(total-2, 1) , total + 1 )
|
||||
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
|
||||
graded = False
|
||||
scores.append( Score(correct,total, graded, p.get("name")) )
|
||||
graded = s.metadata.get('graded', False)
|
||||
scores = []
|
||||
for module in yield_descendents(s):
|
||||
(correct, total) = get_score(student, module, student_module_cache)
|
||||
|
||||
section_total, graded_total = graders.aggregate_scores(scores, s.get("name"))
|
||||
#Add the graded total to totaled_scores
|
||||
format = s.get('format', "")
|
||||
subtitle = s.get('subtitle', format)
|
||||
if format and graded_total[1] > 0:
|
||||
format_scores = totaled_scores.get(format, [])
|
||||
format_scores.append( graded_total )
|
||||
totaled_scores[ format ] = format_scores
|
||||
if correct is None and total is None:
|
||||
continue
|
||||
|
||||
section_score={'section':s.get("name"),
|
||||
'scores':scores,
|
||||
'section_total' : section_total,
|
||||
'format' : format,
|
||||
'subtitle' : subtitle,
|
||||
'due' : s.get("due") or "",
|
||||
'graded' : graded,
|
||||
}
|
||||
sections.append(section_score)
|
||||
if settings.GENERATE_PROFILE_SCORES:
|
||||
if total > 1:
|
||||
correct = random.randrange(max(total - 2, 1), total + 1)
|
||||
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
|
||||
graded = False
|
||||
|
||||
scores.append(Score(correct, total, graded, module.metadata.get('display_name')))
|
||||
|
||||
section_total, graded_total = graders.aggregate_scores(scores, s.metadata.get('display_name'))
|
||||
#Add the graded total to totaled_scores
|
||||
format = s.metadata.get('format', "")
|
||||
if format and graded_total.possible > 0:
|
||||
format_scores = totaled_scores.get(format, [])
|
||||
format_scores.append(graded_total)
|
||||
totaled_scores[format] = format_scores
|
||||
|
||||
sections.append({
|
||||
'section': s.metadata.get('display_name'),
|
||||
'scores': scores,
|
||||
'section_total': section_total,
|
||||
'format': format,
|
||||
'due': s.metadata.get("due", ""),
|
||||
'graded': graded,
|
||||
})
|
||||
|
||||
chapters.append({'course': course.metadata.get('display_name'),
|
||||
'chapter': c.metadata.get('display_name'),
|
||||
'sections': sections})
|
||||
|
||||
chapters.append({'course':course,
|
||||
'chapter' : c.get("name"),
|
||||
'sections' : sections,})
|
||||
|
||||
|
||||
grader = course_settings.GRADER
|
||||
grade_summary = grader.grade(totaled_scores)
|
||||
|
||||
return {'courseware_summary' : chapters,
|
||||
'grade_summary' : grade_summary}
|
||||
|
||||
def get_score(user, problem, cache, coursename=None):
|
||||
## HACK: assumes max score is fixed per problem
|
||||
id = problem.get('id')
|
||||
return {'courseware_summary': chapters,
|
||||
'grade_summary': grade_summary}
|
||||
|
||||
|
||||
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
|
||||
|
||||
# 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
|
||||
|
||||
# Grab the # correct from cache
|
||||
if id in cache:
|
||||
response = cache[id]
|
||||
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
|
||||
else:
|
||||
## HACK 1: We shouldn't specifically reference capa_module
|
||||
## HACK 2: Backwards-compatibility: This should be written when a grade is saved, and removed from the system
|
||||
# TODO: These are no longer correct params for I4xSystem -- figure out what this code
|
||||
# does, clean it up.
|
||||
# from module_render import I4xSystem
|
||||
# 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
|
||||
# If the ID is not in the cache, add the item
|
||||
instance_module = cache.lookup(problem.category, problem.id)
|
||||
if instance_module is None:
|
||||
instance_module = StudentModule(module_type=problem.category,
|
||||
module_state_key=problem.id,
|
||||
student=user,
|
||||
state=None,
|
||||
grade=0,
|
||||
max_grade=problem.max_score(),
|
||||
done='i')
|
||||
cache.append(instance_module)
|
||||
instance_module.save()
|
||||
|
||||
# If this problem is ungraded/ungradable, bail
|
||||
if instance_module.max_grade is None:
|
||||
return (None, None)
|
||||
|
||||
correct = instance_module.grade if instance_module.grade is not None else 0
|
||||
total = instance_module.max_grade
|
||||
|
||||
if correct is not None and total is not None:
|
||||
#Now we re-weight the problem, if specified
|
||||
weight = getattr(problem, 'weight', 1)
|
||||
if weight != 1:
|
||||
correct = correct * weight / total
|
||||
total = weight
|
||||
|
||||
return (correct, total)
|
||||
|
||||
@@ -6,50 +6,36 @@ from django.core.management.base import BaseCommand
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
from courseware.content_parser import course_file
|
||||
import courseware.module_render
|
||||
import xmodule
|
||||
|
||||
import mitxmako.middleware as middleware
|
||||
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'''
|
||||
all_ok = True
|
||||
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]
|
||||
# TODO: Abstract this out in render_module.py
|
||||
try:
|
||||
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'})
|
||||
|
||||
def _check_module(module):
|
||||
try:
|
||||
module.get_html()
|
||||
except Exception as ex:
|
||||
print "==============> Error in ", etree.tostring(module)
|
||||
print "==============> Error in ", module.id
|
||||
print ""
|
||||
print ex
|
||||
all_ok = False
|
||||
for child in module.get_children():
|
||||
_check_module(child)
|
||||
_check_module(module)
|
||||
print "Module render check finished"
|
||||
return all_ok
|
||||
|
||||
def check_sections(user, course):
|
||||
|
||||
def check_sections(course):
|
||||
all_ok = True
|
||||
sections_dir = settings.DATA_DIR + "/sections"
|
||||
print "Checking that all sections exist and parse properly"
|
||||
@@ -69,11 +55,13 @@ def check_sections(user, course):
|
||||
all_ok = False
|
||||
print "checked all sections"
|
||||
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
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Does basic validity tests on course.xml."
|
||||
|
||||
def handle(self, *args, **options):
|
||||
all_ok = True
|
||||
|
||||
@@ -86,22 +74,25 @@ class Command(BaseCommand):
|
||||
|
||||
sample_user = User.objects.all()[0]
|
||||
|
||||
|
||||
print "Attempting to load courseware"
|
||||
course = course_file(sample_user)
|
||||
|
||||
to_run = [check_names,
|
||||
# TODO (vshnayder) : make check_rendering work (use module_render.py),
|
||||
# turn it on
|
||||
# check_rendering,
|
||||
check_sections,
|
||||
]
|
||||
# TODO (cpennington): Get coursename in a legitimate way
|
||||
course_location = 'i4x://edx/6002xs12/course/6.002_Spring_2012'
|
||||
student_module_cache = StudentModuleCache(sample_user, keystore().get_item(course_location))
|
||||
(course, _, _, _) = get_module(sample_user, None, course_location, student_module_cache)
|
||||
|
||||
to_run = [
|
||||
#TODO (vshnayder) : make check_rendering work (use module_render.py),
|
||||
# turn it on
|
||||
check_rendering,
|
||||
check_sections,
|
||||
]
|
||||
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"
|
||||
|
||||
|
||||
if all_ok:
|
||||
print 'Courseware passes all checks!'
|
||||
else:
|
||||
else:
|
||||
print "Courseware fails some checks"
|
||||
|
||||
@@ -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,106 @@ 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)
|
||||
|
||||
# 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)
|
||||
)
|
||||
|
||||
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.location.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)
|
||||
|
||||
@@ -1,30 +1,22 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from django.conf import settings
|
||||
from django.http import Http404
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import redirect
|
||||
from lxml import etree
|
||||
|
||||
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 multicourse import multicourse_settings
|
||||
from util.views import accepts
|
||||
|
||||
import courseware.content_parser as content_parser
|
||||
import xmodule
|
||||
from keystore.django import keystore
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
from models import StudentModule, StudentModuleCache
|
||||
|
||||
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
|
||||
@@ -33,8 +25,8 @@ class I4xSystem(object):
|
||||
Note that these functions can be closures over e.g. a django request
|
||||
and user, or other environment-specific info.
|
||||
'''
|
||||
def __init__(self, ajax_url, track_function, render_function,
|
||||
module_from_xml, render_template, request=None,
|
||||
def __init__(self, ajax_url, track_function,
|
||||
get_module, render_template, user=None,
|
||||
filestore=None):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
@@ -44,39 +36,28 @@ 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
|
||||
module to html. Dictionary will contain keys 'content'
|
||||
and 'type'.
|
||||
render_template - a function that takes (template_file, context), and returns
|
||||
rendered html.
|
||||
request - the request in progress
|
||||
user - The user to base the seed off of for this request
|
||||
filestore - A filestore ojbect. Defaults to an instance of OSFS based at
|
||||
settings.DATA_DIR.
|
||||
'''
|
||||
self.ajax_url = ajax_url
|
||||
self.track_function = track_function
|
||||
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.render_function = render_function
|
||||
self.filestore = filestore
|
||||
self.get_module = get_module
|
||||
self.render_template = render_template
|
||||
self.exception404 = Http404
|
||||
self.DEBUG = settings.DEBUG
|
||||
self.id = request.user.id if request is not None else 0
|
||||
self.seed = user.id if user is not None else 0
|
||||
|
||||
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 +67,9 @@ 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 +79,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 +107,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.metadata.get('display_name') == active_chapter and
|
||||
section.metadata.get('display_name') == active_section)
|
||||
|
||||
sections.append({'name': section.metadata.get('display_name'),
|
||||
'format': section.metadata.get('format', ''),
|
||||
'due': section.metadata.get('due', ''),
|
||||
'active': active})
|
||||
|
||||
chapters.append({'name': chapter.metadata.get('display_name'),
|
||||
'sections': sections,
|
||||
'active': chapter.metadata.get('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.metadata.get('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.metadata.get('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,196 +196,122 @@ 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.category, descriptor.location.url())
|
||||
shared_state_key = getattr(descriptor, 'shared_state_key', None)
|
||||
if shared_state_key is not None:
|
||||
shared_module = student_module_cache.lookup(descriptor.category, 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.location.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(
|
||||
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,
|
||||
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_template=render_to_string,
|
||||
ajax_url=ajax_url,
|
||||
# TODO (cpennington): Figure out how to share info between systems
|
||||
filestore=descriptor.system.resources_fs,
|
||||
get_module=_get_module,
|
||||
user=user,
|
||||
)
|
||||
# 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 settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
|
||||
module = add_histogram(module)
|
||||
|
||||
# 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.category,
|
||||
module_state_key=module.id,
|
||||
state=module.get_instance_state(),
|
||||
max_grade=module.max_score())
|
||||
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.category,
|
||||
module_state_key=shared_state_key,
|
||||
state=module.get_shared_state())
|
||||
shared_module.save()
|
||||
student_module_cache.append(shared_module)
|
||||
|
||||
return (instance, smod, module_type)
|
||||
return (module, instance_module, shared_module, descriptor.category)
|
||||
|
||||
def render_x_module(user, request, module_xml, student_module_cache, position=None):
|
||||
''' Generic module for extensions. This renders to HTML.
|
||||
|
||||
modules include sequential, vertical, problem, video, html
|
||||
def add_histogram(module):
|
||||
original_get_html = module.get_html
|
||||
|
||||
Note that modules can recurse. problems, video, html, can be inside sequential or vertical.
|
||||
|
||||
Arguments:
|
||||
|
||||
- user : current django User
|
||||
- request : current django HTTPrequest
|
||||
- module_xml : lxml etree of xml subtree for the current 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 position within module
|
||||
|
||||
Returns:
|
||||
|
||||
- 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 :
|
||||
return {"content": ""}
|
||||
|
||||
(instance, smod, 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
|
||||
if settings.MITX_FEATURES.get('DISPLAY_HISTOGRAMS_TO_STAFF') and user.is_staff:
|
||||
module_id = module_xml.get('id')
|
||||
def get_html():
|
||||
module_id = module.id
|
||||
histogram = grade_histogram(module_id)
|
||||
render_histogram = len(histogram) > 0
|
||||
staff_context = {'xml': etree.tostring(module_xml),
|
||||
'module_id': module_id,
|
||||
staff_context = {'definition': json.dumps(module.definition, indent=4),
|
||||
'metadata': json.dumps(module.metadata, indent=4),
|
||||
'element_id': module.location.html_id(),
|
||||
'histogram': json.dumps(histogram),
|
||||
'render_histogram': render_histogram}
|
||||
content += render_to_string("staff_problem_info.html", staff_context)
|
||||
'render_histogram': render_histogram,
|
||||
'module_content': original_get_html()}
|
||||
return render_to_string("staff_problem_info.html", staff_context)
|
||||
|
||||
context = {'content': content, 'type': module_type}
|
||||
return context
|
||||
module.get_html = get_html
|
||||
return module
|
||||
|
||||
def modx_dispatch(request, module=None, dispatch=None, id=None):
|
||||
|
||||
def modx_dispatch(request, dispatch=None, id=None):
|
||||
''' Generic view for extensions. This is where AJAX calls go.
|
||||
|
||||
Arguments:
|
||||
|
||||
- request -- the django request.
|
||||
- module -- the type of the module, as used in the course configuration xml.
|
||||
e.g. 'problem', 'video', etc
|
||||
- dispatch -- the command string to pass through to the module's handle_ajax call
|
||||
(e.g. 'problem_reset'). If this string contains '?', only pass
|
||||
through the part before the first '?'.
|
||||
- id -- the module id. Used to look up the student module.
|
||||
e.g. filenamexformularesponse
|
||||
- id -- the module id. Used to look up the XModule instance
|
||||
'''
|
||||
# ''' (fix emacs broken parsing)
|
||||
if not request.user.is_authenticated():
|
||||
return redirect('/')
|
||||
|
||||
# python concats adjacent strings
|
||||
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,
|
||||
module=module, id=id)
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
if coursename and settings.ENABLE_MULTICOURSE:
|
||||
xp = multicourse_settings.get_course_xmlpath(coursename)
|
||||
data_root = settings.DATA_DIR + xp
|
||||
else:
|
||||
data_root = settings.DATA_DIR
|
||||
student_module_cache = StudentModuleCache(request.user, keystore().get_item(id))
|
||||
instance, instance_module, shared_module, module_type = get_module(request.user, request, id, student_module_cache)
|
||||
|
||||
# Grab the XML corresponding to the request from course.xml
|
||||
try:
|
||||
xml = content_parser.module_xml(request.user, module, 'id', id, coursename)
|
||||
except:
|
||||
log.exception(
|
||||
"Unable to load module during ajax call. module=%s, dispatch=%s, id=%s",
|
||||
module, dispatch, id)
|
||||
if accepts(request, 'text/html'):
|
||||
return render_to_response("module-error.html", {})
|
||||
else:
|
||||
response = HttpResponse(json.dumps({'success': error_msg}))
|
||||
return response
|
||||
if instance_module is None:
|
||||
log.debug("Couldn't find module '%s' for user '%s'",
|
||||
id, request.user)
|
||||
raise Http404
|
||||
|
||||
# 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_from_xml = make_module_from_xml_fn(
|
||||
request.user, request, [s], 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),
|
||||
)
|
||||
|
||||
try:
|
||||
module_class = xmodule.get_module_class(module)
|
||||
instance = module_class(system, xml, id, state=oldstate)
|
||||
except:
|
||||
log.exception("Unable to load module instance during ajax call")
|
||||
if accepts(request, 'text/html'):
|
||||
return render_to_response("module-error.html", {})
|
||||
else:
|
||||
response = HttpResponse(json.dumps({'success': error_msg}))
|
||||
return response
|
||||
oldgrade = instance_module.grade
|
||||
old_instance_state = instance_module.state
|
||||
old_shared_state = shared_module.state if shared_module is not None else None
|
||||
|
||||
# Let the module handle the AJAX
|
||||
try:
|
||||
@@ -351,10 +321,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_module.state = instance.get_instance_state()
|
||||
if instance.get_score():
|
||||
instance_module.grade = instance.get_score()['score']
|
||||
if instance_module.grade != oldgrade or instance_module.state != old_instance_state:
|
||||
instance_module.save()
|
||||
|
||||
if shared_module is not None:
|
||||
shared_module.state = instance.get_shared_state()
|
||||
if shared_module.state != old_shared_state:
|
||||
shared_module.save()
|
||||
|
||||
# Return whatever the module wanted to return to the client/caller
|
||||
return HttpResponse(ajax_return)
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import logging
|
||||
import urllib
|
||||
|
||||
from fs.osfs import OSFS
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.context_processors import csrf
|
||||
from django.contrib.auth.models import User
|
||||
@@ -16,40 +14,73 @@ 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 toc_for_course, get_module, get_section
|
||||
from models import StudentModuleCache
|
||||
from student.models import UserProfile
|
||||
from multicourse import multicourse_settings
|
||||
import xmodule
|
||||
from keystore.django import keystore
|
||||
|
||||
import courseware.content_parser as content_parser
|
||||
|
||||
import courseware.grades as grades
|
||||
from util.cache import cache
|
||||
from student.models import UserTestGroup
|
||||
from courseware import grades
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments = True))
|
||||
remove_comments=True))
|
||||
|
||||
template_imports = {'urllib': urllib}
|
||||
|
||||
|
||||
def user_groups(user):
|
||||
if not user.is_authenticated():
|
||||
return []
|
||||
|
||||
# TODO: Rewrite in Django
|
||||
key = 'user_group_names_{user.id}'.format(user=user)
|
||||
cache_expiration = 60 * 60 # one hour
|
||||
|
||||
# Kill caching on dev machines -- we switch groups a lot
|
||||
group_names = cache.get(key)
|
||||
|
||||
if group_names is None:
|
||||
group_names = [u.name for u in UserTestGroup.objects.filter(users=user)]
|
||||
cache.set(key, group_names, cache_expiration)
|
||||
|
||||
return group_names
|
||||
|
||||
|
||||
def format_url_params(params):
|
||||
return [urllib.quote(string.replace(' ', '_')) for string in params]
|
||||
|
||||
template_imports={'urllib':urllib}
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def gradebook(request):
|
||||
if 'course_admin' not in content_parser.user_groups(request.user):
|
||||
if 'course_admin' not in user_groups(request.user):
|
||||
raise Http404
|
||||
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
|
||||
student_objects = User.objects.all()[:100]
|
||||
student_info = [{'username': s.username,
|
||||
'id': s.id,
|
||||
'email': s.email,
|
||||
'grade_info': grades.grade_sheet(s, coursename),
|
||||
'realname': UserProfile.objects.get(user = s).name
|
||||
} for s in student_objects]
|
||||
student_info = []
|
||||
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
course_location = multicourse_settings.get_course_location(coursename)
|
||||
|
||||
for student in student_objects:
|
||||
student_module_cache = StudentModuleCache(student, keystore().get_item(course_location))
|
||||
course, _, _, _ = get_module(request.user, request, course_location, student_module_cache)
|
||||
student_info.append({
|
||||
'username': student.username,
|
||||
'id': student.id,
|
||||
'email': student.email,
|
||||
'grade_info': grades.grade_sheet(student, course, student_module_cache),
|
||||
'realname': UserProfile.objects.get(user=student).name
|
||||
})
|
||||
|
||||
return render_to_response('gradebook.html', {'students': student_info})
|
||||
|
||||
|
||||
@login_required
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def profile(request, student_id=None):
|
||||
@@ -59,23 +90,26 @@ def profile(request, student_id=None):
|
||||
if student_id is None:
|
||||
student = request.user
|
||||
else:
|
||||
if 'course_admin' not in content_parser.user_groups(request.user):
|
||||
if 'course_admin' not in user_groups(request.user):
|
||||
raise Http404
|
||||
student = User.objects.get( id = int(student_id))
|
||||
student = User.objects.get(id=int(student_id))
|
||||
|
||||
user_info = UserProfile.objects.get(user=student) # request.user.profile_cache #
|
||||
user_info = UserProfile.objects.get(user=student)
|
||||
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
course_location = multicourse_settings.get_course_location(coursename)
|
||||
student_module_cache = StudentModuleCache(request.user, keystore().get_item(course_location))
|
||||
course, _, _, _ = get_module(request.user, request, course_location, student_module_cache)
|
||||
|
||||
context = {'name': user_info.name,
|
||||
'username': student.username,
|
||||
'location': user_info.location,
|
||||
'language': user_info.language,
|
||||
'email': student.email,
|
||||
'format_url_params': content_parser.format_url_params,
|
||||
'format_url_params': format_url_params,
|
||||
'csrf': csrf(request)['csrf_token']
|
||||
}
|
||||
context.update(grades.grade_sheet(student, coursename))
|
||||
context.update(grades.grade_sheet(student, course, student_module_cache))
|
||||
|
||||
return render_to_response('profile.html', context)
|
||||
|
||||
@@ -87,73 +121,23 @@ 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', format_url_params),
|
||||
('csrf', csrf(request)['csrf_token'])] + template_imports.items())
|
||||
return render_to_string('accordion.html', context)
|
||||
|
||||
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def render_section(request, section):
|
||||
''' TODO: Consolidate with index
|
||||
'''
|
||||
user = request.user
|
||||
if not settings.COURSEWARE_ENABLED:
|
||||
return redirect('/')
|
||||
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
|
||||
try:
|
||||
dom = content_parser.section_file(user, section, coursename)
|
||||
except:
|
||||
log.exception("Unable to parse courseware xml")
|
||||
return render_to_response('courseware-error.html', {})
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'accordion': render_accordion(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 = []
|
||||
|
||||
try:
|
||||
module = render_x_module(user, request, dom, student_module_cache)
|
||||
except:
|
||||
log.exception("Unable to load module")
|
||||
context.update({
|
||||
'init': '',
|
||||
'content': render_to_string("module-error.html", {}),
|
||||
})
|
||||
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 +145,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 +154,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 +183,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 +206,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.location, 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
|
||||
@@ -327,7 +238,8 @@ def jump_to(request, probname=None):
|
||||
|
||||
# look for problem of given name
|
||||
pxml = xml.xpath('//problem[@filename="%s"]' % probname)
|
||||
if pxml: pxml = pxml[0]
|
||||
if pxml:
|
||||
pxml = pxml[0]
|
||||
|
||||
# get the parent element
|
||||
parent = pxml.getparent()
|
||||
@@ -336,7 +248,7 @@ def jump_to(request, probname=None):
|
||||
chapter = None
|
||||
section = None
|
||||
branch = parent
|
||||
for k in range(4): # max depth of recursion
|
||||
for k in range(4): # max depth of recursion
|
||||
if branch.tag == 'section':
|
||||
section = branch.get('name')
|
||||
if branch.tag == 'chapter':
|
||||
@@ -345,7 +257,7 @@ def jump_to(request, probname=None):
|
||||
|
||||
position = None
|
||||
if parent.tag == 'sequential':
|
||||
position = parent.index(pxml) + 1 # position in sequence
|
||||
position = parent.index(pxml) + 1 # position in sequence
|
||||
|
||||
return index(request,
|
||||
course=coursename, chapter=chapter,
|
||||
|
||||
@@ -31,11 +31,13 @@ if hasattr(settings,'COURSE_SETTINGS'): # in the future, this could be repla
|
||||
elif hasattr(settings,'COURSE_NAME'): # backward compatibility
|
||||
COURSE_SETTINGS = {settings.COURSE_NAME: {'number': settings.COURSE_NUMBER,
|
||||
'title': settings.COURSE_TITLE,
|
||||
'location': settings.COURSE_LOCATION,
|
||||
},
|
||||
}
|
||||
else: # default to 6.002_Spring_2012
|
||||
COURSE_SETTINGS = {'6.002_Spring_2012': {'number': '6.002x',
|
||||
'title': 'Circuits and Electronics',
|
||||
'location': 'i4x://edx/6002xs12/course/6.002 Spring 2012',
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,31 +53,47 @@ def get_coursename_from_request(request):
|
||||
|
||||
def get_course_settings(coursename):
|
||||
if not coursename:
|
||||
if hasattr(settings,'COURSE_DEFAULT'):
|
||||
if hasattr(settings, 'COURSE_DEFAULT'):
|
||||
coursename = settings.COURSE_DEFAULT
|
||||
else:
|
||||
coursename = '6.002_Spring_2012'
|
||||
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename]
|
||||
coursename = coursename.replace(' ','_')
|
||||
if coursename in COURSE_SETTINGS: return COURSE_SETTINGS[coursename]
|
||||
if coursename in COURSE_SETTINGS:
|
||||
return COURSE_SETTINGS[coursename]
|
||||
coursename = coursename.replace(' ', '_')
|
||||
if coursename in COURSE_SETTINGS:
|
||||
return COURSE_SETTINGS[coursename]
|
||||
return None
|
||||
|
||||
|
||||
def is_valid_course(coursename):
|
||||
return get_course_settings(coursename) != None
|
||||
|
||||
def get_course_property(coursename,property):
|
||||
|
||||
def get_course_property(coursename, property):
|
||||
cs = get_course_settings(coursename)
|
||||
if not cs: return '' # raise exception instead?
|
||||
if property in cs: return cs[property]
|
||||
return '' # default
|
||||
|
||||
# raise exception instead?
|
||||
if not cs:
|
||||
return ''
|
||||
|
||||
if property in cs:
|
||||
return cs[property]
|
||||
|
||||
# default
|
||||
return ''
|
||||
|
||||
|
||||
def get_course_xmlpath(coursename):
|
||||
return get_course_property(coursename,'xmlpath')
|
||||
return get_course_property(coursename, 'xmlpath')
|
||||
|
||||
|
||||
def get_course_title(coursename):
|
||||
return get_course_property(coursename,'title')
|
||||
return get_course_property(coursename, 'title')
|
||||
|
||||
|
||||
def get_course_number(coursename):
|
||||
return get_course_property(coursename,'number')
|
||||
|
||||
return get_course_property(coursename, 'number')
|
||||
|
||||
|
||||
def get_course_location(coursename):
|
||||
return get_course_property(coursename, 'location')
|
||||
|
||||
@@ -132,10 +132,25 @@ COURSE_DEFAULT = '6.002_Spring_2012'
|
||||
COURSE_SETTINGS = {'6.002_Spring_2012': {'number' : '6.002x',
|
||||
'title' : 'Circuits and Electronics',
|
||||
'xmlpath': '6002x/',
|
||||
'location': 'i4x://edx/6002xs12/course/6.002_Spring_2012',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
############################### XModule Store ##################################
|
||||
KEYSTORE = {
|
||||
'default': {
|
||||
'ENGINE': 'keystore.xml.XMLModuleStore',
|
||||
'OPTIONS': {
|
||||
'org': 'edx',
|
||||
'course': '6002xs12',
|
||||
'data_dir': DATA_DIR,
|
||||
'default_class': 'xmodule.hidden_module.HiddenDescriptor',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
############################### DJANGO BUILT-INS ###############################
|
||||
# Change DEBUG/TEMPLATE_DEBUG in your environment settings files, not here
|
||||
DEBUG = False
|
||||
|
||||
@@ -11,7 +11,7 @@ from .common import *
|
||||
from .logsettings import get_logger_config
|
||||
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = True
|
||||
TEMPLATE_DEBUG = False
|
||||
|
||||
LOGGING = get_logger_config(ENV_ROOT / "log",
|
||||
logging_env="dev",
|
||||
|
||||
@@ -174,7 +174,7 @@ def quickedit(request, id=None, qetemplate='quickedit.html',coursename=None):
|
||||
module = 'problem'
|
||||
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)
|
||||
system = I4xSystem(track_function = make_track_function(request),
|
||||
@@ -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,
|
||||
|
||||
@@ -20,8 +20,8 @@ class @Courseware
|
||||
id = $(this).attr('id').replace(/video_/, '')
|
||||
new Video id, $(this).data('streams')
|
||||
$('.course-content .problems-wrapper').each ->
|
||||
id = $(this).attr('id').replace(/problem_/, '')
|
||||
new Problem id, $(this).data('url')
|
||||
id = $(this).attr('problem-id')
|
||||
new Problem id, $(this).attr('id'), $(this).data('url')
|
||||
$('.course-content .histogram').each ->
|
||||
id = $(this).attr('id').replace(/histogram_/, '')
|
||||
new Histogram id, $(this).data('histogram')
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class @Problem
|
||||
constructor: (@id, url) ->
|
||||
@element = $("#problem_#{id}")
|
||||
constructor: (@id, @element_id, url) ->
|
||||
@element = $("##{element_id}")
|
||||
@render()
|
||||
|
||||
$: (selector) ->
|
||||
@@ -26,13 +26,13 @@ class @Problem
|
||||
@element.html(content)
|
||||
@bind()
|
||||
else
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_get", (response) =>
|
||||
$.postWithPrefix "/modx/#{@id}/problem_get", (response) =>
|
||||
@element.html(response.html)
|
||||
@bind()
|
||||
|
||||
check: =>
|
||||
Logger.log 'problem_check', @answers
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_check", @answers, (response) =>
|
||||
$.postWithPrefix "/modx/#{@id}/problem_check", @answers, (response) =>
|
||||
switch response.success
|
||||
when 'incorrect', 'correct'
|
||||
@render(response.contents)
|
||||
@@ -42,14 +42,14 @@ class @Problem
|
||||
|
||||
reset: =>
|
||||
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)
|
||||
@updateProgress response
|
||||
|
||||
show: =>
|
||||
if !@element.hasClass 'showed'
|
||||
Logger.log 'problem_show', problem: @id
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_show", (response) =>
|
||||
$.postWithPrefix "/modx/#{@id}/problem_show", (response) =>
|
||||
answers = response.answers
|
||||
$.each answers, (key, value) =>
|
||||
if $.isArray(value)
|
||||
@@ -69,7 +69,7 @@ class @Problem
|
||||
|
||||
save: =>
|
||||
Logger.log 'problem_save', @answers
|
||||
$.postWithPrefix "/modx/problem/#{@id}/problem_save", @answers, (response) =>
|
||||
$.postWithPrefix "/modx/#{@id}/problem_save", @answers, (response) =>
|
||||
if response.success
|
||||
alert 'Saved'
|
||||
@updateProgress response
|
||||
@@ -94,4 +94,4 @@ class @Problem
|
||||
element.schematic.update_value()
|
||||
@$(".CodeMirror").each (index, element) ->
|
||||
element.CodeMirror.save() if element.CodeMirror.save
|
||||
@answers = @$("[id^=input_#{@id}_]").serialize()
|
||||
@answers = @$("[id^=input_#{@element_id.replace(/problem_/, '')}_]").serialize()
|
||||
|
||||
@@ -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()
|
||||
@@ -88,7 +88,7 @@ class @Sequence
|
||||
if @position != new_position
|
||||
if @position != undefined
|
||||
@mark_visited @position
|
||||
$.postWithPrefix "/modx/#{@tag}/#{@id}/goto_position", position: new_position
|
||||
$.postWithPrefix "/modx/#{@id}/goto_position", position: new_position
|
||||
|
||||
@mark_active new_position
|
||||
@$('#seq_content').html @elements[new_position - 1].content
|
||||
|
||||
@@ -1 +1 @@
|
||||
<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() {
|
||||
|
||||
<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['subtitle']}
|
||||
${section['format']}
|
||||
%if 'due' in section and section['due']!="":
|
||||
due ${section['due']}
|
||||
%endif
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div id="sequence_${id}" class="sequence">
|
||||
<div id="sequence_${element_id}" class="sequence">
|
||||
<nav aria-label="Section Navigation" class="sequence-nav">
|
||||
<ol id="sequence-list">
|
||||
</ol>
|
||||
@@ -22,7 +22,7 @@
|
||||
<%block name="js_extra">
|
||||
<script type="text/javascript">
|
||||
$(function(){
|
||||
new Sequence('${id}', ${items}, '${tag}', ${position});
|
||||
new Sequence('${item_id}', '${element_id}', ${items}, '${tag}', ${position});
|
||||
});
|
||||
</script>
|
||||
</%block>
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
${module_content}
|
||||
<div class="staff_info">
|
||||
${xml | h}
|
||||
definition = ${definition | h}
|
||||
metadata = ${metadata | h}
|
||||
</div>
|
||||
%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
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<ol class="vert-mod">
|
||||
% for t in items:
|
||||
<li id="vert-${items.index(t)}">
|
||||
${t[1]['content']}
|
||||
% for idx, item in enumerate(items):
|
||||
<li id="vert-${idx}">
|
||||
${item}
|
||||
</li>
|
||||
% endfor
|
||||
</ol>
|
||||
|
||||
@@ -13,11 +13,3 @@
|
||||
</article>
|
||||
</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:
|
||||
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'^jumpto/(?P<probname>[^/]+)/$', 'courseware.views.jump_to'),
|
||||
url(r'^section/(?P<section>[^/]*)/$', 'courseware.views.render_section'),
|
||||
url(r'^modx/(?P<module>[^/]*)/(?P<id>[^/]*)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
|
||||
url(r'^modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
|
||||
url(r'^profile$', 'courseware.views.profile'),
|
||||
url(r'^profile/(?P<student_id>[^/]*)/$', 'courseware.views.profile'),
|
||||
url(r'^change_setting$', 'student.views.change_setting'),
|
||||
|
||||
Reference in New Issue
Block a user