Commit 1509cdcc by ichuang

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

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

Note that stable-edx4edx will now no longer be a clean merge.  But this code has priority.
parents ff96fbf5 b5cd0184
......@@ -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 &amp; 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]
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):
"""
......
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')
)
import logging
from fs.osfs import OSFS
from importlib import import_module
from lxml import etree
from path import path
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
from . import ModuleStore, Location
from .exceptions import ItemNotFoundError
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True))
log = logging.getLogger(__name__)
class XMLModuleStore(ModuleStore):
"""
An XML backed ModuleStore
"""
def __init__(self, org, course, data_dir, default_class=None):
self.data_dir = path(data_dir)
self.modules = {}
module_path, _, class_name = default_class.rpartition('.')
class_ = getattr(import_module(module_path), class_name)
self.default_class = class_
with open(self.data_dir / "course.xml") as course_file:
class ImportSystem(XMLParsingSystem):
def __init__(self, modulestore):
"""
modulestore: the XMLModuleStore to store the loaded modules in
"""
self.unnamed_modules = 0
def process_xml(xml):
try:
xml_data = etree.fromstring(xml)
except:
log.exception("Unable to parse xml: {xml}".format(xml=xml))
raise
if xml_data.get('name'):
xml_data.set('slug', Location.clean(xml_data.get('name')))
else:
self.unnamed_modules += 1
xml_data.set('slug', '{tag}_{count}'.format(tag=xml_data.tag, count=self.unnamed_modules))
module = XModuleDescriptor.load_from_xml(etree.tostring(xml_data), self, org, course, modulestore.default_class)
modulestore.modules[module.location] = module
return module
XMLParsingSystem.__init__(self, modulestore.get_item, OSFS(data_dir), process_xml)
ImportSystem(self).process_xml(course_file.read())
def get_item(self, location):
"""
Returns an XModuleDescriptor instance for the item at location.
If location.revision is None, returns the most item with the most
recent revision
If any segment of the location is None except revision, raises
keystore.exceptions.InsufficientSpecificationError
If no object is found at that location, raises keystore.exceptions.ItemNotFoundError
location: Something that can be passed to Location
"""
location = Location(location)
try:
return self.modules[location]
except KeyError:
raise ItemNotFoundError(location)
def create_item(self, location):
raise NotImplementedError("XMLModuleStores are read-only")
def update_item(self, location, data):
"""
Set the data in the item specified by the location to
data
location: Something that can be passed to Location
data: A nested dictionary of problem data
"""
raise NotImplementedError("XMLModuleStores are read-only")
def update_children(self, location, children):
"""
Set the children for the item specified by the location to
data
location: Something that can be passed to Location
children: A list of child item identifiers
"""
raise NotImplementedError("XMLModuleStores are read-only")
import capa_module
import html_module
import schematic_module
import seq_module
import template_module
import vertical_module
import video_module
# Import all files in modules directory, excluding backups (# and . in name)
# and __init__
#
# Stick them in a list
# modx_module_list = []
# for f in os.listdir(os.path.dirname(__file__)):
# if f!='__init__.py' and \
# f[-3:] == ".py" and \
# "." not in f[:-3] \
# and '#' not in f:
# mod_path = 'courseware.modules.'+f[:-3]
# mod = __import__(mod_path, fromlist = "courseware.modules")
# if 'Module' in mod.__dict__:
# modx_module_list.append(mod)
#print modx_module_list
modx_module_list = [capa_module, html_module, schematic_module, seq_module, template_module, vertical_module, video_module]
#print modx_module_list
modx_modules = {}
# Convert list to a dictionary for lookup by tag
def update_modules():
global modx_modules
modx_modules = dict()
for module in modx_module_list:
for tag in module.Module.get_xml_tags():
modx_modules[tag] = module.Module
update_modules()
def get_module_class(tag):
''' Given an XML tag (e.g. 'video'), return
the associated module (e.g. video_module.Module).
'''
if tag not in modx_modules:
update_modules()
return modx_modules[tag]
def get_module_id(tag):
''' Given an XML tag (e.g. 'video'), return
the default ID for that module (e.g. 'youtube_id')
'''
return modx_modules[tag].id_attribute
def get_valid_tags():
return modx_modules.keys()
def get_default_ids():
tags = get_valid_tags()
ids = map(get_module_id, tags)
return dict(zip(tags, ids))
import json
import random
from lxml import etree
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
from xmodule.xml_module import XmlDescriptor
from xmodule.exceptions import InvalidDefinitionError
def group_from_value(groups, v):
''' Given group: (('a',0.3),('b',0.4),('c',0.3)) And random value
in [0,1], return the associated group (in the above case, return
'a' if v<0.3, 'b' if 0.3<=v<0.7, and 'c' if v>0.7
'''
sum = 0
for (g, p) in groups:
sum = sum + p
if sum > v:
return g
# Round off errors might cause us to run to the end of the list
# If the do, return the last element
return g
class ABTestModule(XModule):
"""
Implements an A/B test with an aribtrary number of competing groups
Format:
<abtest>
<group name="a" portion=".1"><contenta/></group>
<group name="b" portion=".2"><contentb/></group>
<default><contentdefault/></default>
</abtest>
"""
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
target_groups = self.definition['data'].keys()
if shared_state is None:
self.group = group_from_value(
self.definition['data']['group_portions'],
random.uniform(0, 1)
)
else:
shared_state = json.loads(shared_state)
# TODO (cpennington): Remove this once we aren't passing in
# groups from django groups
if 'groups' in shared_state:
self.group = None
target_names = [elem.get('name') for elem in target_groups]
for group in shared_state['groups']:
if group in target_names:
self.group = group
break
else:
self.group = shared_state['group']
def get_shared_state(self):
print self.group
return json.dumps({'group': self.group})
def displayable_items(self):
return [self.system.get_module(child)
for child
in self.definition['data']['group_content'][self.group]]
class ABTestDescriptor(RawDescriptor, XmlDescriptor):
module_class = ABTestModule
def __init__(self, system, definition=None, **kwargs):
kwargs['shared_state_key'] = definition['data']['experiment']
RawDescriptor.__init__(self, system, definition, **kwargs)
@classmethod
def definition_from_xml(cls, xml_object, system):
experiment = xml_object.get('experiment')
if experiment is None:
raise InvalidDefinitionError("ABTests must specify an experiment. Not found in:\n{xml}".format(xml=etree.tostring(xml_object, pretty_print=True)))
definition = {
'data': {
'experiment': experiment,
'group_portions': [],
'group_content': {None: []},
},
'children': []}
for group in xml_object:
if group.tag == 'default':
name = None
else:
name = group.get('name')
definition['data']['group_portions'].append(
(name, float(group.get('portion', 0)))
)
child_content_urls = [
system.process_xml(etree.tostring(child)).location.url()
for child in group
]
definition['data']['group_content'][name] = child_content_urls
definition['children'].extend(child_content_urls)
default_portion = 1 - sum(portion for (name, portion) in definition['data']['group_portions'])
if default_portion < 0:
raise InvalidDefinitionError("ABTest portions must add up to less than or equal to 1")
definition['data']['group_portions'].append((None, default_portion))
return definition
class InvalidDefinitionError(Exception):
pass
from xmodule.x_module import XModule
from xmodule.raw_module import RawDescriptor
class HiddenModule(XModule):
pass
class HiddenDescriptor(RawDescriptor):
module_class = HiddenModule
import json
import 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)}}
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,31 +14,32 @@ 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
@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
if instance_state is not None:
state = json.loads(instance_state)
if 'position' in state:
self.position = int(state['position'])
def get_init_js(self):
self.render()
return self.init_js
# 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_destroy_js(self):
def get_instance_state(self):
return json.dumps({'position': self.position})
def get_html(self):
self.render()
return self.destroy_js
return self.content
def get_progress(self):
''' Return the total progress, adding total done and total available.
......@@ -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}
if self.xmltree.tag in ['sequential', 'videosequence']:
self.content = self.system.render_template('seq_module.html', params)
if self.xmltree.tag == 'tab':
self.content = self.system.render_template('tab_module.html', params)
log.debug("rendered content: %s", content)
self.rendered = True
def __init__(self, system, xml, item_id, state=None):
XModule.__init__(self, system, xml, item_id, state)
self.xmltree = etree.fromstring(xml)
self.position = 1
if state is not None:
state = json.loads(state)
if 'position' in state: self.position = int(state['position'])
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 position is specified in system, then use that instead
if system.get('position'):
self.position = int(system.get('position'))
self.content = self.system.render_template('seq_module.html', params)
self.rendered = True
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",
]
}
)
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))
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
from xmodule.x_module import XModuleDescriptor
from lxml import etree
class XmlDescriptor(XModuleDescriptor):
"""
Mixin class for standardized parsing of from xml
"""
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
Return the definition to be passed to the newly created descriptor
during from_xml
"""
raise NotImplementedError("%s does not implement definition_from_xml" % cls.__class__.__name__)
@classmethod
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for
this module
system: An XModuleSystem for interacting with external resources
org and course are optional strings that will be used in the generated modules
url identifiers
"""
xml_object = etree.fromstring(xml_data)
metadata = {}
for attr in ('format', 'graceperiod', 'showanswer', 'rerandomize', 'due'):
from_xml = xml_object.get(attr)
if from_xml is not None:
metadata[attr] = from_xml
if xml_object.get('graded') is not None:
metadata['graded'] = xml_object.get('graded') == 'true'
if xml_object.get('name') is not None:
metadata['display_name'] = xml_object.get('name')
return cls(
system,
cls.definition_from_xml(xml_object, system),
location=['i4x',
org,
course,
xml_object.tag,
xml_object.get('slug')],
metadata=metadata,
)
from lxml import etree
import random
import 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'))
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")) )
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
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)
chapters.append({'course':course,
'chapter' : c.get("name"),
'sections' : sections,})
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 = s.metadata.get('graded', False)
scores = []
for module in yield_descendents(s):
(correct, total) = get_score(student, module, student_module_cache)
if correct is None and total is None:
continue
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})
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
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]
return '/'.join([self.module_type, self.student.username, self.module_state_key, str(self.state)[:20]])
# TODO (cpennington): Remove these once the LMS switches to using XModuleDescriptors
class StudentModuleCache(object):
"""
A cache of StudentModules for a specific student
"""
def __init__(self, user, descriptor, depth=None):
'''
Find any StudentModule objects that are needed by any child modules of the
supplied descriptor. Avoids making multiple queries to the database
'''
if user.is_authenticated():
module_ids = self._get_module_state_keys(descriptor, depth)
# This works around a limitation in sqlite3 on the number of parameters
# that can be put into a single query
self.cache = []
chunk_size = 500
for id_chunk in [module_ids[i:i+chunk_size] for i in xrange(0, len(module_ids), chunk_size)]:
self.cache.extend(StudentModule.objects.filter(
student=user,
module_state_key__in=id_chunk)
)
# @classmethod
# 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)
else:
self.cache = []
# return student_module
def _get_module_state_keys(self, descriptor, depth):
'''
Get a list of the state_keys needed for StudentModules
required for this chunk of module xml
'''
keys = [descriptor.location.url()]
@classmethod
def key_for(cls, student, module_id):
return "StudentModule-student_id:{0};module_id:{1}".format(student.id, module_id)
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
# def clear_cache_by_student_and_module_id(sender, instance, *args, **kwargs):
# k = sender.key_for(instance.student, instance.module_id)
# cache.delete(k)
for child in descriptor.get_children():
keys.extend(self._get_module_state_keys(child, new_depth))
# 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)
return keys
def lookup(self, module_type, module_state_key):
'''
Look for a student module with the given type and id in the cache.
#post_save.connect(update_cache_by_student_and_module_id, sender=StudentModule, weak=False)
#post_delete.connect(clear_cache_by_student_and_module_id, sender=StudentModule, weak=False)
cache -- list of student modules
#cache_model(StudentModule)
returns first found object, or None
'''
for o in self.cache:
if o.module_type == module_type and o.module_state_key == module_state_key:
return o
return None
def append(self, student_module):
self.cache.append(student_module)
......@@ -31,11 +31,13 @@ if hasattr(settings,'COURSE_SETTINGS'): # in the future, this could be repla
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')
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()
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
......
<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
......
<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>
${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
<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'),
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment