' % traceback.format_exc().replace('<', '<')
# create a dummy problem with error message instead of failing
- problem_text = 'Problem %s has an error:%s' % (self.location.url(), msg)
- self.lcp = LoncapaProblem(problem_text, self.location.html_id(), instance_state, seed=seed, system=self.system)
+ problem_text = (''
+ 'Problem %s has an error:%s' %
+ (self.location.url(), msg))
+ self.lcp = LoncapaProblem(
+ problem_text, 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
+ Property accessor that returns self.metadata['rerandomize'] in a
+ canonical form
"""
rerandomize = self.metadata.get('rerandomize', 'always')
if rerandomize in ("", "always", "true"):
@@ -203,7 +212,10 @@ class CapaModule(XModule):
except Exception, err:
if self.system.DEBUG:
log.exception(err)
- msg = '[courseware.capa.capa_module] Failed to generate HTML for problem %s' % (self.location.url())
+ msg = (
+ '[courseware.capa.capa_module] '
+ 'Failed to generate HTML for problem %s' %
+ (self.location.url()))
msg += '
Error:
%s
' % str(err).replace('<', '<')
msg += '
%s
' % traceback.format_exc().replace('<', '<')
html = msg
@@ -215,8 +227,8 @@ class CapaModule(XModule):
'weight': self.weight,
}
- # We using strings as truthy values, because the terminology of the check button
- # is context-specific.
+ # We using strings as truthy values, because the terminology of the
+ # check button is context-specific.
check_button = "Grade" if self.max_attempts else "Check"
reset_button = True
save_button = True
@@ -242,7 +254,8 @@ class CapaModule(XModule):
if not self.lcp.done:
reset_button = False
- # We don't need a "save" button if infinite number of attempts and non-randomized
+ # We don't need a "save" button if infinite number of attempts and
+ # non-randomized
if self.max_attempts is None and self.rerandomize != "always":
save_button = False
@@ -517,11 +530,13 @@ class CapaModule(XModule):
self.lcp.do_reset()
if self.rerandomize == "always":
- # reset random number generator seed (note the self.lcp.get_state() in next line)
+ # reset random number generator seed (note the self.lcp.get_state()
+ # in next line)
self.lcp.seed = None
self.lcp = LoncapaProblem(self.definition['data'],
- self.location.html_id(), self.lcp.get_state(), system=self.system)
+ self.location.html_id(), self.lcp.get_state(),
+ system=self.system)
event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info)
@@ -537,6 +552,7 @@ class CapaDescriptor(RawDescriptor):
module_class = CapaModule
+ # VS[compat]
# TODO (cpennington): Delete this method once all fall 2012 course are being
# edited in the cms
@classmethod
@@ -545,3 +561,7 @@ class CapaDescriptor(RawDescriptor):
'problems/' + path[8:],
path[8:],
]
+ @classmethod
+ def split_to_file(cls, xml_object):
+ '''Problems always written in their own files'''
+ return True
diff --git a/common/lib/xmodule/xmodule/course_module.py b/common/lib/xmodule/xmodule/course_module.py
index a04324237c..dfac1ac9c6 100644
--- a/common/lib/xmodule/xmodule/course_module.py
+++ b/common/lib/xmodule/xmodule/course_module.py
@@ -10,6 +10,7 @@ log = logging.getLogger(__name__)
class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule
+ metadata_attributes = SequenceDescriptor.metadata_attributes + ('org', 'course')
def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs)
@@ -17,23 +18,40 @@ class CourseDescriptor(SequenceDescriptor):
try:
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
except KeyError:
- self.start = time.gmtime(0) # The epoch
- log.critical("Course loaded without a start date. " + str(self.id))
- except ValueError, e:
- self.start = time.gmtime(0) # The epoch
- log.critical("Course loaded with a bad start date. " + str(self.id) + " '" + str(e) + "'")
+ self.start = time.gmtime(0) #The epoch
+ log.critical("Course loaded without a start date. %s", self.id)
+ except ValueError as e:
+ self.start = time.gmtime(0) #The epoch
+ log.critical("Course loaded with a bad start date. %s '%s'",
+ self.id, e)
def has_started(self):
return time.gmtime() > self.start
- @classmethod
- def id_to_location(cls, course_id):
+ @staticmethod
+ def id_to_location(course_id):
+ '''Convert the given course_id (org/course/name) to a location object.
+ Throws ValueError if course_id is of the wrong format.
+ '''
org, course, name = course_id.split('/')
return Location('i4x', org, course, 'course', name)
+ @staticmethod
+ def location_to_id(location):
+ '''Convert a location of a course to a course_id. If location category
+ is not "course", raise a ValueError.
+
+ location: something that can be passed to Location
+ '''
+ loc = Location(location)
+ if loc.category != "course":
+ raise ValueError("{0} is not a course location".format(loc))
+ return "/".join([loc.org, loc.course, loc.name])
+
+
@property
def id(self):
- return "/".join([self.location.org, self.location.course, self.location.name])
+ return self.location_to_id(self.location)
@property
def start_date_text(self):
diff --git a/common/lib/xmodule/xmodule/errorhandlers.py b/common/lib/xmodule/xmodule/errorhandlers.py
new file mode 100644
index 0000000000..0f97377b2a
--- /dev/null
+++ b/common/lib/xmodule/xmodule/errorhandlers.py
@@ -0,0 +1,45 @@
+import logging
+import sys
+
+log = logging.getLogger(__name__)
+
+def in_exception_handler():
+ '''Is there an active exception?'''
+ return sys.exc_info() != (None, None, None)
+
+def strict_error_handler(msg, exc_info=None):
+ '''
+ Do not let errors pass. If exc_info is not None, ignore msg, and just
+ re-raise. Otherwise, check if we are in an exception-handling context.
+ If so, re-raise. Otherwise, raise Exception(msg).
+
+ Meant for use in validation, where any errors should trap.
+ '''
+ if exc_info is not None:
+ raise exc_info[0], exc_info[1], exc_info[2]
+
+ if in_exception_handler():
+ raise
+
+ raise Exception(msg)
+
+
+def logging_error_handler(msg, exc_info=None):
+ '''Log all errors, but otherwise let them pass, relying on the caller to
+ workaround.'''
+ if exc_info is not None:
+ log.exception(msg, exc_info=exc_info)
+ return
+
+ if in_exception_handler():
+ log.exception(msg)
+ return
+
+ log.error(msg)
+
+
+def ignore_errors_handler(msg, exc_info=None):
+ '''Ignore all errors, relying on the caller to workaround.
+ Meant for use in the LMS, where an error in one part of the course
+ shouldn't bring down the whole system'''
+ pass
diff --git a/common/lib/xmodule/xmodule/exceptions.py b/common/lib/xmodule/xmodule/exceptions.py
index 9107d9dc4d..3db5ceccde 100644
--- a/common/lib/xmodule/xmodule/exceptions.py
+++ b/common/lib/xmodule/xmodule/exceptions.py
@@ -1,5 +1,6 @@
class InvalidDefinitionError(Exception):
pass
+
class NotFoundError(Exception):
pass
diff --git a/common/lib/xmodule/xmodule/html_module.py b/common/lib/xmodule/xmodule/html_module.py
index 97509c6f34..b9bc34aed6 100644
--- a/common/lib/xmodule/xmodule/html_module.py
+++ b/common/lib/xmodule/xmodule/html_module.py
@@ -12,8 +12,10 @@ 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)
+ 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']
@@ -42,3 +44,8 @@ class HtmlDescriptor(RawDescriptor):
def file_to_xml(cls, file_object):
parser = etree.HTMLParser()
return etree.parse(file_object, parser).getroot()
+
+ @classmethod
+ def split_to_file(cls, xml_object):
+ # never include inline html
+ return True
diff --git a/common/lib/xmodule/xmodule/js/src/capa/schematic.js b/common/lib/xmodule/xmodule/js/src/capa/schematic.js
index a92bca65cd..56c4bc8195 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/schematic.js
+++ b/common/lib/xmodule/xmodule/js/src/capa/schematic.js
@@ -52,6 +52,7 @@ function update_schematics() {
schematics[i].setAttribute("loaded","true");
}
}
+window.update_schematics = update_schematics;
// add ourselves to the tasks that get performed when window is loaded
function add_schematic_handler(other_onload) {
diff --git a/common/lib/xmodule/xmodule/mako_module.py b/common/lib/xmodule/xmodule/mako_module.py
index 9a90afb896..fcc47aaaaf 100644
--- a/common/lib/xmodule/xmodule/mako_module.py
+++ b/common/lib/xmodule/xmodule/mako_module.py
@@ -2,9 +2,12 @@ from x_module import XModuleDescriptor, DescriptorSystem
class MakoDescriptorSystem(DescriptorSystem):
- def __init__(self, render_template, *args, **kwargs):
+ def __init__(self, load_item, resources_fs, error_handler,
+ render_template):
+ super(MakoDescriptorSystem, self).__init__(
+ load_item, resources_fs, error_handler)
+
self.render_template = render_template
- super(MakoDescriptorSystem, self).__init__(*args, **kwargs)
class MakoModuleDescriptor(XModuleDescriptor):
@@ -19,7 +22,9 @@ class MakoModuleDescriptor(XModuleDescriptor):
def __init__(self, system, definition=None, **kwargs):
if getattr(system, 'render_template', None) is None:
- raise TypeError('{system} must have a render_template function in order to use a MakoDescriptor'.format(system=system))
+ raise TypeError('{system} must have a render_template function'
+ ' in order to use a MakoDescriptor'.format(
+ system=system))
super(MakoModuleDescriptor, self).__init__(system, definition, **kwargs)
def get_context(self):
@@ -29,4 +34,5 @@ class MakoModuleDescriptor(XModuleDescriptor):
return {'module': self}
def get_html(self):
- return self.system.render_template(self.mako_template, self.get_context())
+ return self.system.render_template(
+ self.mako_template, self.get_context())
diff --git a/common/lib/xmodule/xmodule/modulestore/__init__.py b/common/lib/xmodule/xmodule/modulestore/__init__.py
index 5527d4108e..279782b61a 100644
--- a/common/lib/xmodule/xmodule/modulestore/__init__.py
+++ b/common/lib/xmodule/xmodule/modulestore/__init__.py
@@ -45,13 +45,28 @@ class Location(_LocationBase):
"""
return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
- def __new__(_cls, loc_or_tag=None, org=None, course=None, category=None, name=None, revision=None):
+ @classmethod
+ def is_valid(cls, value):
+ '''
+ Check if the value is a valid location, in any acceptable format.
+ '''
+ try:
+ Location(value)
+ except InvalidLocationError:
+ return False
+ return True
+
+ def __new__(_cls, loc_or_tag=None, org=None, course=None, category=None,
+ name=None, revision=None):
"""
Create a new location that is a clone of the specifed one.
location - Can be any of the following types:
- string: should be of the form {tag}://{org}/{course}/{category}/{name}[/{revision}]
+ string: should be of the form
+ {tag}://{org}/{course}/{category}/{name}[/{revision}]
+
list: should be of the form [tag, org, course, category, name, revision]
+
dict: should be of the form {
'tag': tag,
'org': org,
@@ -62,16 +77,19 @@ class Location(_LocationBase):
}
Location: another Location object
- In both the dict and list forms, the revision is optional, and can be ommitted.
+ In both the dict and list forms, the revision is optional, and can be
+ ommitted.
- Components must be composed of alphanumeric characters, or the characters '_', '-', and '.'
+ 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
+ Components may be set to None, which may be interpreted by some contexts
+ to mean wildcard selection
"""
- if org is None and course is None and category is None and name is None and revision is None:
+ 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)
@@ -131,9 +149,11 @@ class Location(_LocationBase):
def html_id(self):
"""
- Return a string with a version of the location that is safe for use in html id attributes
+ Return a string with a version of the location that is safe for use in
+ html id attributes
"""
- return "-".join(str(v) for v in self.list() if v is not None).replace('.', '_')
+ return "-".join(str(v) for v in self.list()
+ if v is not None).replace('.', '_')
def dict(self):
"""
@@ -154,7 +174,8 @@ class Location(_LocationBase):
class ModuleStore(object):
"""
- An abstract interface for a database backend that stores XModuleDescriptor instances
+ An abstract interface for a database backend that stores XModuleDescriptor
+ instances
"""
def get_item(self, location, depth=0):
"""
@@ -164,13 +185,16 @@ class ModuleStore(object):
If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError
- If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError
+
+ If no object is found at that location, raises
+ xmodule.modulestore.exceptions.ItemNotFoundError
location: Something that can be passed to Location
- depth (int): An argument that some module stores may use to prefetch descendents of the queried modules
- for more efficient results later in the request. The depth is counted in the number of
- calls to get_children() to cache. None indicates to cache all descendents
+ depth (int): An argument that some module stores may use to prefetch
+ descendents of the queried modules for more efficient results later
+ in the request. The depth is counted in the number of calls to
+ get_children() to cache. None indicates to cache all descendents
"""
raise NotImplementedError
@@ -182,9 +206,10 @@ class ModuleStore(object):
location: Something that can be passed to Location
- depth: An argument that some module stores may use to prefetch descendents of the queried modules
- for more efficient results later in the request. The depth is counted in the number of calls
- to get_children() to cache. None indicates to cache all descendents
+ depth: An argument that some module stores may use to prefetch
+ descendents of the queried modules for more efficient results later
+ in the request. The depth is counted in the number of calls to
+ get_children() to cache. None indicates to cache all descendents
"""
raise NotImplementedError
@@ -229,3 +254,25 @@ class ModuleStore(object):
'''
raise NotImplementedError
+ def path_to_location(self, location, course=None, chapter=None, section=None):
+ '''
+ Try to find a course/chapter/section[/position] path to this location.
+
+ raise ItemNotFoundError if the location doesn't exist.
+
+ If course, chapter, section are not None, restrict search to paths with those
+ components as specified.
+
+ raise NoPathToItem if the location exists, but isn't accessible via
+ a path that matches the course/chapter/section restrictions.
+
+ In general, a location may be accessible via many paths. This method may
+ return any valid path.
+
+ Return a tuple (course, chapter, section, position).
+
+ If the section a sequence, position should be the position of this location
+ in that sequence. Otherwise, position should be None.
+ '''
+ raise NotImplementedError
+
diff --git a/common/lib/xmodule/xmodule/modulestore/exceptions.py b/common/lib/xmodule/xmodule/modulestore/exceptions.py
index a6dc99883f..a63efc3e43 100644
--- a/common/lib/xmodule/xmodule/modulestore/exceptions.py
+++ b/common/lib/xmodule/xmodule/modulestore/exceptions.py
@@ -13,3 +13,11 @@ class InsufficientSpecificationError(Exception):
class InvalidLocationError(Exception):
pass
+
+
+class NoPathToItem(Exception):
+ pass
+
+
+class DuplicateItemError(Exception):
+ pass
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index 7cd005336c..df4e20f3a7 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -1,17 +1,20 @@
import pymongo
-from bson.objectid import ObjectId
from bson.son import SON
from fs.osfs import OSFS
from itertools import repeat
+from path import path
from importlib import import_module
+from xmodule.errorhandlers import strict_error_handler
from xmodule.x_module import XModuleDescriptor
from xmodule.mako_module import MakoDescriptorSystem
+from xmodule.course_module import CourseDescriptor
from mitxmako.shortcuts import render_to_string
from . import ModuleStore, Location
-from .exceptions import ItemNotFoundError, InsufficientSpecificationError
+from .exceptions import (ItemNotFoundError, InsufficientSpecificationError,
+ NoPathToItem, DuplicateItemError)
# TODO (cpennington): This code currently operates under the assumption that
# there is only one revision for each item. Once we start versioning inside the CMS,
@@ -23,15 +26,26 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
A system that has a cache of module json that it will use to load modules
from, with a backup of calling to the underlying modulestore for more data
"""
- def __init__(self, modulestore, module_data, default_class, resources_fs, render_template):
+ def __init__(self, modulestore, module_data, default_class, resources_fs,
+ error_handler, render_template):
"""
modulestore: the module store that can be used to retrieve additional modules
- module_data: a dict mapping Location -> json that was cached from the underlying modulestore
- default_class: The default_class to use when loading an XModuleDescriptor from the module_data
+
+ module_data: a dict mapping Location -> json that was cached from the
+ underlying modulestore
+
+ default_class: The default_class to use when loading an
+ XModuleDescriptor from the module_data
+
resources_fs: a filesystem, as per MakoDescriptorSystem
- render_template: a function for rendering templates, as per MakoDescriptorSystem
+
+ error_handler:
+
+ render_template: a function for rendering templates, as per
+ MakoDescriptorSystem
"""
- super(CachingDescriptorSystem, self).__init__(render_template, self.load_item, resources_fs)
+ super(CachingDescriptorSystem, self).__init__(
+ self.load_item, resources_fs, error_handler, render_template)
self.modulestore = modulestore
self.module_data = module_data
self.default_class = default_class
@@ -83,7 +97,7 @@ class MongoModuleStore(ModuleStore):
module_path, _, class_name = default_class.rpartition('.')
class_ = getattr(import_module(module_path), class_name)
self.default_class = class_
- self.fs_root = fs_root
+ self.fs_root = path(fs_root)
def _clean_item_data(self, item):
"""
@@ -127,19 +141,23 @@ class MongoModuleStore(ModuleStore):
"""
Load an XModuleDescriptor from item, using the children stored in data_cache
"""
- resource_fs = OSFS(self.fs_root / item.get('data_dir', item['location']['course']))
+ data_dir = item.get('metadata', {}).get('data_dir', item['location']['course'])
+ resource_fs = OSFS(self.fs_root / data_dir)
+
system = CachingDescriptorSystem(
self,
data_cache,
self.default_class,
resource_fs,
- render_to_string
+ strict_error_handler,
+ render_to_string,
)
return system.load_item(item['location'])
def _load_items(self, items, depth=0):
"""
- Load a list of xmodules from the data in items, with children cached up to specified depth
+ Load a list of xmodules from the data in items, with children cached up
+ to specified depth
"""
data_cache = self._cache_children(items, depth)
@@ -153,6 +171,14 @@ class MongoModuleStore(ModuleStore):
course_filter = Location("i4x", category="course")
return self.get_items(course_filter)
+ def _find_one(self, location):
+ '''Look for a given location in the collection.
+ If revision isn't specified, returns the latest.'''
+ return self.collection.find_one(
+ location_to_query(location),
+ sort=[('revision', pymongo.ASCENDING)],
+ )
+
def get_item(self, location, depth=0):
"""
Returns an XModuleDescriptor instance for the item at location.
@@ -176,10 +202,7 @@ class MongoModuleStore(ModuleStore):
if key != 'revision' and val is None:
raise InsufficientSpecificationError(location)
- item = self.collection.find_one(
- location_to_query(location),
- sort=[('revision', pymongo.ASCENDING)],
- )
+ item = self._find_one(location)
if item is None:
raise ItemNotFoundError(location)
return self._load_items([item], depth)[0]
@@ -192,15 +215,22 @@ class MongoModuleStore(ModuleStore):
return self._load_items(list(items), depth)
+ # TODO (cpennington): This needs to be replaced by clone_item as soon as we allow
+ # creation of items from the cms
def create_item(self, location):
"""
- Create an empty item at the specified location with the supplied editor
+ Create an empty item at the specified location.
+
+ If that location already exists, raises a DuplicateItemError
location: Something that can be passed to Location
"""
- self.collection.insert({
- '_id': Location(location).dict(),
- })
+ try:
+ self.collection.insert({
+ '_id': Location(location).dict(),
+ })
+ except pymongo.errors.DuplicateKeyError:
+ raise DuplicateItemError(location)
def update_item(self, location, data):
"""
@@ -237,7 +267,7 @@ class MongoModuleStore(ModuleStore):
def update_metadata(self, location, metadata):
"""
- Set the children for the item specified by the location to
+ Set the metadata for the item specified by the location to
metadata
location: Something that can be passed to Location
@@ -250,3 +280,98 @@ class MongoModuleStore(ModuleStore):
{'_id': Location(location).dict()},
{'$set': {'metadata': metadata}}
)
+
+ def get_parent_locations(self, location):
+ '''Find all locations that are the parents of this location.
+ Mostly intended for use in path_to_location, but exposed for testing
+ and possible other usefulness.
+
+ returns an iterable of things that can be passed to Location.
+ '''
+ location = Location(location)
+ items = self.collection.find({'definition.children': str(location)},
+ {'_id': True})
+ return [i['_id'] for i in items]
+
+ def path_to_location(self, location, course_name=None):
+ '''
+ Try to find a course_id/chapter/section[/position] path to this location.
+ The courseware insists that the first level in the course is chapter,
+ but any kind of module can be a "section".
+
+ location: something that can be passed to Location
+ course_name: [optional]. If not None, restrict search to paths
+ in that course.
+
+ raise ItemNotFoundError if the location doesn't exist.
+
+ raise NoPathToItem if the location exists, but isn't accessible via
+ a chapter/section path in the course(s) being searched.
+
+ Return a tuple (course_id, chapter, section, position) suitable for the
+ courseware index view.
+
+ A location may be accessible via many paths. This method may
+ return any valid path.
+
+ If the section is a sequence, position will be the position
+ of this location in that sequence. Otherwise, position will
+ be None. TODO (vshnayder): Not true yet.
+ '''
+ # Check that location is present at all
+ if self._find_one(location) is None:
+ raise ItemNotFoundError(location)
+
+ def flatten(xs):
+ '''Convert lisp-style (a, (b, (c, ()))) lists into a python list.
+ Not a general flatten function. '''
+ p = []
+ while xs != ():
+ p.append(xs[0])
+ xs = xs[1]
+ return p
+
+ def find_path_to_course(location, course_name=None):
+ '''Find a path up the location graph to a node with the
+ specified category. If no path exists, return None. If a
+ path exists, return it as a list with target location
+ first, and the starting location last.
+ '''
+ # Standard DFS
+
+ # To keep track of where we came from, the work queue has
+ # tuples (location, path-so-far). To avoid lots of
+ # copying, the path-so-far is stored as a lisp-style
+ # list--nested hd::tl tuples, and flattened at the end.
+ queue = [(location, ())]
+ while len(queue) > 0:
+ (loc, path) = queue.pop() # Takes from the end
+ loc = Location(loc)
+ # print 'Processing loc={0}, path={1}'.format(loc, path)
+ if loc.category == "course":
+ if course_name is None or course_name == loc.name:
+ # Found it!
+ path = (loc, path)
+ return flatten(path)
+
+ # otherwise, add parent locations at the end
+ newpath = (loc, path)
+ parents = self.get_parent_locations(loc)
+ queue.extend(zip(parents, repeat(newpath)))
+
+ # If we're here, there is no path
+ return None
+
+ path = find_path_to_course(location, course_name)
+ if path is None:
+ raise(NoPathToItem(location))
+
+ n = len(path)
+ course_id = CourseDescriptor.location_to_id(path[0])
+ chapter = path[1].name if n > 1 else None
+ section = path[2].name if n > 2 else None
+
+ # TODO (vshnayder): not handling position at all yet...
+ position = None
+
+ return (course_id, chapter, section, position)
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_location.py b/common/lib/xmodule/xmodule/modulestore/tests/test_location.py
index 19bdb105c1..70c6351685 100644
--- a/common/lib/xmodule/xmodule/modulestore/tests/test_location.py
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_location.py
@@ -13,14 +13,51 @@ def test_string_roundtrip():
check_string_roundtrip("tag://org/course/category/name/revision")
+input_dict = {
+ 'tag': 'tag',
+ 'course': 'course',
+ 'category': 'category',
+ 'name': 'name',
+ 'org': 'org'
+}
+
+input_list = ['tag', 'org', 'course', 'category', 'name']
+
+input_str = "tag://org/course/category/name"
+input_str_rev = "tag://org/course/category/name/revision"
+
+valid = (input_list, input_dict, input_str, input_str_rev)
+
+invalid_dict = {
+ 'tag': 'tag',
+ 'course': 'course',
+ 'category': 'category',
+ 'name': 'name/more_name',
+ 'org': 'org'
+}
+
+invalid_dict2 = {
+ 'tag': 'tag',
+ 'course': 'course',
+ 'category': 'category',
+ 'name': 'name ', # extra space
+ 'org': 'org'
+}
+
+invalid = ("foo", ["foo"], ["foo", "bar"],
+ ["foo", "bar", "baz", "blat", "foo/bar"],
+ "tag://org/course/category/name with spaces/revision",
+ invalid_dict,
+ invalid_dict2)
+
+def test_is_valid():
+ for v in valid:
+ assert_equals(Location.is_valid(v), True)
+
+ for v in invalid:
+ assert_equals(Location.is_valid(v), False)
+
def test_dict():
- input_dict = {
- 'tag': 'tag',
- 'course': 'course',
- 'category': 'category',
- 'name': 'name',
- 'org': 'org'
- }
assert_equals("tag://org/course/category/name", Location(input_dict).url())
assert_equals(dict(revision=None, **input_dict), Location(input_dict).dict())
@@ -30,7 +67,6 @@ def test_dict():
def test_list():
- input_list = ['tag', 'org', 'course', 'category', 'name']
assert_equals("tag://org/course/category/name", Location(input_list).url())
assert_equals(input_list + [None], Location(input_list).list())
@@ -65,3 +101,13 @@ def test_equality():
Location('tag', 'org', 'course', 'category', 'name1'),
Location('tag', 'org', 'course', 'category', 'name')
)
+
+def test_clean():
+ pairs = [ ('',''),
+ (' ', '_'),
+ ('abc,', 'abc_'),
+ ('ab fg!@//\\aj', 'ab_fg_aj'),
+ (u"ab\xA9", "ab_"), # no unicode allowed for now
+ ]
+ for input, output in pairs:
+ assert_equals(Location.clean(input), output)
diff --git a/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
new file mode 100644
index 0000000000..cb2bc6e20c
--- /dev/null
+++ b/common/lib/xmodule/xmodule/modulestore/tests/test_mongo.py
@@ -0,0 +1,136 @@
+import pymongo
+
+from nose.tools import assert_equals, assert_raises, assert_not_equals, with_setup
+from path import path
+from pprint import pprint
+
+from xmodule.modulestore import Location
+from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
+from xmodule.modulestore.mongo import MongoModuleStore
+from xmodule.modulestore.xml_importer import import_from_xml
+
+# from ~/mitx_all/mitx/common/lib/xmodule/xmodule/modulestore/tests/
+# to ~/mitx_all/mitx/common/test
+TEST_DIR = path(__file__).abspath().dirname()
+for i in range(5):
+ TEST_DIR = TEST_DIR.dirname()
+TEST_DIR = TEST_DIR / 'test'
+
+DATA_DIR = TEST_DIR / 'data'
+
+
+HOST = 'localhost'
+PORT = 27017
+DB = 'test'
+COLLECTION = 'modulestore'
+FS_ROOT = DATA_DIR # TODO (vshnayder): will need a real fs_root for testing load_item
+DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
+
+
+class TestMongoModuleStore(object):
+
+ @classmethod
+ def setupClass(cls):
+ cls.connection = pymongo.connection.Connection(HOST, PORT)
+ cls.connection.drop_database(DB)
+
+ # NOTE: Creating a single db for all the tests to save time. This
+ # is ok only as long as none of the tests modify the db.
+ # If (when!) that changes, need to either reload the db, or load
+ # once and copy over to a tmp db for each test.
+ cls.store = cls.initdb()
+
+ @classmethod
+ def teardownClass(cls):
+ pass
+
+ @staticmethod
+ def initdb():
+ # connect to the db
+ store = MongoModuleStore(HOST, DB, COLLECTION, FS_ROOT, default_class=DEFAULT_CLASS)
+ # Explicitly list the courses to load (don't want the big one)
+ courses = ['toy', 'simple']
+ import_from_xml(store, DATA_DIR, courses)
+ return store
+
+ @staticmethod
+ def destroy_db(connection):
+ # Destroy the test db.
+ connection.drop_database(DB)
+
+ def setUp(self):
+ # make a copy for convenience
+ self.connection = TestMongoModuleStore.connection
+
+ def tearDown(self):
+ pass
+
+ def test_init(self):
+ '''Make sure the db loads, and print all the locations in the db.
+ Call this directly from failing tests to see what's loaded'''
+ ids = list(self.connection[DB][COLLECTION].find({}, {'_id': True}))
+
+ pprint([Location(i['_id']).url() for i in ids])
+
+ def test_get_courses(self):
+ '''Make sure the course objects loaded properly'''
+ courses = self.store.get_courses()
+ assert_equals(len(courses), 2)
+ courses.sort(key=lambda c: c.id)
+ assert_equals(courses[0].id, 'edX/simple/2012_Fall')
+ assert_equals(courses[1].id, 'edX/toy/2012_Fall')
+
+ def test_loads(self):
+ assert_not_equals(
+ self.store.get_item("i4x://edX/toy/course/2012_Fall"),
+ None)
+
+ assert_not_equals(
+ self.store.get_item("i4x://edX/simple/course/2012_Fall"),
+ None)
+
+ assert_not_equals(
+ self.store.get_item("i4x://edX/toy/video/Welcome"),
+ None)
+
+
+
+ def test_find_one(self):
+ assert_not_equals(
+ self.store._find_one(Location("i4x://edX/toy/course/2012_Fall")),
+ None)
+
+ assert_not_equals(
+ self.store._find_one(Location("i4x://edX/simple/course/2012_Fall")),
+ None)
+
+ assert_not_equals(
+ self.store._find_one(Location("i4x://edX/toy/video/Welcome")),
+ None)
+
+ def test_path_to_location(self):
+ '''Make sure that path_to_location works'''
+ should_work = (
+ ("i4x://edX/toy/video/Welcome",
+ ("edX/toy/2012_Fall", "Overview", "Welcome", None)),
+ ("i4x://edX/toy/html/toylab",
+ ("edX/toy/2012_Fall", "Overview", "Toy_Videos", None)),
+ )
+ for location, expected in should_work:
+ assert_equals(self.store.path_to_location(location), expected)
+
+ not_found = (
+ "i4x://edX/toy/video/WelcomeX",
+ )
+ for location in not_found:
+ assert_raises(ItemNotFoundError, self.store.path_to_location, location)
+
+ # Since our test files are valid, there shouldn't be any
+ # elements with no path to them. But we can look for them in
+ # another course.
+ no_path = (
+ "i4x://edX/simple/video/Lost_Video",
+ )
+ for location in no_path:
+ assert_raises(NoPathToItem, self.store.path_to_location, location, "toy")
+
diff --git a/common/lib/xmodule/xmodule/modulestore/xml.py b/common/lib/xmodule/xmodule/modulestore/xml.py
index 3981009cef..7dd6868f78 100644
--- a/common/lib/xmodule/xmodule/modulestore/xml.py
+++ b/common/lib/xmodule/xmodule/modulestore/xml.py
@@ -3,6 +3,7 @@ from fs.osfs import OSFS
from importlib import import_module
from lxml import etree
from path import path
+from xmodule.errorhandlers import logging_error_handler
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
from xmodule.mako_module import MakoDescriptorSystem
from cStringIO import StringIO
@@ -12,153 +13,188 @@ import re
from . import ModuleStore, Location
from .exceptions import ItemNotFoundError
-etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
- remove_comments=True, remove_blank_text=True))
+etree.set_default_parser(
+ etree.XMLParser(dtd_validation=False, load_dtd=False,
+ remove_comments=True, remove_blank_text=True))
log = logging.getLogger('mitx.' + __name__)
-# TODO (cpennington): Remove this once all fall 2012 courses have been imported into the cms from xml
+# VS[compat]
+# TODO (cpennington): Remove this once all fall 2012 courses have been imported
+# into the cms from xml
def clean_out_mako_templating(xml_string):
xml_string = xml_string.replace('%include', 'include')
xml_string = re.sub("(?m)^\s*%.*$", '', xml_string)
return xml_string
+class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
+ def __init__(self, xmlstore, org, course, course_dir, error_handler):
+ """
+ A class that handles loading from xml. Does some munging to ensure that
+ all elements have unique slugs.
+
+ xmlstore: the XMLModuleStore to store the loaded modules in
+ """
+ self.unnamed_modules = 0
+ self.used_slugs = set()
+
+ def process_xml(xml):
+ try:
+ # VS[compat]
+ # TODO (cpennington): Remove this once all fall 2012 courses
+ # have been imported into the cms from xml
+ xml = clean_out_mako_templating(xml)
+ xml_data = etree.fromstring(xml)
+ except:
+ log.exception("Unable to parse xml: {xml}".format(xml=xml))
+ raise
+
+ # VS[compat]. Take this out once course conversion is done
+ if xml_data.get('slug') is None and xml_data.get('url_name') is None:
+ if xml_data.get('name'):
+ slug = Location.clean(xml_data.get('name'))
+ elif xml_data.get('display_name'):
+ slug = Location.clean(xml_data.get('display_name'))
+ else:
+ self.unnamed_modules += 1
+ slug = '{tag}_{count}'.format(tag=xml_data.tag,
+ count=self.unnamed_modules)
+
+ while slug in self.used_slugs:
+ self.unnamed_modules += 1
+ slug = '{slug}_{count}'.format(slug=slug,
+ count=self.unnamed_modules)
+
+ self.used_slugs.add(slug)
+ # log.debug('-> slug=%s' % slug)
+ xml_data.set('url_name', slug)
+
+ module = XModuleDescriptor.load_from_xml(
+ etree.tostring(xml_data), self, org,
+ course, xmlstore.default_class)
+
+ #log.debug('==> importing module location %s' % repr(module.location))
+ module.metadata['data_dir'] = course_dir
+
+ xmlstore.modules[module.location] = module
+
+ if xmlstore.eager:
+ module.get_children()
+ return module
+
+ render_template = lambda: ''
+ load_item = xmlstore.get_item
+ resources_fs = OSFS(xmlstore.data_dir / course_dir)
+
+ MakoDescriptorSystem.__init__(self, load_item, resources_fs,
+ error_handler, render_template)
+ XMLParsingSystem.__init__(self, load_item, resources_fs,
+ error_handler, process_xml)
+
+
class XMLModuleStore(ModuleStore):
"""
An XML backed ModuleStore
"""
- def __init__(self, data_dir, default_class=None, eager=False, course_dirs=None):
+ def __init__(self, data_dir, default_class=None, eager=False,
+ course_dirs=None,
+ error_handler=logging_error_handler):
"""
Initialize an XMLModuleStore from data_dir
data_dir: path to data directory containing the course directories
- default_class: dot-separated string defining the default descriptor class to use if non is specified in entry_points
- eager: If true, load the modules children immediately to force the entire course tree to be parsed
- course_dirs: If specified, the list of course_dirs to load. Otherwise, load
- all course dirs
+
+ default_class: dot-separated string defining the default descriptor
+ class to use if none is specified in entry_points
+
+ eager: If true, load the modules children immediately to force the
+ entire course tree to be parsed
+
+ course_dirs: If specified, the list of course_dirs to load. Otherwise,
+ load all course dirs
+
+ error_handler: The error handler used here and in the underlying
+ DescriptorSystem. By default, raise exceptions for all errors.
+ See the comments in x_module.py:DescriptorSystem
"""
self.eager = eager
self.data_dir = path(data_dir)
self.modules = {} # location -> XModuleDescriptor
self.courses = {} # course_dir -> XModuleDescriptor for the course
+ self.error_handler = error_handler
if default_class is None:
self.default_class = None
else:
module_path, _, class_name = default_class.rpartition('.')
- log.debug('module_path = %s' % module_path)
+ #log.debug('module_path = %s' % module_path)
class_ = getattr(import_module(module_path), class_name)
self.default_class = class_
- # TODO (cpennington): We need a better way of selecting specific sets of debug messages to enable. These were drowning out important messages
+ # TODO (cpennington): We need a better way of selecting specific sets of
+ # debug messages to enable. These were drowning out important messages
#log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager, self.data_dir))
#log.debug('default_class = %s' % self.default_class)
- for course_dir in os.listdir(self.data_dir):
- if course_dirs is not None and course_dir not in course_dirs:
- continue
-
- if not os.path.exists(self.data_dir / course_dir / "course.xml"):
- continue
+ # If we are specifically asked for missing courses, that should
+ # be an error. If we are asked for "all" courses, find the ones
+ # that have a course.xml
+ if course_dirs is None:
+ course_dirs = [d for d in os.listdir(self.data_dir) if
+ os.path.exists(self.data_dir / d / "course.xml")]
+ for course_dir in course_dirs:
try:
course_descriptor = self.load_course(course_dir)
self.courses[course_dir] = course_descriptor
except:
- log.exception("Failed to load course %s" % course_dir)
+ msg = "Failed to load course '%s'" % course_dir
+ log.exception(msg)
+ error_handler(msg)
+
def load_course(self, course_dir):
"""
Load a course into this module store
course_path: Course directory name
+
+ returns a CourseDescriptor for the course
"""
+ log.debug('========> Starting course import from {0}'.format(course_dir))
with open(self.data_dir / course_dir / "course.xml") as course_file:
- # TODO (cpennington): Remove this once all fall 2012 courses have been imported
- # into the cms from xml
+ # VS[compat]
+ # TODO (cpennington): Remove this once all fall 2012 courses have
+ # been imported into the cms from xml
course_file = StringIO(clean_out_mako_templating(course_file.read()))
course_data = etree.parse(course_file).getroot()
org = course_data.get('org')
if org is None:
- log.error(
- "No 'org' attribute set for course in {dir}. Using default 'edx'".format(
- dir=course_dir))
+ log.error("No 'org' attribute set for course in {dir}. "
+ "Using default 'edx'".format(dir=course_dir))
org = 'edx'
course = course_data.get('course')
if course is None:
- log.error(
- "No 'course' attribute set for course in {dir}. Using default '{default}'".format(
- dir=course_dir,
- default=course_dir
- ))
+ log.error("No 'course' attribute set for course in {dir}."
+ " Using default '{default}'".format(
+ dir=course_dir,
+ default=course_dir
+ ))
course = course_dir
- class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
- def __init__(self, xmlstore):
- """
- xmlstore: the XMLModuleStore to store the loaded modules in
- """
- self.unnamed_modules = 0
- self.used_slugs = set()
+ system = ImportSystem(self, org, course, course_dir,
+ self.error_handler)
- def process_xml(xml):
- try:
- # TODO (cpennington): Remove this once all fall 2012 courses
- # have been imported into the cms from xml
- xml = clean_out_mako_templating(xml)
- xml_data = etree.fromstring(xml)
- except:
- log.exception("Unable to parse xml: {xml}".format(xml=xml))
- raise
- if xml_data.get('slug') is None:
- if xml_data.get('name'):
- slug = Location.clean(xml_data.get('name'))
- else:
- self.unnamed_modules += 1
- slug = '{tag}_{count}'.format(tag=xml_data.tag,
- count=self.unnamed_modules)
-
- if slug in self.used_slugs:
- self.unnamed_modules += 1
- slug = '{slug}_{count}'.format(slug=slug,
- count=self.unnamed_modules)
-
- self.used_slugs.add(slug)
- # log.debug('-> slug=%s' % slug)
- xml_data.set('slug', slug)
-
- module = XModuleDescriptor.load_from_xml(
- etree.tostring(xml_data), self, org,
- course, xmlstore.default_class)
- log.debug('==> importing module location %s' % repr(module.location))
- module.metadata['data_dir'] = course_dir
-
- xmlstore.modules[module.location] = module
-
- if xmlstore.eager:
- module.get_children()
- return module
-
- system_kwargs = dict(
- render_template=lambda: '',
- load_item=xmlstore.get_item,
- resources_fs=OSFS(xmlstore.data_dir / course_dir),
- process_xml=process_xml
- )
- MakoDescriptorSystem.__init__(self, **system_kwargs)
- XMLParsingSystem.__init__(self, **system_kwargs)
-
-
- course_descriptor = ImportSystem(self).process_xml(etree.tostring(course_data))
- log.debug('========> Done with course import')
+ course_descriptor = system.process_xml(etree.tostring(course_data))
+ log.debug('========> Done with course import from {0}'.format(course_dir))
return course_descriptor
def get_item(self, location, depth=0):
@@ -169,7 +205,9 @@ class XMLModuleStore(ModuleStore):
If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError
- If no object is found at that location, raises xmodule.modulestore.exceptions.ItemNotFoundError
+
+ If no object is found at that location, raises
+ xmodule.modulestore.exceptions.ItemNotFoundError
location: Something that can be passed to Location
"""
diff --git a/common/lib/xmodule/xmodule/modulestore/xml_importer.py b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
new file mode 100644
index 0000000000..578ade95fe
--- /dev/null
+++ b/common/lib/xmodule/xmodule/modulestore/xml_importer.py
@@ -0,0 +1,40 @@
+import logging
+
+from .xml import XMLModuleStore
+from .exceptions import DuplicateItemError
+
+log = logging.getLogger(__name__)
+
+
+def import_from_xml(store, data_dir, course_dirs=None, eager=True,
+ default_class='xmodule.raw_module.RawDescriptor'):
+ """
+ Import the specified xml data_dir into the "store" modulestore,
+ using org and course as the location org and course.
+
+ course_dirs: If specified, the list of course_dirs to load. Otherwise, load
+ all course dirs
+
+ """
+ module_store = XMLModuleStore(
+ data_dir,
+ default_class=default_class,
+ eager=eager,
+ course_dirs=course_dirs
+ )
+ for module in module_store.modules.itervalues():
+
+ # TODO (cpennington): This forces import to overrite the same items.
+ # This should in the future create new revisions of the items on import
+ try:
+ store.create_item(module.location)
+ except DuplicateItemError:
+ log.exception('Item already exists at %s' % module.location.url())
+ pass
+ if 'data' in module.definition:
+ store.update_item(module.location, module.definition['data'])
+ if 'children' in module.definition:
+ store.update_children(module.location, module.definition['children'])
+ store.update_metadata(module.location, dict(module.metadata))
+
+ return module_store
diff --git a/common/lib/xmodule/xmodule/raw_module.py b/common/lib/xmodule/xmodule/raw_module.py
index 2794e27dd6..90f4139bd5 100644
--- a/common/lib/xmodule/xmodule/raw_module.py
+++ b/common/lib/xmodule/xmodule/raw_module.py
@@ -6,9 +6,10 @@ import logging
log = logging.getLogger(__name__)
+
class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
"""
- Module that provides a raw editing view of it's data and children
+ Module that provides a raw editing view of its data and children
"""
mako_template = "widgets/raw-edit.html"
@@ -31,8 +32,11 @@ class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
except etree.XMLSyntaxError as err:
lines = self.definition['data'].split('\n')
line, offset = err.position
- log.exception("Unable to create xml for problem {loc}. Context: '{context}'".format(
- context=lines[line-1][offset - 40:offset + 40],
- loc=self.location
- ))
+ msg = ("Unable to create xml for problem {loc}. "
+ "Context: '{context}'".format(
+ context=lines[line - 1][offset - 40:offset + 40],
+ loc=self.location))
+ log.exception(msg)
+ self.system.error_handler(msg)
+ # no workaround possible, so just re-raise
raise
diff --git a/common/lib/xmodule/xmodule/seq_module.py b/common/lib/xmodule/xmodule/seq_module.py
index b39292c2ca..5f7f41bf8d 100644
--- a/common/lib/xmodule/xmodule/seq_module.py
+++ b/common/lib/xmodule/xmodule/seq_module.py
@@ -20,12 +20,15 @@ class_priority = ['video', 'problem']
class SequenceModule(XModule):
''' Layout module which lays out content in a temporal sequence
'''
- js = {'coffee': [resource_string(__name__, 'js/src/sequence/display.coffee')]}
+ js = {'coffee': [resource_string(__name__,
+ 'js/src/sequence/display.coffee')]}
css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
js_module_name = "Sequence"
- def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
+ 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:
@@ -92,7 +95,8 @@ class SequenceModule(XModule):
self.rendered = True
def get_icon_class(self):
- child_classes = set(child.get_icon_class() for child in self.get_children())
+ 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:
@@ -114,5 +118,20 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
def definition_to_xml(self, resource_fs):
xml_object = etree.Element('sequential')
for child in self.get_children():
- xml_object.append(etree.fromstring(child.export_to_xml(resource_fs)))
+ xml_object.append(
+ etree.fromstring(child.export_to_xml(resource_fs)))
return xml_object
+
+ @classmethod
+ def split_to_file(cls, xml_object):
+ # Note: if we end up needing subclasses, can port this logic there.
+ yes = ('chapter',)
+ no = ('course',)
+
+ if xml_object.tag in yes:
+ return True
+ elif xml_object.tag in no:
+ return False
+
+ # otherwise maybe--delegate to superclass.
+ return XmlDescriptor.split_to_file(xml_object)
diff --git a/common/lib/xmodule/xmodule/template_module.py b/common/lib/xmodule/xmodule/template_module.py
index 268ce24559..3f926555f4 100644
--- a/common/lib/xmodule/xmodule/template_module.py
+++ b/common/lib/xmodule/xmodule/template_module.py
@@ -21,19 +21,23 @@ class CustomTagModule(XModule):
course.xml::
...
- book
+
...
Renders to::
More information given in the text
"""
- def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs):
- XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs)
+ 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'])
- template_name = xmltree.find('impl').text
+ template_name = xmltree.attrib['impl']
params = dict(xmltree.items())
- with self.system.filestore.open('custom_tags/{name}'.format(name=template_name)) as template:
+ with self.system.filestore.open(
+ 'custom_tags/{name}'.format(name=template_name)) as template:
self.html = Template(template.read()).render(**params)
def get_html(self):
diff --git a/common/lib/xmodule/xmodule/video_module.py b/common/lib/xmodule/xmodule/video_module.py
index a8f6db9c27..da10e4bc91 100644
--- a/common/lib/xmodule/xmodule/video_module.py
+++ b/common/lib/xmodule/xmodule/video_module.py
@@ -60,7 +60,7 @@ class VideoModule(XModule):
return None
def get_instance_state(self):
- log.debug(u"STATE POSITION {0}".format(self.position))
+ #log.debug(u"STATE POSITION {0}".format(self.position))
return json.dumps({'position': self.position})
def video_list(self):
diff --git a/common/lib/xmodule/xmodule/x_module.py b/common/lib/xmodule/xmodule/x_module.py
index 996d31a83d..3406bcb99c 100644
--- a/common/lib/xmodule/xmodule/x_module.py
+++ b/common/lib/xmodule/xmodule/x_module.py
@@ -3,6 +3,7 @@ import pkg_resources
import logging
from xmodule.modulestore import Location
+
from functools import partial
log = logging.getLogger('mitx.' + __name__)
@@ -31,23 +32,28 @@ class Plugin(object):
def load_class(cls, identifier, default=None):
"""
Loads a single class instance specified by identifier. If identifier
- specifies more than a single class, then logs a warning and returns the first
- class identified.
+ specifies more than a single class, then logs a warning and returns the
+ first class identified.
- If default is not None, will return default if no entry_point matching identifier
- is found. Otherwise, will raise a ModuleMissingError
+ If default is not None, will return default if no entry_point matching
+ identifier is found. Otherwise, will raise a ModuleMissingError
"""
if cls._plugin_cache is None:
cls._plugin_cache = {}
if identifier not in cls._plugin_cache:
identifier = identifier.lower()
- classes = list(pkg_resources.iter_entry_points(cls.entry_point, name=identifier))
+ classes = list(pkg_resources.iter_entry_points(
+ cls.entry_point, name=identifier))
+
if len(classes) > 1:
- log.warning("Found multiple classes for {entry_point} with identifier {id}: {classes}. Returning the first one.".format(
+ log.warning("Found multiple classes for {entry_point} with "
+ "identifier {id}: {classes}. "
+ "Returning the first one.".format(
entry_point=cls.entry_point,
id=identifier,
- classes=", ".join(class_.module_name for class_ in classes)))
+ classes=", ".join(
+ class_.module_name for class_ in classes)))
if len(classes) == 0:
if default is not None:
@@ -79,9 +85,12 @@ class HTMLSnippet(object):
def get_javascript(cls):
"""
Return a dictionary containing some of the following keys:
+
coffee: A list of coffeescript fragments that should be compiled and
placed on the page
- js: A list of javascript fragments that should be included on the page
+
+ js: A list of javascript fragments that should be included on the
+ page
All of these will be loaded onto the page in the CMS
"""
@@ -91,12 +100,15 @@ class HTMLSnippet(object):
def get_css(cls):
"""
Return a dictionary containing some of the following keys:
- css: A list of css fragments that should be applied to the html contents
- of the snippet
- sass: A list of sass fragments that should be applied to the html contents
- of the snippet
- scss: A list of scss fragments that should be applied to the html contents
- of the snippet
+
+ css: A list of css fragments that should be applied to the html
+ contents of the snippet
+
+ sass: A list of sass fragments that should be applied to the html
+ contents of the snippet
+
+ scss: A list of scss fragments that should be applied to the html
+ contents of the snippet
"""
return cls.css
@@ -104,47 +116,70 @@ class HTMLSnippet(object):
"""
Return the html used to display this snippet
"""
- raise NotImplementedError("get_html() must be provided by specific modules - not present in {0}"
+ raise NotImplementedError(
+ "get_html() must be provided by specific modules - not present in {0}"
.format(self.__class__))
class XModule(HTMLSnippet):
''' Implements a generic learning module.
- Subclasses must at a minimum provide a definition for get_html in order to be displayed to users.
+ 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.
'''
- # 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
+ # 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):
+ def __init__(self, system, location, definition,
+ instance_state=None, shared_state=None, **kwargs):
'''
Construct a new xmodule
system: A ModuleSystem 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
+
+ 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.
+
+ '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: SCAFFOLDING - This dictionary will be split into several different types of metadata
- in the future (course policy, modification history, etc).
- A dictionary containing data that specifies information that is particular
- to a problem in the context of a course
+
+ metadata: SCAFFOLDING - This dictionary will be split into
+ several different types of metadata in the future (course
+ policy, modification history, etc). 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)
@@ -158,24 +193,23 @@ class XModule(HTMLSnippet):
self._loaded_children = None
def get_name(self):
- name = self.__xmltree.get('name')
- if name:
- return name
- else:
- raise "We should iterate through children and find a default name"
+ return self.name
def get_children(self):
'''
Return module instances for all the children of this module.
'''
if self._loaded_children is None:
- self._loaded_children = [self.system.get_module(child) for child in self.definition.get('children', [])]
+ self._loaded_children = [
+ self.system.get_module(child)
+ for child in self.definition.get('children', [])]
+
return self._loaded_children
def get_display_items(self):
'''
- Returns a list of descendent module instances that will display immediately
- inside this module
+ Returns a list of descendent module instances that will display
+ immediately inside this module
'''
items = []
for child in self.get_children():
@@ -185,8 +219,8 @@ class XModule(HTMLSnippet):
def displayable_items(self):
'''
- Returns list of displayable modules contained by this module. If this module
- is visible, should return [self]
+ Returns list of displayable modules contained by this module. If this
+ module is visible, should return [self]
'''
return [self]
@@ -217,16 +251,21 @@ class XModule(HTMLSnippet):
def max_score(self):
''' Maximum score. Two notes:
- * This is generic; in abstract, a problem could be 3/5 points on one randomization, and 5/7 on another
- * In practice, this is a Very Bad Idea, and (a) will break some code in place (although that code
- should get fixed), and (b) break some analytics we plan to put in place.
+
+ * This is generic; in abstract, a problem could be 3/5 points on one
+ randomization, and 5/7 on another
+
+ * In practice, this is a Very Bad Idea, and (a) will break some code
+ in place (although that code should get fixed), and (b) break some
+ analytics we plan to put in place.
'''
return None
def get_progress(self):
- ''' Return a progress.Progress object that represents how far the student has gone
- in this module. Must be implemented to get correct progress tracking behavior in
- nesting modules like sequence and vertical.
+ ''' Return a progress.Progress object that represents how far the
+ student has gone in this module. Must be implemented to get correct
+ progress tracking behavior in nesting modules like sequence and
+ vertical.
If this module has no notion of progress, return None.
'''
@@ -240,13 +279,14 @@ class XModule(HTMLSnippet):
class XModuleDescriptor(Plugin, HTMLSnippet):
"""
- An XModuleDescriptor is a specification for an element of a course. This could
- be a problem, an organizational element (a group of content), or a segment of video,
- for example.
+ An XModuleDescriptor is a specification for an element of a course. This
+ could be a problem, an organizational element (a group of content), or a
+ segment of video, for example.
- XModuleDescriptors are independent and agnostic to the current student state on a
- problem. They handle the editing interface used by instructors to create a problem,
- and can generate XModules (which do know about student state).
+ XModuleDescriptors are independent and agnostic to the current student state
+ on a problem. They handle the editing interface used by instructors to
+ create a problem, and can generate XModules (which do know about student
+ state).
"""
entry_point = "xmodule.v1"
module_class = XModule
@@ -255,46 +295,58 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
inheritable_metadata = (
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
- # This is used by the XMLModuleStore to provide for locations for static files,
- # and will need to be removed when that code is removed
+ # TODO: This is used by the XMLModuleStore to provide for locations for
+ # static files, and will need to be removed when that code is removed
'data_dir'
)
- # A list of descriptor attributes that must be equal for the descriptors to be
- # equal
- equality_attributes = ('definition', 'metadata', 'location', 'shared_state_key', '_inherited_metadata')
+ # A list of descriptor attributes that must be equal for the descriptors to
+ # be equal
+ equality_attributes = ('definition', 'metadata', 'location',
+ 'shared_state_key', '_inherited_metadata')
- # ============================= STRUCTURAL MANIPULATION ===========================
+ # ============================= STRUCTURAL MANIPULATION ===================
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).
+ 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.
+ 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
+ system: A DescriptorSystem for interacting with external resources
+
+ definition: A dict containing `data` and `children` representing the
+ problem definition
Current arguments passed in kwargs:
- location: A xmodule.modulestore.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
+
+ location: A xmodule.modulestore.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
+ 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
start (string): The date for which this module will be available
due (string): The due date for this module
- graceperiod (string): The amount of grace period to allow when enforcing the due date
+ 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
+ rerandomize (string): When to generate a newly randomized
+ instance of the module data
"""
self.system = system
self.metadata = kwargs.get('metadata', {})
@@ -321,7 +373,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
self.metadata[attr] = metadata[attr]
def get_children(self):
- """Returns a list of XModuleDescriptor instances for the children of this module"""
+ """Returns a list of XModuleDescriptor instances for the children of
+ this module"""
if self._child_instances is None:
self._child_instances = []
for child_loc in self.definition.get('children', []):
@@ -333,8 +386,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
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
+ Returns a constructor for an XModule. This constructor takes two
+ arguments: instance_state and shared_state, and returns a fully
+ instantiated XModule
"""
return partial(
self.module_class,
@@ -344,7 +398,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
metadata=self.metadata
)
- # ================================= JSON PARSING ===================================
+ # ================================= JSON PARSING ===========================
@staticmethod
def load_from_json(json_data, system, default_class=None):
"""
@@ -366,13 +420,14 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
Creates an instance of this descriptor from the supplied json_data.
This may be overridden by subclasses
- json_data: A json object specifying the definition and any optional keyword arguments for
- the XModuleDescriptor
- system: An XModuleSystem for interacting with external resources
+ json_data: A json object specifying the definition and any optional
+ keyword arguments for the XModuleDescriptor
+
+ system: A DescriptorSystem for interacting with external resources
"""
return cls(system=system, **json_data)
- # ================================= XML PARSING ====================================
+ # ================================= XML PARSING ============================
@staticmethod
def load_from_xml(xml_data,
system,
@@ -384,16 +439,19 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
on the contents of xml_data.
xml_data must be a string containing valid xml
+
system is an XMLParsingSystem
- org and course are optional strings that will be used in the generated modules
- url identifiers
+
+ org and course are optional strings that will be used in the generated
+ modules url identifiers
"""
class_ = XModuleDescriptor.load_class(
etree.fromstring(xml_data).tag,
default_class
)
- # leave next line in code, commented out - useful for low-level debugging
- # log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (etree.fromstring(xml_data).tag,class_))
+ # leave next line, commented out - useful for low-level debugging
+ # log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
+ # etree.fromstring(xml_data).tag,class_))
return class_.from_xml(xml_data, system, org, course)
@classmethod
@@ -402,35 +460,42 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
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
+ xml_data: A string of xml that will be translated into data and children
+ for this module
+
system is an XMLParsingSystem
- org and course are optional strings that will be used in the generated modules
- url identifiers
+
+ org and course are optional strings that will be used in the generated
+ module's url identifiers
"""
- raise NotImplementedError('Modules must implement from_xml to be parsable from xml')
+ raise NotImplementedError(
+ 'Modules must implement from_xml to be parsable from xml')
def export_to_xml(self, resource_fs):
"""
- Returns an xml string representing this module, and all modules underneath it.
- May also write required resources out to resource_fs
+ Returns an xml string representing this module, and all modules
+ underneath it. May also write required resources out to resource_fs
- Assumes that modules have single parantage (that no module appears twice in the same course),
- and that it is thus safe to nest modules as xml children as appropriate.
+ Assumes that modules have single parentage (that no module appears twice
+ in the same course), and that it is thus safe to nest modules as xml
+ children as appropriate.
- The returned XML should be able to be parsed back into an identical XModuleDescriptor
- using the from_xml method with the same system, org, and course
+ The returned XML should be able to be parsed back into an identical
+ XModuleDescriptor using the from_xml method with the same system, org,
+ and course
"""
- raise NotImplementedError('Modules must implement export_to_xml to enable xml export')
+ raise NotImplementedError(
+ 'Modules must implement export_to_xml to enable xml export')
- # =============================== Testing ===================================
+ # =============================== Testing ==================================
def get_sample_state(self):
"""
- Return a list of tuples of instance_state, shared_state. Each tuple defines a sample case for this module
+ Return a list of tuples of instance_state, shared_state. Each tuple
+ defines a sample case for this module
"""
return [('{}', '{}')]
- # =============================== BUILTIN METHODS ===========================
+ # =============================== BUILTIN METHODS ==========================
def __eq__(self, other):
eq = (self.__class__ == other.__class__ and
all(getattr(self, attr, None) == getattr(other, attr, None)
@@ -438,38 +503,72 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
if not eq:
for attr in self.equality_attributes:
- print getattr(self, attr, None), getattr(other, attr, None), getattr(self, attr, None) == getattr(other, attr, None)
+ print(getattr(self, attr, None),
+ getattr(other, attr, None),
+ getattr(self, attr, None) == getattr(other, attr, None))
return eq
def __repr__(self):
- return "{class_}({system!r}, {definition!r}, location={location!r}, metadata={metadata!r})".format(
+ return ("{class_}({system!r}, {definition!r}, location={location!r},"
+ " metadata={metadata!r})".format(
class_=self.__class__.__name__,
system=self.system,
definition=self.definition,
location=self.location,
metadata=self.metadata
- )
+ ))
class DescriptorSystem(object):
- def __init__(self, load_item, resources_fs, **kwargs):
+ def __init__(self, load_item, resources_fs, error_handler):
"""
load_item: Takes a Location and returns an XModuleDescriptor
+
resources_fs: A Filesystem object that contains all of the
resources needed for the course
+
+ error_handler: A hook for handling errors in loading the descriptor.
+ Must be a function of (error_msg, exc_info=None).
+ See errorhandlers.py for some simple ones.
+
+ Patterns for using the error handler:
+ try:
+ x = access_some_resource()
+ check_some_format(x)
+ except SomeProblem:
+ msg = 'Grommet {0} is broken'.format(x)
+ log.exception(msg) # don't rely on handler to log
+ self.system.error_handler(msg)
+ # if we get here, work around if possible
+ raise # if no way to work around
+ OR
+ return 'Oops, couldn't load grommet'
+
+ OR, if not in an exception context:
+
+ if not check_something(thingy):
+ msg = "thingy {0} is broken".format(thingy)
+ log.critical(msg)
+ error_handler(msg)
+ # if we get here, work around
+ pass # e.g. if no workaround needed
"""
self.load_item = load_item
self.resources_fs = resources_fs
+ self.error_handler = error_handler
class XMLParsingSystem(DescriptorSystem):
- def __init__(self, load_item, resources_fs, process_xml, **kwargs):
+ def __init__(self, load_item, resources_fs, error_handler, process_xml):
"""
- process_xml: Takes an xml string, and returns the the XModuleDescriptor created from that xml
+ load_item, resources_fs, error_handler: see DescriptorSystem
+
+ process_xml: Takes an xml string, and returns a XModuleDescriptor
+ created from that xml
"""
- DescriptorSystem.__init__(self, load_item, resources_fs)
+ DescriptorSystem.__init__(self, load_item, resources_fs, error_handler)
self.process_xml = process_xml
@@ -487,24 +586,33 @@ class ModuleSystem(object):
'''
def __init__(self, ajax_url, track_function,
get_module, render_template, replace_urls,
- user=None, filestore=None, debug=False, xqueue_callback_url=None):
+ user=None, filestore=None, debug=False,
+ xqueue_callback_url=None):
'''
Create a closure around the system environment.
ajax_url - the url where ajax calls to the encapsulating module go.
+
track_function - function of (event_type, event), intended for logging
or otherwise tracking the event.
TODO: Not used, and has inconsistent args in different
files. Update or remove.
+
get_module - function that takes (location) and returns a corresponding
- module instance object.
- render_template - a function that takes (template_file, context), and returns
- rendered html.
- user - The user to base the random number generator seed off of for this request
- filestore - A filestore ojbect. Defaults to an instance of OSFS based at
- settings.DATA_DIR.
+ module instance object.
+
+ render_template - a function that takes (template_file, context), and
+ returns rendered html.
+
+ user - The user to base the random number generator seed off of for this
+ request
+
+ filestore - A filestore ojbect. Defaults to an instance of OSFS based
+ at settings.DATA_DIR.
+
replace_urls - TEMPORARY - A function like static_replace.replace_urls
- that capa_module can use to fix up the static urls in ajax results.
+ that capa_module can use to fix up the static urls in
+ ajax results.
'''
self.ajax_url = ajax_url
self.xqueue_callback_url = xqueue_callback_url
@@ -529,4 +637,3 @@ class ModuleSystem(object):
def __str__(self):
return str(self.__dict__)
-
diff --git a/common/lib/xmodule/xmodule/xml_module.py b/common/lib/xmodule/xmodule/xml_module.py
index a7fc686e45..6750906eb4 100644
--- a/common/lib/xmodule/xmodule/xml_module.py
+++ b/common/lib/xmodule/xmodule/xml_module.py
@@ -1,25 +1,33 @@
from collections import MutableMapping
from xmodule.x_module import XModuleDescriptor
+from xmodule.modulestore import Location
from lxml import etree
import copy
import logging
+import traceback
from collections import namedtuple
from fs.errors import ResourceNotFoundError
import os
log = logging.getLogger(__name__)
-
# TODO (cpennington): This was implemented in an attempt to improve performance,
# but the actual improvement wasn't measured (and it was implemented late at night).
# We should check if it hurts, and whether there's a better way of doing lazy loading
+
+
class LazyLoadingDict(MutableMapping):
"""
- A dictionary object that lazily loads it's contents from a provided
- function on reads (of members that haven't already been set)
+ A dictionary object that lazily loads its contents from a provided
+ function on reads (of members that haven't already been set).
"""
def __init__(self, loader):
+ '''
+ On the first read from this dictionary, it will call loader() to
+ populate its contents. loader() must return something dict-like. Any
+ elements set before the first read will be preserved.
+ '''
self._contents = {}
self._loaded = False
self._loader = loader
@@ -70,10 +78,17 @@ _AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata')
class AttrMap(_AttrMapBase):
"""
- A class that specifies a metadata_key, a function to transform an xml attribute to be placed in that key,
- and to transform that key value
+ A class that specifies a metadata_key, and two functions:
+
+ to_metadata: convert value from the xml representation into
+ an internal python representation
+
+ from_metadata: convert the internal python representation into
+ the value to store in the xml.
"""
- def __new__(_cls, metadata_key, to_metadata=lambda x: x, from_metadata=lambda x: x):
+ def __new__(_cls, metadata_key,
+ to_metadata=lambda x: x,
+ from_metadata=lambda x: x):
return _AttrMapBase.__new__(_cls, metadata_key, to_metadata, from_metadata)
@@ -88,15 +103,35 @@ class XmlDescriptor(XModuleDescriptor):
# The attributes will be removed from the definition xml passed
# to definition_from_xml, and from the xml returned by definition_to_xml
metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize',
- 'start', 'due', 'graded', 'name', 'slug', 'hide_from_toc')
+ 'start', 'due', 'graded', 'display_name', 'url_name', 'hide_from_toc',
+ # VS[compat] Remove once unused.
+ 'name', 'slug')
- # A dictionary mapping xml attribute names to functions of the value
- # that return the metadata key and value
+
+ # A dictionary mapping xml attribute names AttrMaps that describe how
+ # to import and export them
xml_attribute_map = {
- 'graded': AttrMap('graded', lambda val: val == 'true', lambda val: str(val).lower()),
- 'name': AttrMap('display_name'),
+ # type conversion: want True/False in python, "true"/"false" in xml
+ 'graded': AttrMap('graded',
+ lambda val: val == 'true',
+ lambda val: str(val).lower()),
}
+
+ # VS[compat]. Backwards compatibility code that can go away after
+ # importing 2012 courses.
+ # A set of metadata key conversions that we want to make
+ metadata_translations = {
+ 'slug' : 'url_name',
+ 'name' : 'display_name',
+ }
+
+ @classmethod
+ def _translate(cls, key):
+ 'VS[compat]'
+ return cls.metadata_translations.get(key, key)
+
+
@classmethod
def definition_from_xml(cls, xml_object, system):
"""
@@ -105,12 +140,14 @@ class XmlDescriptor(XModuleDescriptor):
xml_object: An etree Element
"""
- raise NotImplementedError("%s does not implement definition_from_xml" % cls.__name__)
+ raise NotImplementedError(
+ "%s does not implement definition_from_xml" % cls.__name__)
@classmethod
def clean_metadata_from_xml(cls, xml_object):
"""
- Remove any attribute named in self.metadata_attributes from the supplied xml_object
+ Remove any attribute named in cls.metadata_attributes from the supplied
+ xml_object
"""
for attr in cls.metadata_attributes:
if xml_object.get(attr) is not None:
@@ -134,20 +171,25 @@ class XmlDescriptor(XModuleDescriptor):
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
+ system: A DescriptorSystem 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)
+ # VS[compat] -- just have the url_name lookup once translation is done
+ slug = xml_object.get('url_name', xml_object.get('slug'))
+ location = Location('i4x', org, course, xml_object.tag, slug)
def metadata_loader():
metadata = {}
for attr in cls.metadata_attributes:
val = xml_object.get(attr)
if val is not None:
+ # VS[compat]. Remove after all key translations done
+ attr = cls._translate(attr)
+
attr_map = cls.xml_attribute_map.get(attr, AttrMap(attr))
metadata[attr_map.metadata_key] = attr_map.to_metadata(val)
-
return metadata
def definition_loader():
@@ -157,6 +199,7 @@ class XmlDescriptor(XModuleDescriptor):
else:
filepath = cls._format_filepath(xml_object.tag, filename)
+ # VS[compat]
# TODO (cpennington): If the file doesn't exist at the right path,
# give the class a chance to fix it up. The file will be written out again
# in the correct format.
@@ -169,13 +212,20 @@ class XmlDescriptor(XModuleDescriptor):
filepath = candidate
break
- log.debug('filepath=%s, resources_fs=%s' % (filepath, system.resources_fs))
try:
with system.resources_fs.open(filepath) as file:
definition_xml = cls.file_to_xml(file)
except (ResourceNotFoundError, etree.XMLSyntaxError):
- log.exception('Unable to load file contents at path %s' % filepath)
- return {'data': 'Error loading file contents at path %s' % filepath}
+ msg = 'Unable to load file contents at path %s for item %s' % (filepath, location.url())
+ log.exception(msg)
+ system.error_handler(msg)
+ # if error_handler didn't reraise, work around problem.
+ error_elem = etree.Element('error')
+ message_elem = etree.SubElement(error_elem, 'error_message')
+ message_elem.text = msg
+ stack_elem = etree.SubElement(error_elem, 'stack_trace')
+ stack_elem.text = traceback.format_exc()
+ return {'data': etree.tostring(error_elem)}
cls.clean_metadata_from_xml(definition_xml)
return cls.definition_from_xml(definition_xml, system)
@@ -183,64 +233,90 @@ class XmlDescriptor(XModuleDescriptor):
return cls(
system,
LazyLoadingDict(definition_loader),
- location=['i4x',
- org,
- course,
- xml_object.tag,
- xml_object.get('slug')],
+ location=location,
metadata=LazyLoadingDict(metadata_loader),
)
@classmethod
def _format_filepath(cls, category, name):
- return u'{category}/{name}.{ext}'.format(category=category, name=name, ext=cls.filename_extension)
+ return u'{category}/{name}.{ext}'.format(category=category,
+ name=name,
+ ext=cls.filename_extension)
+
+ @classmethod
+ def split_to_file(cls, xml_object):
+ '''
+ Decide whether to write this object to a separate file or not.
+
+ xml_object: an xml definition of an instance of cls.
+
+ This default implementation will split if this has more than 7
+ descendant tags.
+
+ Can be overridden by subclasses.
+ '''
+ return len(list(xml_object.iter())) > 7
def export_to_xml(self, resource_fs):
"""
- Returns an xml string representing this module, and all modules underneath it.
- May also write required resources out to resource_fs
+ Returns an xml string representing this module, and all modules
+ underneath it. May also write required resources out to resource_fs
- Assumes that modules have single parantage (that no module appears twice in the same course),
- and that it is thus safe to nest modules as xml children as appropriate.
+ Assumes that modules have single parentage (that no module appears twice
+ in the same course), and that it is thus safe to nest modules as xml
+ children as appropriate.
- The returned XML should be able to be parsed back into an identical XModuleDescriptor
- using the from_xml method with the same system, org, and course
+ The returned XML should be able to be parsed back into an identical
+ XModuleDescriptor using the from_xml method with the same system, org,
+ and course
- resource_fs is a pyfilesystem office (from the fs package)
+ resource_fs is a pyfilesystem object (from the fs package)
"""
+
+ # Get the definition
xml_object = self.definition_to_xml(resource_fs)
self.__class__.clean_metadata_from_xml(xml_object)
- # Put content in a separate file if it's large (has more than 5 descendent tags)
- if len(list(xml_object.iter())) > 5:
+ # Set the tag first, so it's right if writing to a file
+ xml_object.tag = self.category
+ # Write it to a file if necessary
+ if self.split_to_file(xml_object):
+ # Put this object in it's own file
filepath = self.__class__._format_filepath(self.category, self.name)
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
with resource_fs.open(filepath, 'w') as file:
file.write(etree.tostring(xml_object, pretty_print=True))
-
+ # ...and remove all of its children here
for child in xml_object:
xml_object.remove(child)
+ # also need to remove the text of this object.
+ xml_object.text = ''
+ # and the tail for good measure...
+ xml_object.tail = ''
+
xml_object.set('filename', self.name)
- xml_object.set('slug', self.name)
- xml_object.tag = self.category
-
+ # Add the metadata
+ xml_object.set('url_name', self.name)
for attr in self.metadata_attributes:
attr_map = self.xml_attribute_map.get(attr, AttrMap(attr))
metadata_key = attr_map.metadata_key
- if metadata_key not in self.metadata or metadata_key in self._inherited_metadata:
+ if (metadata_key not in self.metadata or
+ metadata_key in self._inherited_metadata):
continue
val = attr_map.from_metadata(self.metadata[metadata_key])
xml_object.set(attr, val)
+ # Now we just have to make it beautiful
return etree.tostring(xml_object, pretty_print=True)
def definition_to_xml(self, resource_fs):
"""
Return a new etree Element object created from this modules definition.
"""
- raise NotImplementedError("%s does not implement definition_to_xml" % self.__class__.__name__)
+ raise NotImplementedError(
+ "%s does not implement definition_to_xml" % self.__class__.__name__)
diff --git a/lms/static/images/correct-icon.png b/common/static/images/correct-icon.png
similarity index 100%
rename from lms/static/images/correct-icon.png
rename to common/static/images/correct-icon.png
diff --git a/lms/static/images/incorrect-icon.png b/common/static/images/incorrect-icon.png
similarity index 100%
rename from lms/static/images/incorrect-icon.png
rename to common/static/images/incorrect-icon.png
diff --git a/lms/static/images/unanswered-icon.png b/common/static/images/unanswered-icon.png
similarity index 100%
rename from lms/static/images/unanswered-icon.png
rename to common/static/images/unanswered-icon.png
diff --git a/common/test/data/full/chapter/Overview.xml b/common/test/data/full/chapter/Overview.xml
index 8a0d3ef7ec..89917d20da 100644
--- a/common/test/data/full/chapter/Overview.xml
+++ b/common/test/data/full/chapter/Overview.xml
@@ -3,6 +3,7 @@
See the Lab Introduction or Interactive Lab Usage Handout for information on how to do the lab
+
diff --git a/common/test/data/full/course.xml b/common/test/data/full/course.xml
index 52d4753a1a..c365e68cc1 100644
--- a/common/test/data/full/course.xml
+++ b/common/test/data/full/course.xml
@@ -1 +1 @@
-
+
diff --git a/common/test/data/full/course/6.002_Spring_2012.xml b/common/test/data/full/course/6.002_Spring_2012.xml
index d6398cdc46..0d22e96beb 100644
--- a/common/test/data/full/course/6.002_Spring_2012.xml
+++ b/common/test/data/full/course/6.002_Spring_2012.xml
@@ -6,7 +6,7 @@
-
+
diff --git a/common/test/data/full/html/html_5555.html b/common/test/data/full/html/html_5555.html
new file mode 100644
index 0000000000..44a015faa1
--- /dev/null
+++ b/common/test/data/full/html/html_5555.html
@@ -0,0 +1 @@
+ Lab Introduction or Interactive Lab Usage Handout for information on how to do the lab
diff --git a/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml b/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml
index 28b56fb9b0..5c4c65f12d 100644
--- a/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml
+++ b/common/test/data/full/sequential/Administrivia_and_Circuit_Elements.xml
@@ -2,17 +2,13 @@
-
- discuss
-
+
S1E4 has been removed.
Minor correction: Six elements (five resistors)
-
- discuss
-
+
diff --git a/common/test/data/full/sequential/System_Usage_Sequence.xml b/common/test/data/full/sequential/System_Usage_Sequence.xml
index baf73f1e9e..571229be7c 100644
--- a/common/test/data/full/sequential/System_Usage_Sequence.xml
+++ b/common/test/data/full/sequential/System_Usage_Sequence.xml
@@ -3,5 +3,4 @@
-
diff --git a/common/test/data/full/vertical/vertical_58.xml b/common/test/data/full/vertical/vertical_58.xml
index 7eba0f9dad..5707e6acf3 100644
--- a/common/test/data/full/vertical/vertical_58.xml
+++ b/common/test/data/full/vertical/vertical_58.xml
@@ -1,12 +1,6 @@
-
- discuss
-
-
- book
-
-
- slides
-
+
+
+
diff --git a/common/test/data/full/vertical/vertical_89.xml b/common/test/data/full/vertical/vertical_89.xml
index 06b13846d2..da15a6751a 100644
--- a/common/test/data/full/vertical/vertical_89.xml
+++ b/common/test/data/full/vertical/vertical_89.xml
@@ -3,13 +3,7 @@
Each month, a credit
+ card statement will come with the option for you to pay a
+ minimum amount of your charge, usually 2% of the balance due.
+ However, the credit card company earns money by charging
+ interest on the balance that you don't pay. So even if you
+ pay credit card payments on time, interest is still accruing
+ on the outstanding balance.
+
Say you've made a
+ $5,000 purchase on a credit card with 18% annual interest
+ rate and 2% minimum monthly payment rate. After a year, how
+ much is the remaining balance? Use the following
+ equations.
For month 1, compute the minimum monthly payment by taking 2% of the balance.
+
+
Minimum monthly payment
+= .02 x $5000 = $100
+
We can't simply deduct this from the balance because
+ there is compounding interest. Of this $100 monthly
+ payment, compute how much will go to paying off interest
+ and how much will go to paying off the principal. Remember
+ that it's the annual interest rate that is given, so we
+ need to divide it by 12 to get the monthly interest
+ rate.
Note: This part of the lab is just to develop your intuition about
-superposition. There are no responses that need to be checked.
-
-
Circuits with multiple sources can be hard to analyze as-is. For example, what is the voltage
-between the two terminals on the right of Figure 1?
-
-
-
-Figure 1. Example multi-source circuit
-
-
-
We can use superposition to make the analysis much easier.
-The circuit in Figure 1 can be decomposed into two separate
-subcircuits: one involving only the voltage source and one involving only the
-current source. We'll analyze each circuit separately and combine the
-results using superposition. Recall that to decompose a circuit for
-analysis, we'll pick each source in turn and set all the other sources
-to zero (i.e., voltage sources become short circuits and current
-sources become open circuits). The circuit above has two sources, so
-the decomposition produces two subcircuits, as shown in Figure 2.
-
-
-
-
-(a) Subcircuit for analyzing contribution of voltage source
-
-
-(b) Subcircuit for analyzing contribution of current source
-
- Figure 2. Decomposition of Figure 1 into subcircuits
-
-
- Let's use the DC analysis capability of the schematic tool to see superposition
-in action. The sliders below control the resistances of R1, R2, R3 and R4 in all
-the diagrams. As you move the sliders, the schematic tool will adjust the appropriate
-resistance, perform a DC analysis and display the node voltages on the diagrams. Here's
-what you want to observe as you play with the sliders:
-
-
-The voltage for a node in Figure 1 is the sum of the voltages for
-that node in Figures 2(a) and 2(b), just as predicted by
-superposition. (Note that due to round-off in the display of the
-voltages, the sum of the displayed voltages in Figure 2 may only be within
-.01 of the voltages displayed in Figure 1.)
-
diff --git a/doc/overview.md b/doc/overview.md
index 88ea3bdb5e..36e22e16eb 100644
--- a/doc/overview.md
+++ b/doc/overview.md
@@ -13,17 +13,17 @@ You should be familiar with the following. If you're not, go read some docs...
- css
- git
- mako templates -- we use these instead of django templates, because they support embedding real python.
-
+
## Other relevant terms
- CAPA -- lon-capa.org -- content management system that has defined a standard for online learning and assessment materials. Many of our materials follow this standard.
- - TODO: add more details / link to relevant docs. lon-capa.org is not immediately intuitive.
+ - TODO: add more details / link to relevant docs. lon-capa.org is not immediately intuitive.
- lcp = loncapa problem
## Parts of the system
- - LMS -- Learning Management System. The student-facing parts of the system. Handles student accounts, displaying videos, tutorials, exercies, problems, etc.
+ - LMS -- Learning Management System. The student-facing parts of the system. Handles student accounts, displaying videos, tutorials, exercies, problems, etc.
- CMS -- Course Management System. The instructor-facing parts of the system. Allows instructors to see and modify their course, add lectures, problems, reorder things, etc.
@@ -42,7 +42,7 @@ You should be familiar with the following. If you're not, go read some docs...
## High Level Entities in the code
-### Common libraries
+### Common libraries
- xmodule: generic learning modules. *x* can be sequence, video, template, html,
vertical, capa, etc. These are the things that one puts inside sections
@@ -51,7 +51,7 @@ You should be familiar with the following. If you're not, go read some docs...
- XModuleDescriptor: This defines the problem and all data and UI needed to edit
that problem. It is unaware of any student data, but can be used to retrieve
an XModule, which is aware of that student state.
-
+
- XModule: The XModule is a problem instance that is particular to a student. It knows
how to render itself to html to display the problem, how to score itself,
and how to handle ajax calls from the front end.
@@ -59,19 +59,25 @@ You should be familiar with the following. If you're not, go read some docs...
- Both XModule and XModuleDescriptor take system context parameters. These are named
ModuleSystem and DescriptorSystem respectively. These help isolate the XModules
from any interactions with external resources that they require.
-
+
For instance, the DescriptorSystem has a function to load an XModuleDescriptor
from a Location object, and the ModuleSystem knows how to render things,
track events, and complain about 404s
- - TODO: document the system context interface--it's different in `x_module.XModule.__init__` and in `x_module tests.py` (do this in the code, not here)
+
+ - `course.xml` format. We use python setuptools to connect supported tags with the descriptors that handle them. See `common/lib/xmodule/setup.py`. There are checking and validation tools in `common/validate`.
+
+ - the xml import+export functionality is in `xml_module.py:XmlDescriptor`, which is a mixin class that's used by the actual descriptor classes.
+
+ - There is a distinction between descriptor _definitions_ that stay the same for any use of that descriptor (e.g. here is what a particular problem is), and _metadata_ describing how that descriptor is used (e.g. whether to allow checking of answers, due date, etc). When reading in `from_xml`, the code pulls out the metadata attributes into a separate structure, and puts it back on export.
+
- in `common/lib/xmodule`
-- capa modules -- defines `LoncapaProblem` and many related things.
+- capa modules -- defines `LoncapaProblem` and many related things.
- in `common/lib/capa`
-### LMS
+### LMS
-The LMS is a django site, with root in `lms/`. It runs in many different environments--the settings files are in `lms/envs`.
+The LMS is a django site, with root in `lms/`. It runs in many different environments--the settings files are in `lms/envs`.
- We use the Django Auth system, including the is_staff and is_superuser flags. User profiles and related code lives in `lms/djangoapps/student/`. There is support for groups of students (e.g. 'want emails about future courses', 'have unenrolled', etc) in `lms/djangoapps/student/models.py`.
@@ -79,19 +85,19 @@ The LMS is a django site, with root in `lms/`. It runs in many different enviro
- `lms/djangoapps/courseware/models.py`
- Core rendering path:
- - `lms/urls.py` points to `courseware.views.index`, which gets module info from the course xml file, pulls list of `StudentModule` objects for this user (to avoid multiple db hits).
+ - `lms/urls.py` points to `courseware.views.index`, which gets module info from the course xml file, pulls list of `StudentModule` objects for this user (to avoid multiple db hits).
- Calls `render_accordion` to render the "accordion"--the display of the course structure.
- To render the current module, calls `module_render.py:render_x_module()`, which gets the `StudentModule` instance, and passes the `StudentModule` state and other system context to the module constructor the get an instance of the appropriate module class for this user.
- calls the module's `.get_html()` method. If the module has nested submodules, render_x_module() will be called again for each.
-
+
- ajax calls go to `module_render.py:modx_dispatch()`, which passes it to the module's `handle_ajax()` function, and then updates the grade and state if they changed.
- [This diagram](https://github.com/MITx/mitx/wiki/MITx-Architecture) visually shows how the clients communicate with problems + modules.
-
-- See `lms/urls.py` for the wirings of urls to views.
+
+- See `lms/urls.py` for the wirings of urls to views.
- Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`.
@@ -110,7 +116,7 @@ environments, defined in `cms/envs`.
- _mako_ -- we use this for templates, and have wrapper called mitxmako that makes mako look like the django templating calls.
-We use a fork of django-pipeline to make sure that the js and css always reflect the latest `*.coffee` and `*.sass` files (We're hoping to get our changes merged in the official version soon). This works differently in development and production. Test uses the production settings.
+We use a fork of django-pipeline to make sure that the js and css always reflect the latest `*.coffee` and `*.sass` files (We're hoping to get our changes merged in the official version soon). This works differently in development and production. Test uses the production settings.
In production, the django `collectstatic` command recompiles everything and puts all the generated static files in a static/ dir. A starting point in the code is `django-pipeline/pipeline/packager.py:pack`.
@@ -127,8 +133,6 @@ See `testing.md`.
## TODO:
-- update lms/envs/README.txt
-
- describe our production environment
- describe the front-end architecture, tools, etc. Starting point: `lms/static`
diff --git a/lms/djangoapps/courseware/management/commands/clean_xml.py b/lms/djangoapps/courseware/management/commands/clean_xml.py
new file mode 100644
index 0000000000..7523fd8373
--- /dev/null
+++ b/lms/djangoapps/courseware/management/commands/clean_xml.py
@@ -0,0 +1,171 @@
+import os
+import sys
+import traceback
+
+from filecmp import dircmp
+from fs.osfs import OSFS
+from path import path
+from lxml import etree
+
+from django.core.management.base import BaseCommand
+
+from xmodule.modulestore.xml import XMLModuleStore
+
+
+def traverse_tree(course):
+ '''Load every descriptor in course. Return bool success value.'''
+ queue = [course]
+ while len(queue) > 0:
+ node = queue.pop()
+# print '{0}:'.format(node.location)
+# if 'data' in node.definition:
+# print '{0}'.format(node.definition['data'])
+ queue.extend(node.get_children())
+
+ return True
+
+def make_logging_error_handler():
+ '''Return a tuple (handler, error_list), where
+ the handler appends the message and any exc_info
+ to the error_list on every call.
+ '''
+ errors = []
+
+ def error_handler(msg, exc_info=None):
+ '''Log errors'''
+ if exc_info is None:
+ if sys.exc_info() != (None, None, None):
+ exc_info = sys.exc_info()
+
+ errors.append((msg, exc_info))
+
+ return (error_handler, errors)
+
+
+def export(course, export_dir):
+ """Export the specified course to course_dir. Creates dir if it doesn't exist.
+ Overwrites files, does not clean out dir beforehand.
+ """
+ fs = OSFS(export_dir, create=True)
+ if not fs.isdirempty('.'):
+ print ('WARNING: Directory {dir} not-empty.'
+ ' May clobber/confuse things'.format(dir=export_dir))
+
+ try:
+ xml = course.export_to_xml(fs)
+ with fs.open('course.xml', mode='w') as f:
+ f.write(xml)
+
+ return True
+ except:
+ print 'Export failed!'
+ traceback.print_exc()
+
+ return False
+
+
+def import_with_checks(course_dir, verbose=True):
+ all_ok = True
+
+ print "Attempting to load '{0}'".format(course_dir)
+
+ course_dir = path(course_dir)
+ data_dir = course_dir.dirname()
+ course_dirs = [course_dir.basename()]
+
+ (error_handler, errors) = make_logging_error_handler()
+ # No default class--want to complain if it doesn't find plugins for any
+ # module.
+ modulestore = XMLModuleStore(data_dir,
+ default_class=None,
+ eager=True,
+ course_dirs=course_dirs,
+ error_handler=error_handler)
+
+ def str_of_err(tpl):
+ (msg, exc_info) = tpl
+ if exc_info is None:
+ return msg
+
+ exc_str = '\n'.join(traceback.format_exception(*exc_info))
+ return '{msg}\n{exc}'.format(msg=msg, exc=exc_str)
+
+ courses = modulestore.get_courses()
+ if len(errors) != 0:
+ all_ok = False
+ print '\n'
+ print "=" * 40
+ print 'ERRORs during import:'
+ print '\n'.join(map(str_of_err,errors))
+ print "=" * 40
+ print '\n'
+
+ n = len(courses)
+ if n != 1:
+ print 'ERROR: Expect exactly 1 course. Loaded {n}: {lst}'.format(
+ n=n, lst=courses)
+ return (False, None)
+
+ course = courses[0]
+
+ #print course
+ validators = (
+ traverse_tree,
+ )
+
+ print "=" * 40
+ print "Running validators..."
+
+ for validate in validators:
+ print 'Running {0}'.format(validate.__name__)
+ all_ok = validate(course) and all_ok
+
+
+ if all_ok:
+ print 'Course passes all checks!'
+ else:
+ print "Course fails some checks. See above for errors."
+ return all_ok, course
+
+
+def check_roundtrip(course_dir):
+ '''Check that import->export leaves the course the same'''
+
+ print "====== Roundtrip import ======="
+ (ok, course) = import_with_checks(course_dir)
+ if not ok:
+ raise Exception("Roundtrip import failed!")
+
+ print "====== Roundtrip export ======="
+ export_dir = course_dir + ".rt"
+ export(course, export_dir)
+
+ # dircmp doesn't do recursive diffs.
+ # diff = dircmp(course_dir, export_dir, ignore=[], hide=[])
+ print "======== Roundtrip diff: ========="
+ os.system("diff -r {0} {1}".format(course_dir, export_dir))
+ print "======== ideally there is no diff above this ======="
+
+
+def clean_xml(course_dir, export_dir):
+ (ok, course) = import_with_checks(course_dir)
+ if ok:
+ export(course, export_dir)
+ check_roundtrip(export_dir)
+ else:
+ print "Did NOT export"
+
+
+
+class Command(BaseCommand):
+ help = """Imports specified course.xml, validate it, then exports in
+ a canonical format.
+
+Usage: clean_xml PATH-TO-COURSE-DIR PATH-TO-OUTPUT-DIR
+"""
+ def handle(self, *args, **options):
+ if len(args) != 2:
+ print Command.help
+ return
+
+ clean_xml(args[0], args[1])
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 4b21255935..0bfdb1a1b2 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -119,7 +119,7 @@ def get_module(user, request, location, student_module_cache, position=None):
instance_module is a StudentModule specific to this module for this student,
or None if this is an anonymous user
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
+ 'shared_state_key' attribute, or None if the module does not elect to
share state
'''
descriptor = modulestore().get_item(location)
@@ -131,11 +131,13 @@ def get_module(user, request, location, student_module_cache, position=None):
if course_id:
course_id = course_id.group('course_id')
- instance_module = student_module_cache.lookup(descriptor.category, descriptor.location.url())
+ 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)
+ shared_module = student_module_cache.lookup(descriptor.category,
+ shared_state_key)
else:
shared_module = None
@@ -150,10 +152,12 @@ def get_module(user, request, location, student_module_cache, position=None):
# TODO (vshnayder): fix hardcoded urls (use reverse)
# Setup system context for module instance
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/'
- xqueue_callback_url = settings.MITX_ROOT_URL + '/xqueue/' + str(user.id) + '/' + descriptor.location.url() + '/'
+ xqueue_callback_url = (settings.MITX_ROOT_URL + '/xqueue/' +
+ str(user.id) + '/' + descriptor.location.url() + '/')
def _get_module(location):
- (module, _, _, _) = get_module(user, request, location, student_module_cache, position)
+ (module, _, _, _) = get_module(user, request, location,
+ student_module_cache, position)
return module
# TODO (cpennington): When modules are shared between courses, the static
diff --git a/lms/djangoapps/courseware/tests/__init__.py b/lms/djangoapps/courseware/tests/__init__.py
new file mode 100644
index 0000000000..8b13789179
--- /dev/null
+++ b/lms/djangoapps/courseware/tests/__init__.py
@@ -0,0 +1 @@
+
diff --git a/lms/djangoapps/courseware/tests/tests.py b/lms/djangoapps/courseware/tests/tests.py
new file mode 100644
index 0000000000..8e9d13f8d5
--- /dev/null
+++ b/lms/djangoapps/courseware/tests/tests.py
@@ -0,0 +1,208 @@
+import copy
+import json
+import os
+
+from pprint import pprint
+
+from django.test import TestCase
+from django.test.client import Client
+from mock import patch, Mock
+from override_settings import override_settings
+from django.conf import settings
+from django.core.urlresolvers import reverse
+from path import path
+
+from student.models import Registration
+from django.contrib.auth.models import User
+
+from xmodule.modulestore.django import modulestore
+import xmodule.modulestore.django
+from xmodule.modulestore import Location
+from xmodule.modulestore.xml_importer import import_from_xml
+
+
+def parse_json(response):
+ """Parse response, which is assumed to be json"""
+ return json.loads(response.content)
+
+
+def user(email):
+ '''look up a user by email'''
+ return User.objects.get(email=email)
+
+
+def registration(email):
+ '''look up registration object by email'''
+ return Registration.objects.get(user__email=email)
+
+
+# A bit of a hack--want mongo modulestore for these tests, until
+# jump_to works with the xmlmodulestore or we have an even better solution
+# NOTE: this means this test requires mongo to be running.
+
+def mongo_store_config(data_dir):
+ return {
+ 'default': {
+ 'ENGINE': 'xmodule.modulestore.mongo.MongoModuleStore',
+ 'OPTIONS': {
+ 'default_class': 'xmodule.raw_module.RawDescriptor',
+ 'host': 'localhost',
+ 'db': 'xmodule',
+ 'collection': 'modulestore',
+ 'fs_root': data_dir,
+ }
+ }
+}
+
+TEST_DATA_DIR = settings.COMMON_TEST_DATA_ROOT
+TEST_DATA_MODULESTORE = mongo_store_config(TEST_DATA_DIR)
+
+REAL_DATA_DIR = settings.GITHUB_REPO_ROOT
+REAL_DATA_MODULESTORE = mongo_store_config(REAL_DATA_DIR)
+
+class ActivateLoginTestCase(TestCase):
+ '''Check that we can activate and log in'''
+
+ def setUp(self):
+ email = 'view@test.com'
+ password = 'foo'
+ self.create_account('viewtest', email, password)
+ self.activate_user(email)
+ self.login(email, password)
+
+ # ============ User creation and login ==============
+
+ def _login(self, email, pw):
+ '''Login. View should always return 200. The success/fail is in the
+ returned json'''
+ resp = self.client.post(reverse('login'),
+ {'email': email, 'password': pw})
+ self.assertEqual(resp.status_code, 200)
+ return resp
+
+ def login(self, email, pw):
+ '''Login, check that it worked.'''
+ resp = self._login(email, pw)
+ data = parse_json(resp)
+ self.assertTrue(data['success'])
+ return resp
+
+ def _create_account(self, username, email, pw):
+ '''Try to create an account. No error checking'''
+ resp = self.client.post('/create_account', {
+ 'username': username,
+ 'email': email,
+ 'password': pw,
+ 'name': 'Fred Weasley',
+ 'terms_of_service': 'true',
+ 'honor_code': 'true',
+ })
+ return resp
+
+ def create_account(self, username, email, pw):
+ '''Create the account and check that it worked'''
+ resp = self._create_account(username, email, pw)
+ self.assertEqual(resp.status_code, 200)
+ data = parse_json(resp)
+ self.assertEqual(data['success'], True)
+
+ # Check both that the user is created, and inactive
+ self.assertFalse(user(email).is_active)
+
+ return resp
+
+ def _activate_user(self, email):
+ '''Look up the activation key for the user, then hit the activate view.
+ No error checking'''
+ activation_key = registration(email).activation_key
+
+ # and now we try to activate
+ resp = self.client.get(reverse('activate', kwargs={'key': activation_key}))
+ return resp
+
+ def activate_user(self, email):
+ resp = self._activate_user(email)
+ self.assertEqual(resp.status_code, 200)
+ # Now make sure that the user is now actually activated
+ self.assertTrue(user(email).is_active)
+
+ def test_activate_login(self):
+ '''The setup function does all the work'''
+ pass
+
+
+class PageLoader(ActivateLoginTestCase):
+ ''' Base class that adds a function to load all pages in a modulestore '''
+
+ def check_pages_load(self, course_name, data_dir, modstore):
+ print "Checking course {0} in {1}".format(course_name, data_dir)
+ import_from_xml(modstore, data_dir, [course_name])
+
+ n = 0
+ num_bad = 0
+ all_ok = True
+ for descriptor in modstore.get_items(
+ Location(None, None, None, None, None)):
+ n += 1
+ print "Checking ", descriptor.location.url()
+ #print descriptor.__class__, descriptor.location
+ resp = self.client.get(reverse('jump_to',
+ kwargs={'location': descriptor.location.url()}))
+ msg = str(resp.status_code)
+
+ if resp.status_code != 200:
+ msg = "ERROR " + msg
+ all_ok = False
+ num_bad += 1
+ print msg
+ self.assertTrue(all_ok) # fail fast
+
+ print "{0}/{1} good".format(n - num_bad, n)
+ self.assertTrue(all_ok)
+
+
+@override_settings(MODULESTORE=TEST_DATA_MODULESTORE)
+class TestCoursesLoadTestCase(PageLoader):
+ '''Check that all pages in test courses load properly'''
+
+ def setUp(self):
+ ActivateLoginTestCase.setUp(self)
+ xmodule.modulestore.django._MODULESTORES = {}
+ xmodule.modulestore.django.modulestore().collection.drop()
+
+ def test_toy_course_loads(self):
+ self.check_pages_load('toy', TEST_DATA_DIR, modulestore())
+
+ def test_full_course_loads(self):
+ self.check_pages_load('full', TEST_DATA_DIR, modulestore())
+
+
+ # ========= TODO: check ajax interaction here too?
+
+
+@override_settings(MODULESTORE=REAL_DATA_MODULESTORE)
+class RealCoursesLoadTestCase(PageLoader):
+ '''Check that all pages in real courses load properly'''
+
+ def setUp(self):
+ ActivateLoginTestCase.setUp(self)
+ xmodule.modulestore.django._MODULESTORES = {}
+ xmodule.modulestore.django.modulestore().collection.drop()
+
+ # TODO: Disabled test for now.. Fix once things are cleaned up.
+ def Xtest_real_courses_loads(self):
+ '''See if any real courses are available at the REAL_DATA_DIR.
+ If they are, check them.'''
+
+ # TODO: adjust staticfiles_dirs
+ if not os.path.isdir(REAL_DATA_DIR):
+ # No data present. Just pass.
+ return
+
+ courses = [course_dir for course_dir in os.listdir(REAL_DATA_DIR)
+ if os.path.isdir(REAL_DATA_DIR / course_dir)]
+ for course in courses:
+ self.check_pages_load(course, REAL_DATA_DIR, modulestore())
+
+
+ # ========= TODO: check ajax interaction here too?
diff --git a/lms/djangoapps/courseware/views.py b/lms/djangoapps/courseware/views.py
index 74fb001f80..8cdd5c2fd6 100644
--- a/lms/djangoapps/courseware/views.py
+++ b/lms/djangoapps/courseware/views.py
@@ -25,12 +25,16 @@ from models import StudentModuleCache
from student.models import UserProfile
from multicourse import multicourse_settings
from django_comment_client.utils import get_discussion_title
+from xmodule.modulestore import Location
+from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
+from xmodule.modulestore.django import modulestore
+from xmodule.course_module import CourseDescriptor
from util.cache import cache, cache_if_anonymous
from student.models import UserTestGroup, CourseEnrollment
from courseware import grades
from courseware.courses import check_course
-from xmodule.modulestore.django import modulestore
+
import comment_client
@@ -206,65 +210,59 @@ def index(request, course_id, chapter=None, section=None,
if look_for_module:
# TODO (cpennington): Pass the right course in here
- section = get_section(course, 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()
+ section_descriptor = get_section(course, chapter, section)
+ if section_descriptor is not None:
+ student_module_cache = StudentModuleCache(request.user,
+ section_descriptor)
+ module, _, _, _ = get_module(request.user, request,
+ section_descriptor.location,
+ student_module_cache)
+ context['content'] = module.get_html()
+ else:
+ log.warning("Couldn't find a section descriptor for course_id '{0}',"
+ "chapter '{1}', section '{2}'".format(
+ course_id, chapter, section))
+
result = render_to_response('courseware.html', context)
return result
-
-def jump_to(request, probname=None):
+@ensure_csrf_cookie
+def jump_to(request, location):
'''
- Jump to viewing a specific problem. The problem is specified by a
- problem name - currently the filename (minus .xml) of the problem.
- Maybe this should change to a more generic tag, eg "name" given as
- an attribute in .
+ Show the page that contains a specific location.
- We do the jump by (1) reading course.xml to find the first
- instance of with the given filename, then (2) finding
- the parent element of the problem, then (3) rendering that parent
- element with a specific computed position value (if it is
- ).
+ If the location is invalid, return a 404.
+ If the location is valid, but not present in a course, ?
+
+ If the location is valid, but in a course the current user isn't registered for, ?
+ TODO -- let the index view deal with it?
'''
- # get coursename if stored
- coursename = multicourse_settings.get_coursename_from_request(request)
+ # Complain if the location isn't valid
+ try:
+ location = Location(location)
+ except InvalidLocationError:
+ raise Http404("Invalid location")
- # begin by getting course.xml tree
- xml = content_parser.course_file(request.user, coursename)
+ # Complain if there's not data for this location
+ try:
+ (course_id, chapter, section, position) = modulestore().path_to_location(location)
+ except ItemNotFoundError:
+ raise Http404("No data at this location: {0}".format(location))
+ except NoPathToItem:
+ raise Http404("This location is not in any class: {0}".format(location))
- # look for problem of given name
- pxml = xml.xpath('//problem[@filename="%s"]' % probname)
- if pxml:
- pxml = pxml[0]
-
- # get the parent element
- parent = pxml.getparent()
-
- # figure out chapter and section names
- chapter = None
- section = None
- branch = parent
- for k in range(4): # max depth of recursion
- if branch.tag == 'section':
- section = branch.get('name')
- if branch.tag == 'chapter':
- chapter = branch.get('name')
- branch = branch.getparent()
-
- position = None
- if parent.tag == 'sequential':
- position = parent.index(pxml) + 1 # position in sequence
-
- return index(request,
- course=coursename, chapter=chapter,
- section=section, position=position)
+ return index(request, course_id, chapter, section, position)
@ensure_csrf_cookie
def course_info(request, course_id):
+ '''
+ Display the course's info.html, or 404 if there is no such course.
+
+ Assumes the course_id is in a valid format.
+ '''
course = check_course(course_id)
return render_to_response('info.html', {'course': course})
diff --git a/lms/envs/README.txt b/lms/envs/README.txt
index 7a527b290f..6d1512e6f6 100644
--- a/lms/envs/README.txt
+++ b/lms/envs/README.txt
@@ -1,14 +1,16 @@
-Transitional for moving to new settings scheme.
+Transitional for moving to new settings scheme.
-To use:
- django-admin.py runserver --settings=envs.dev --pythonpath=.
+To use:
+ rake lms
+ or
+ django-admin.py runserver --settings=lms.envs.dev --pythonpath=.
NOTE: Using manage.py will automatically run mitx/settings.py first, regardless
of what you send it for an explicit --settings flag. It still works, but might
-have odd side effects. Using django-admin.py avoids that problem.
+have odd side effects. Using django-admin.py avoids that problem.
django-admin.py is installed by default when you install Django.
To use with gunicorn_django in debug mode:
- gunicorn_django envs/dev.py
+ gunicorn_django lms/envs/dev.py
diff --git a/lms/envs/aws.py b/lms/envs/aws.py
index bd2a0fc389..460ec18d27 100644
--- a/lms/envs/aws.py
+++ b/lms/envs/aws.py
@@ -32,12 +32,12 @@ LOG_DIR = ENV_TOKENS['LOG_DIR']
CACHES = ENV_TOKENS['CACHES']
-for feature,value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
+for feature, value in ENV_TOKENS.get('MITX_FEATURES', {}).items():
MITX_FEATURES[feature] = value
-WIKI_ENABLED = ENV_TOKENS.get('WIKI_ENABLED',WIKI_ENABLED)
+WIKI_ENABLED = ENV_TOKENS.get('WIKI_ENABLED', WIKI_ENABLED)
-LOGGING = get_logger_config(LOG_DIR,
+LOGGING = get_logger_config(LOG_DIR,
logging_env=ENV_TOKENS['LOGGING_ENV'],
syslog_addr=(ENV_TOKENS['SYSLOG_SERVER'], 514),
debug=False)
diff --git a/lms/envs/common.py b/lms/envs/common.py
index 17e4decf3b..b3d1d1f1e9 100644
--- a/lms/envs/common.py
+++ b/lms/envs/common.py
@@ -6,16 +6,16 @@ MITX_FEATURES[...]. Modules that extend this one can change the feature
configuration in an environment specific config file and re-calculate those
values.
-We should make a method that calls all these config methods so that you just
+We should make a method that calls all these config methods so that you just
make one call at the end of your site-specific dev file to reset all the
dependent variables (like INSTALLED_APPS) for you.
Longer TODO:
-1. Right now our treatment of static content in general and in particular
+1. Right now our treatment of static content in general and in particular
course-specific static content is haphazard.
2. We should have a more disciplined approach to feature flagging, even if it
just means that we stick them in a dict called MITX_FEATURES.
-3. We need to handle configuration for multiple courses. This could be as
+3. We need to handle configuration for multiple courses. This could be as
multiple sites, but we do need a way to map their data assets.
"""
import sys
@@ -43,7 +43,7 @@ MITX_FEATURES = {
'SAMPLE' : False,
'USE_DJANGO_PIPELINE' : True,
'DISPLAY_HISTOGRAMS_TO_STAFF' : True,
- 'REROUTE_ACTIVATION_EMAIL' : False, # nonempty string = address for all activation emails
+ 'REROUTE_ACTIVATION_EMAIL' : False, # nonempty string = address for all activation emails
'DEBUG_LEVEL' : 0, # 0 = lowest level, least verbose, 255 = max level, most verbose
## DO NOT SET TO True IN THIS FILE
@@ -62,7 +62,7 @@ PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/lms
REPO_ROOT = PROJECT_ROOT.dirname()
COMMON_ROOT = REPO_ROOT / "common"
ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /mitx is in
-ASKBOT_ROOT = ENV_ROOT / "askbot-devel"
+ASKBOT_ROOT = REPO_ROOT / "askbot"
COURSES_ROOT = ENV_ROOT / "data"
# FIXME: To support multiple courses, we should walk the courses dir at startup
@@ -86,7 +86,7 @@ MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates',
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates']
-# This is where Django Template lookup is defined. There are a few of these
+# This is where Django Template lookup is defined. There are a few of these
# still left lying around.
TEMPLATE_DIRS = (
PROJECT_ROOT / "templates",
@@ -104,8 +104,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
)
-# FIXME:
-# We should have separate S3 staged URLs in case we need to make changes to
+# FIXME:
+# We should have separate S3 staged URLs in case we need to make changes to
# these assets and test them.
LIB_URL = '/static/js/'
@@ -121,7 +121,7 @@ STATIC_GRAB = False
DEV_CONTENT = True
# FIXME: Should we be doing this truncation?
-TRACK_MAX_EVENT = 10000
+TRACK_MAX_EVENT = 10000
DEBUG_TRACK_LOG = False
MITX_ROOT_URL = ''
@@ -130,7 +130,7 @@ COURSE_NAME = "6.002_Spring_2012"
COURSE_NUMBER = "6.002x"
COURSE_TITLE = "Circuits and Electronics"
-### Dark code. Should be enabled in local settings for devel.
+### Dark code. Should be enabled in local settings for devel.
ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome)
QUICKEDIT = False
@@ -212,9 +212,9 @@ USE_L10N = True
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
#################################### AWS #######################################
-# S3BotoStorage insists on a timeout for uploaded assets. We should make it
+# S3BotoStorage insists on a timeout for uploaded assets. We should make it
# permanent instead, but rather than trying to figure out exactly where that
-# setting is, I'm just bumping the expiration time to something absurd (100
+# setting is, I'm just bumping the expiration time to something absurd (100
# years). This is only used if DEFAULT_FILE_STORAGE is overriden to use S3
# in the global settings.py
AWS_QUERYSTRING_EXPIRE = 10 * 365 * 24 * 60 * 60 # 10 years
@@ -281,7 +281,7 @@ MIDDLEWARE_CLASSES = (
# Instead of AuthenticationMiddleware, we use a cached backed version
#'django.contrib.auth.middleware.AuthenticationMiddleware',
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
-
+
'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware',
'mitxmako.middleware.MakoMiddleware',
@@ -303,15 +303,15 @@ STATICFILES_STORAGE = 'pipeline.storage.PipelineCachedStorage'
PIPELINE_CSS = {
'application': {
'source_filenames': ['sass/application.scss'],
- 'output_filename': 'css/application.css',
+ 'output_filename': 'css/lms-application.css',
},
'course': {
'source_filenames': ['sass/course.scss', 'js/vendor/CodeMirror/codemirror.css', 'css/vendor/jquery.treeview.css'],
- 'output_filename': 'css/course.css',
+ 'output_filename': 'css/lms-course.css',
},
'ie-fixes': {
'source_filenames': ['sass/ie.scss'],
- 'output_filename': 'css/ie.css',
+ 'output_filename': 'css/lms-ie.css',
},
}
@@ -412,23 +412,23 @@ PIPELINE_JS = {
'js/toggle_login_modal.js',
'js/sticky_filter.js',
],
- 'output_filename': 'js/application.js'
+ 'output_filename': 'js/lms-application.js'
},
'courseware': {
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in courseware_only_js],
- 'output_filename': 'js/courseware.js'
+ 'output_filename': 'js/lms-courseware.js'
},
'main_vendor': {
'source_filenames': main_vendor_js,
- 'output_filename': 'js/main_vendor.js',
+ 'output_filename': 'js/lms-main_vendor.js',
},
'module-js': {
'source_filenames': module_js_sources,
- 'output_filename': 'js/modules.js',
+ 'output_filename': 'js/lms-modules.js',
},
'spec': {
'source_filenames': [pth.replace(PROJECT_ROOT / 'static/', '') for pth in glob2.glob(PROJECT_ROOT / 'static/coffee/spec/**/*.coffee')],
- 'output_filename': 'js/spec.js'
+ 'output_filename': 'js/lms-spec.js'
}
}
diff --git a/lms/envs/test.py b/lms/envs/test.py
index fdfbfb20c4..e6fedcb373 100644
--- a/lms/envs/test.py
+++ b/lms/envs/test.py
@@ -12,17 +12,21 @@ from .logsettings import get_logger_config
import os
from path import path
-INSTALLED_APPS = [
- app
- for app
- in INSTALLED_APPS
- if not app.startswith('askbot')
-]
+# can't test start dates with this True, but on the other hand,
+# can test everything else :)
+MITX_FEATURES['DISABLE_START_DATES'] = True
+
+# Need wiki for courseware views to work. TODO (vshnayder): shouldn't need it.
+WIKI_ENABLED = True
+
+# Makes the tests run much faster...
+SOUTH_TESTS_MIGRATE = False # To disable migrations and use syncdb instead
# Nose Test Runner
-INSTALLED_APPS += ['django_nose']
+INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html',
- '--cover-inclusive', '--cover-html-dir', os.environ.get('NOSE_COVER_HTML_DIR', 'cover_html')]
+ '--cover-inclusive', '--cover-html-dir',
+ os.environ.get('NOSE_COVER_HTML_DIR', 'cover_html')]
for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
@@ -30,25 +34,23 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
# Local Directories
TEST_ROOT = path("test_root")
# Want static files in the same dir for running on jenkins.
-STATIC_ROOT = TEST_ROOT / "staticfiles"
+STATIC_ROOT = TEST_ROOT / "staticfiles"
COURSES_ROOT = TEST_ROOT / "data"
DATA_DIR = COURSES_ROOT
-MAKO_TEMPLATES['course'] = [DATA_DIR]
-MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections']
-MAKO_TEMPLATES['custom_tags'] = [DATA_DIR / 'custom_tags']
-MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
- DATA_DIR / 'info',
- DATA_DIR / 'problems']
-LOGGING = get_logger_config(TEST_ROOT / "log",
+LOGGING = get_logger_config(TEST_ROOT / "log",
logging_env="dev",
tracking_filename="tracking.log",
debug=True)
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data"
+# Where the content data is checked out. This may not exist on jenkins.
+GITHUB_REPO_ROOT = ENV_ROOT / "data"
-# TODO (cpennington): We need to figure out how envs/test.py can inject things into common.py so that we don't have to repeat this sort of thing
+
+# TODO (cpennington): We need to figure out how envs/test.py can inject things
+# into common.py so that we don't have to repeat this sort of thing
STATICFILES_DIRS = [
COMMON_ROOT / "static",
PROJECT_ROOT / "static",
@@ -67,7 +69,7 @@ DATABASES = {
}
CACHES = {
- # This is the cache used for most things. Askbot will not work without a
+ # This is the cache used for most things. Askbot will not work without a
# functioning cache -- it relies on caching to load its settings in places.
# In staging/prod envs, the sessions also live here.
'default': {
diff --git a/lms/envs/with_cms.py b/lms/envs/with_cms.py
new file mode 100644
index 0000000000..b807a0f545
--- /dev/null
+++ b/lms/envs/with_cms.py
@@ -0,0 +1,10 @@
+"""
+Settings for the LMS that runs alongside the CMS on AWS
+"""
+
+from .aws import *
+
+with open(ENV_ROOT / "cms.auth.json") as auth_file:
+ CMS_AUTH_TOKENS = json.load(auth_file)
+
+MODULESTORE = CMS_AUTH_TOKENS['MODULESTORE']
diff --git a/lms/static/.gitignore b/lms/static/.gitignore
index 03f1cdabff..afc42d5e7e 100644
--- a/lms/static/.gitignore
+++ b/lms/static/.gitignore
@@ -5,4 +5,5 @@
*.orig
*.DS_Store
application.css
-ie.css
\ No newline at end of file
+ie.css
+Gemfile.lock
diff --git a/lms/urls.py b/lms/urls.py
index 91ef0bcff9..783e9f3fce 100644
--- a/lms/urls.py
+++ b/lms/urls.py
@@ -13,23 +13,23 @@ if settings.DEBUG:
urlpatterns = ('',
url(r'^$', 'student.views.index', name="root"), # Main marketing page, or redirect to courseware
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
-
+
url(r'^change_email$', 'student.views.change_email_request'),
url(r'^email_confirm/(?P[^/]*)$', 'student.views.confirm_email_change'),
url(r'^change_name$', 'student.views.change_name_request'),
url(r'^accept_name_change$', 'student.views.accept_name_change'),
url(r'^reject_name_change$', 'student.views.reject_name_change'),
url(r'^pending_name_changes$', 'student.views.pending_name_changes'),
-
+
url(r'^event$', 'track.views.user_track'),
url(r'^t/(?P[^/]*)$', 'static_template_view.views.index'), # TODO: Is this used anymore? What is STATIC_GRAB?
-
- url(r'^login$', 'student.views.login_user'),
+
+ url(r'^login$', 'student.views.login_user', name="login"),
url(r'^login/(?P[^/]*)$', 'student.views.login_user'),
url(r'^logout$', 'student.views.logout_user', name='logout'),
url(r'^create_account$', 'student.views.create_account'),
- url(r'^activate/(?P[^/]*)$', 'student.views.activate_account'),
-
+ url(r'^activate/(?P[^/]*)$', 'student.views.activate_account', name="activate"),
+
url(r'^password_reset/$', 'student.views.password_reset', name='password_reset'),
## Obsolete Django views for password resets
## TODO: Replace with Mako-ized views
@@ -44,48 +44,48 @@ urlpatterns = ('',
name='auth_password_reset_complete'),
url(r'^password_reset_done/$', django.contrib.auth.views.password_reset_done,
name='auth_password_reset_done'),
-
+
url(r'^heartbeat$', include('heartbeat.urls')),
-
+
url(r'^university_profile/(?P[^/]+)$', 'courseware.views.university_profile', name="university_profile"),
-
+
#Semi-static views (these need to be rendered and have the login bar, but don't change)
- url(r'^404$', 'static_template_view.views.render',
+ url(r'^404$', 'static_template_view.views.render',
{'template': '404.html'}, name="404"),
- url(r'^about$', 'static_template_view.views.render',
+ url(r'^about$', 'static_template_view.views.render',
{'template': 'about.html'}, name="about_edx"),
- url(r'^jobs$', 'static_template_view.views.render',
+ url(r'^jobs$', 'static_template_view.views.render',
{'template': 'jobs.html'}, name="jobs"),
- url(r'^contact$', 'static_template_view.views.render',
+ url(r'^contact$', 'static_template_view.views.render',
{'template': 'contact.html'}, name="contact"),
url(r'^press$', 'student.views.press', name="press"),
- url(r'^faq$', 'static_template_view.views.render',
+ url(r'^faq$', 'static_template_view.views.render',
{'template': 'faq.html'}, name="faq_edx"),
- url(r'^help$', 'static_template_view.views.render',
+ url(r'^help$', 'static_template_view.views.render',
{'template': 'help.html'}, name="help_edx"),
- url(r'^tos$', 'static_template_view.views.render',
+ url(r'^tos$', 'static_template_view.views.render',
{'template': 'tos.html'}, name="tos"),
- url(r'^privacy$', 'static_template_view.views.render',
+ url(r'^privacy$', 'static_template_view.views.render',
{'template': 'privacy.html'}, name="privacy_edx"),
# TODO: (bridger) The copyright has been removed until it is updated for edX
- # url(r'^copyright$', 'static_template_view.views.render',
+ # url(r'^copyright$', 'static_template_view.views.render',
# {'template': 'copyright.html'}, name="copyright"),
- url(r'^honor$', 'static_template_view.views.render',
+ url(r'^honor$', 'static_template_view.views.render',
{'template': 'honor.html'}, name="honor"),
-
- #Press releases
- url(r'^press/mit-and-harvard-announce-edx$', 'static_template_view.views.render',
+
+ #Press releases
+ url(r'^press/mit-and-harvard-announce-edx$', 'static_template_view.views.render',
{'template': 'press_releases/MIT_and_Harvard_announce_edX.html'}, name="press/mit-and-harvard-announce-edx"),
- url(r'^press/uc-berkeley-joins-edx$', 'static_template_view.views.render',
+ url(r'^press/uc-berkeley-joins-edx$', 'static_template_view.views.render',
{'template': 'press_releases/UC_Berkeley_joins_edX.html'}, name="press/uc-berkeley-joins-edx"),
# Should this always update to point to the latest press release?
- (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}),
-
-
-
+ (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}),
+
+
+
(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
-
+
# TODO: These urls no longer work. They need to be updated before they are re-enabled
# url(r'^send_feedback$', 'util.views.send_feedback'),
# url(r'^reactivate/(?P[^/]*)$', 'student.views.reactivation_email'),
@@ -97,42 +97,43 @@ if settings.PERFSTATS:
if settings.COURSEWARE_ENABLED:
urlpatterns += (
url(r'^masquerade/', include('masquerade.urls')),
- url(r'^jumpto/(?P[^/]+)/$', 'courseware.views.jump_to'),
+ url(r'^jump_to/(?P.*)$', 'courseware.views.jump_to', name="jump_to"),
+
url(r'^modx/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
url(r'^xqueue/(?P[^/]*)/(?P.*?)/(?P[^/]*)$', 'courseware.module_render.xqueue_callback'),
url(r'^change_setting$', 'student.views.change_setting'),
-
+
# TODO: These views need to be updated before they work
# url(r'^calculate$', 'util.views.calculate'),
# url(r'^gradebook$', 'courseware.views.gradebook'),
# TODO: We should probably remove the circuit package. I believe it was only used in the old way of saving wiki circuits for the wiki
# url(r'^edit_circuit/(?P[^/]*)$', 'circuit.views.edit_circuit'),
# url(r'^save_circuit/(?P[^/]*)$', 'circuit.views.save_circuit'),
-
- url(r'^courses/?$', 'courseware.views.courses', name="courses"),
- url(r'^change_enrollment$',
+
+ url(r'^courses/?$', 'courseware.views.courses', name="courses"),
+ url(r'^change_enrollment$',
'student.views.change_enrollment_view', name="change_enrollment"),
-
+
#About the course
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/about$',
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/about$',
'courseware.views.course_about', name="about_course"),
-
+
#Inside the course
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/info$',
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/info$',
'courseware.views.course_info', name="info"),
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book$',
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book$',
'staticbook.views.index', name="book"),
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book/(?P[^/]*)$',
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book/(?P[^/]*)$',
'staticbook.views.index'),
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book-shifted/(?P[^/]*)$',
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/book-shifted/(?P[^/]*)$',
'staticbook.views.index_shifted'),
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/courseware/?$',
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/courseware/?$',
'courseware.views.index', name="courseware"),
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/courseware/(?P[^/]*)/(?P[^/]*)/$',
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/courseware/(?P[^/]*)/(?P[^/]*)/$',
'courseware.views.index', name="courseware_section"),
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/profile$',
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/profile$',
'courseware.views.profile', name="profile"),
- url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/profile/(?P[^/]*)/$',
+ url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/profile/(?P[^/]*)/$',
'courseware.views.profile'),
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/news$',
'courseware.views.news', name="news"),
@@ -141,7 +142,7 @@ if settings.COURSEWARE_ENABLED:
url(r'^courses/(?P[^/]+/[^/]+/[^/]+)/discussion/',
include('django_comment_client.urls')),
)
-
+
# Multicourse wiki
if settings.WIKI_ENABLED:
urlpatterns += (
@@ -170,9 +171,9 @@ urlpatterns = patterns(*urlpatterns)
if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
-
-
-#Custom error pages
+
+
+#Custom error pages
handler404 = 'static_template_view.views.render_404'
handler500 = 'static_template_view.views.render_500'
diff --git a/rakefile b/rakefile
index fd23b9643a..01491ce981 100644
--- a/rakefile
+++ b/rakefile
@@ -27,7 +27,7 @@ NORMALIZED_DEPLOY_NAME = DEPLOY_NAME.downcase().gsub(/[_\/]/, '-')
INSTALL_DIR_PATH = File.join(DEPLOY_DIR, NORMALIZED_DEPLOY_NAME)
PIP_REPO_REQUIREMENTS = "#{INSTALL_DIR_PATH}/repo-requirements.txt"
# Set up the clean and clobber tasks
-CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage', 'test_root/*_repo')
+CLOBBER.include(BUILD_DIR, REPORT_DIR, 'cover*', '.coverage', 'test_root/*_repo', 'test_root/staticfiles')
CLEAN.include("#{BUILD_DIR}/*.deb", "#{BUILD_DIR}/util")
def select_executable(*cmds)
@@ -51,6 +51,11 @@ default_options = {
task :predjango do
sh("find . -type f -name *.pyc -delete")
sh('pip install -e common/lib/xmodule')
+ sh('git submodule update --init')
+end
+
+task :clean_test_files do
+ sh("git clean -fdx test_root")
end
[:lms, :cms, :common].each do |system|
@@ -92,7 +97,7 @@ end
# Per System tasks
desc "Run all django tests on our djangoapps for the #{system}"
- task "test_#{system}" => ["#{system}:collectstatic:test", "fasttest_#{system}"]
+ task "test_#{system}" => ["clean_test_files", "#{system}:collectstatic:test", "fasttest_#{system}"]
# Have a way to run the tests without running collectstatic -- useful when debugging without
# messing with static files.
@@ -150,14 +155,14 @@ end
task :package do
FileUtils.mkdir_p(BUILD_DIR)
-
+
Dir.chdir(BUILD_DIR) do
afterremove = Tempfile.new('afterremove')
afterremove.write <<-AFTERREMOVE.gsub(/^\s*/, '')
#! /bin/bash
set -e
set -x
-
+
# to be a little safer this rm is executed
# as the makeitso user