Commit 036cc6b8 by Calen Pennington

Merge pull request #313 from MITx/feature/victor/xml-verify

Feature/victor/xml verify
parents 2984d7a4 05ad0ba1
[submodule "askbot"]
path = askbot
url = git@github.com:MITx/askbot-devel.git
Subproject commit 1c3381046c78e055439ba1c78e0df48410fcc13e
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.xml import XMLModuleStore
import logging
log = logging.getLogger(__name__)
def import_from_xml(data_dir, course_dirs=None):
"""
Import the specified xml data_dir into the django defined modulestore,
using org and course as the location org and course.
"""
module_store = XMLModuleStore(
data_dir,
default_class='xmodule.raw_module.RawDescriptor',
eager=True,
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:
modulestore().create_item(module.location)
except:
log.exception('Item already exists at %s' % module.location.url())
pass
if 'data' in module.definition:
modulestore().update_item(module.location, module.definition['data'])
if 'children' in module.definition:
modulestore().update_children(module.location, module.definition['children'])
modulestore().update_metadata(module.location, dict(module.metadata))
return module_store
......@@ -3,7 +3,9 @@
###
from django.core.management.base import BaseCommand, CommandError
from contentstore import import_from_xml
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
unnamed_modules = 0
......@@ -21,4 +23,4 @@ class Command(BaseCommand):
course_dirs = args[1:]
else:
course_dirs = None
import_from_xml(data_dir, course_dirs)
import_from_xml(modulestore(), data_dir, course_dirs)
......@@ -12,7 +12,7 @@ from django.contrib.auth.models import User
from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django
from xmodule.modulestore import Location
from contentstore import import_from_xml
from xmodule.modulestore.xml_importer import import_from_xml
import copy
......@@ -74,7 +74,7 @@ class ContentStoreTestCase(TestCase):
return resp
def _activate_user(self, email):
'''look up the user's activation key in the db, then hit the activate view.
'''Look up the activation key for the user, then hit the activate view.
No error checking'''
activation_key = registration(email).activation_key
......@@ -196,7 +196,7 @@ class EditTestCase(ContentStoreTestCase):
xmodule.modulestore.django.modulestore().collection.drop()
def check_edit_item(self, test_course_name):
import_from_xml('common/test/data/', test_course_name)
import_from_xml(modulestore(), 'common/test/data/', [test_course_name])
for descriptor in modulestore().get_items(Location(None, None, None, None, None)):
print "Checking ", descriptor.location.url()
......
......@@ -5,7 +5,8 @@ from django.conf import settings
from fs.osfs import OSFS
from git import Repo, PushInfo
from contentstore import import_from_xml
from xmodule.modulestore.xml_importer import import_from_xml
from xmodule.modulestore.django import modulestore
from xmodule.modulestore import Location
from .exceptions import GithubSyncError
......@@ -56,7 +57,8 @@ def import_from_github(repo_settings):
git_repo = setup_repo(repo_settings)
git_repo.head.reset('origin/%s' % repo_settings['branch'], index=True, working_tree=True)
module_store = import_from_xml(settings.GITHUB_REPO_ROOT, course_dirs=[course_dir])
module_store = import_from_xml(modulestore(),
settings.GITHUB_REPO_ROOT, course_dirs=[course_dir])
return git_repo.head.commit.hexsha, module_store.courses[course_dir]
......
......@@ -57,7 +57,7 @@ class GithubSyncTestCase(TestCase):
"""
self.assertEquals('Toy Course', self.import_course.metadata['display_name'])
self.assertIn(
Location('i4x://edx/local_repo/chapter/Overview'),
Location('i4x://edX/toy/chapter/Overview'),
[child.location for child in self.import_course.get_children()])
self.assertEquals(1, len(self.import_course.get_children()))
......
......@@ -30,6 +30,7 @@ from django_future.csrf import ensure_csrf_cookie
from student.models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
from util.cache import cache_if_anonymous
from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError
......
......@@ -12,8 +12,8 @@ log = logging.getLogger(__name__)
def process_includes(fn):
"""
Wraps a XModuleDescriptor.from_xml method, and modifies xml_data to replace
any immediate child <include> items with the contents of the file that they are
supposed to include
any immediate child <include> items with the contents of the file that they
are supposed to include
"""
@wraps(fn)
def from_xml(cls, xml_data, system, org=None, course=None):
......@@ -25,15 +25,21 @@ def process_includes(fn):
try:
ifp = system.resources_fs.open(file)
except Exception:
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True)))
log.exception('Cannot find file %s in %s' % (file, dir))
msg = 'Error in problem xml include: %s\n' % (
etree.tostring(next_include, pretty_print=True))
msg += 'Cannot find file %s in %s' % (file, dir)
log.exception(msg)
system.error_handler(msg)
raise
try:
# read in and convert to XML
incxml = etree.XML(ifp.read())
except Exception:
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True)))
log.exception('Cannot parse XML in %s' % (file))
msg = 'Error in problem xml include: %s\n' % (
etree.tostring(next_include, pretty_print=True))
msg += 'Cannot parse XML in %s' % (file)
log.exception(msg)
system.error_handler(msg)
raise
# insert new XML into tree in place of inlcude
parent = next_include.getparent()
......@@ -50,8 +56,8 @@ class SemanticSectionDescriptor(XModuleDescriptor):
@process_includes
def from_xml(cls, xml_data, system, org=None, course=None):
"""
Removes sections single child elements in favor of just embedding the child element
Removes sections with single child elements in favor of just embedding
the child element
"""
xml_object = etree.fromstring(xml_data)
......@@ -76,7 +82,6 @@ class TranslateCustomTagDescriptor(XModuleDescriptor):
xml_object = etree.fromstring(xml_data)
tag = xml_object.tag
xml_object.tag = 'customtag'
impl = etree.SubElement(xml_object, 'impl')
impl.text = tag
xml_object.attrib['impl'] = tag
return system.process_xml(etree.tostring(xml_object))
......@@ -67,7 +67,8 @@ class ComplexEncoder(json.JSONEncoder):
class CapaModule(XModule):
'''
An XModule implementing LonCapa format problems, implemented by way of capa.capa_problem.LoncapaProblem
An XModule implementing LonCapa format problems, implemented by way of
capa.capa_problem.LoncapaProblem
'''
icon_class = 'problem'
......@@ -77,8 +78,10 @@ class CapaModule(XModule):
js_module_name = "Problem"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
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.attempts = 0
self.max_attempts = None
......@@ -133,7 +136,8 @@ class CapaModule(XModule):
seed = None
try:
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), instance_state, seed=seed, system=self.system)
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
instance_state, seed=seed, system=self.system)
except Exception:
msg = 'cannot create LoncapaProblem %s' % self.location.url()
log.exception(msg)
......@@ -141,15 +145,20 @@ class CapaModule(XModule):
msg = '<p>%s</p>' % msg.replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
# create a dummy problem with error message instead of failing
problem_text = '<problem><text><font color="red" size="+2">Problem %s has an error:</font>%s</text></problem>' % (self.location.url(), msg)
self.lcp = LoncapaProblem(problem_text, self.location.html_id(), instance_state, seed=seed, system=self.system)
problem_text = ('<problem><text><font color="red" size="+2">'
'Problem %s has an error:</font>%s</text></problem>' %
(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] <font size="+1" color="red">Failed to generate HTML for problem %s</font>' % (self.location.url())
msg = (
'[courseware.capa.capa_module] <font size="+1" color="red">'
'Failed to generate HTML for problem %s</font>' %
(self.location.url()))
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
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
......@@ -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):
......
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
......@@ -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
......@@ -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())
......@@ -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
......@@ -13,3 +13,6 @@ class InsufficientSpecificationError(Exception):
class InvalidLocationError(Exception):
pass
class NoPathToItem(Exception):
pass
......@@ -6,32 +6,47 @@ from fs.osfs import OSFS
from itertools import repeat
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)
# 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,
# that assumption will have to change
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
......@@ -127,19 +142,22 @@ 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']))
resource_fs = OSFS(self.fs_root / item.get('data_dir',
item['location']['course']))
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]
......@@ -237,7 +260,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 +273,101 @@ 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)
......@@ -13,14 +13,51 @@ def test_string_roundtrip():
check_string_roundtrip("tag://org/course/category/name/revision")
def test_dict():
input_dict = {
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():
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)
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")
......@@ -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,
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(
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
"""
......
import logging
from .xml import XMLModuleStore
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:
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
......@@ -8,7 +8,7 @@ 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 +31,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(
msg = ("Unable to create xml for problem {loc}. "
"Context: '{context}'".format(
context=lines[line-1][offset - 40:offset + 40],
loc=self.location
))
loc=self.location))
log.exception(msg)
self.system.error_handler(msg)
# no workaround possible, so just re-raise
raise
......@@ -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)
......@@ -21,19 +21,23 @@ class CustomTagModule(XModule):
course.xml::
...
<customtag page="234"><impl>book</impl></customtag>
<customtag page="234" impl="book"/>
...
Renders to::
More information given in <a href="/book/234">the text</a>
"""
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):
......
......@@ -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):
......
......@@ -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.
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__)
......@@ -9,17 +9,22 @@ 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 +75,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,16 +100,36 @@ 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):
"""
Return the definition to be passed to the newly created descriptor
......@@ -105,12 +137,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,7 +168,7 @@ 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
"""
......@@ -145,9 +179,11 @@ class XmlDescriptor(XModuleDescriptor):
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 +193,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,17 +206,21 @@ 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)
msg = 'Unable to load file contents at path %s' % filepath
log.exception(msg)
system.error_handler(msg)
# if error_handler didn't reraise, work around problem.
return {'data': 'Error loading file contents at path %s' % filepath}
cls.clean_metadata_from_xml(definition_xml)
return cls.definition_from_xml(definition_xml, system)
# VS[compat] -- just have the url_name lookup once translation is done
slug = xml_object.get('url_name', xml_object.get('slug'))
return cls(
system,
LazyLoadingDict(definition_loader),
......@@ -187,60 +228,90 @@ class XmlDescriptor(XModuleDescriptor):
org,
course,
xml_object.tag,
xml_object.get('slug')],
slug],
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
xml_object.set('filename', self.name)
# 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__)
......@@ -3,6 +3,7 @@
<sequential filename="System_Usage_Sequence" slug="System_Usage_Sequence" format="Lecture Sequence" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="System Usage Sequence"/>
<vertical slug="Lab0_Using_the_tools" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="Lab0: Using the tools">
<html slug="html_19" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
<html slug="html_5555" filename="html_5555"/>
<problem filename="Lab_0_Using_the_Tools" slug="Lab_0_Using_the_Tools" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Lab 0: Using the Tools"/>
</vertical>
<problem filename="Circuit_Sandbox" slug="Circuit_Sandbox" format="Lab" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Circuit Sandbox"/>
......
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012"/>
<course filename="6.002_Spring_2012" slug="6.002_Spring_2012" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never" name="6.002 Spring 2012" start="2015-07-17T12:00" course="full" org="edx"/>
......@@ -6,7 +6,7 @@
<vertical slug="vertical_1122" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true">
<html filename="Midterm_Exam_1123" slug="Midterm_Exam_1123" graceperiod="0 day 0 hours 5 minutes 0 seconds" rerandomize="per_student" due="April 30, 12:00" graded="true" name="Midterm Exam"/>
</vertical>
<vertical filename="vertical_1124" slug="vertical_1124" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true"/>
<vertical filename="vertical_98" slug="vertical_1124" graceperiod="0 day 0 hours 5 minutes 0 seconds" showanswer="attempted" rerandomize="per_student" due="April 30, 12:00" graded="true"/>
</sequential>
</chapter>
</sequential>
<html slug="html_5555" filename="html_5555> See the <a href="/section/labintro"> Lab Introduction </a> or <a href="/static/handouts/schematic_tutorial.pdf">Interactive Lab Usage Handout </a> for information on how to do the lab </html>
......@@ -2,17 +2,13 @@
<vertical filename="vertical_58" slug="vertical_58" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/>
<vertical slug="vertical_66" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never">
<problem filename="S1E3_AC_power" slug="S1E3_AC_power" name="S1E3: AC power"/>
<customtag tag="S1E3" slug="discuss_67">
<impl>discuss</impl>
</customtag>
<customtag tag="S1E3" slug="discuss_67" impl="discuss"/>
<html slug="html_68"> S1E4 has been removed. </html>
</vertical>
<vertical filename="vertical_89" slug="vertical_89" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/>
<vertical slug="vertical_94" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never">
<video youtube="0.75:XNh13VZhThQ,1.0:XbDRmF6J0K0,1.25:JDty12WEQWk,1.50:wELKGj-5iyM" slug="What_s_next" name="What's next"/>
<html slug="html_95">Minor correction: Six elements (five resistors)</html>
<customtag tag="S1" slug="discuss_96">
<impl>discuss</impl>
</customtag>
<customtag tag="S1" slug="discuss_96" impl="discuss"/>
</vertical>
</sequential>
......@@ -3,5 +3,4 @@
<problem filename="Sample_Numeric_Problem" slug="Sample_Numeric_Problem" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Sample Numeric Problem"/>
<problem filename="Sample_Algebraic_Problem" slug="Sample_Algebraic_Problem" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="false" name="Sample Algebraic Problem"/>
</vertical>
<vertical filename="vertical_16" slug="vertical_16" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/>
</sequential>
<sequential>
<video youtube="1.50:8kARlsUt9lM,1.25:4cLA-IME32w,1.0:pFOrD8k9_p4,0.75:CcgAYu0n0bg" slug="S1V9_Demo_Setup_-_Lumped_Elements" name="S1V9: Demo Setup - Lumped Elements"/>
<customtag tag="S1" slug="discuss_59">
<impl>discuss</impl>
</customtag>
<customtag page="29" slug="book_60">
<impl>book</impl>
</customtag>
<customtag lecnum="1" slug="slides_61">
<impl>slides</impl>
</customtag>
<customtag tag="S1" slug="discuss_59" impl="discuss"/>
<customtag page="29" slug="book_60" impl="book"/>
<customtag lecnum="1" slug="slides_61" impl="slides"/>
</sequential>
......@@ -3,13 +3,7 @@
<h1> </h1>
</html>
<video youtube="1.50:vl9xrfxcr38,1.25:qxNX4REGqx4,1.0:BGU1poJDgOY,0.75:8rK9vnpystQ" slug="S1V14_Summary" name="S1V14: Summary"/>
<customtag tag="S1" slug="discuss_91">
<impl>discuss</impl>
</customtag>
<customtag page="70" slug="book_92">
<impl>book</impl>
</customtag>
<customtag lecnum="1" slug="slides_93">
<impl>slides</impl>
</customtag>
<customtag tag="S1" slug="discuss_91" impl="discuss"/>
<customtag page="70" slug="book_92" impl="book"/>
<customtag lecnum="1" slug="slides_93" impl="slides"/>
</sequential>
<sequential>
<video youtube="0.75:3NIegrCmA5k,1.0:eLAyO33baQ8,1.25:m1zWi_sh4Aw,1.50:EG-fRTJln_E" slug="S2V1_Review_KVL_KCL" name="S2V1: Review KVL, KCL"/>
<customtag tag="S2" slug="discuss_95">
<impl>discuss</impl>
</customtag>
<customtag page="54" slug="book_96">
<impl>book</impl>
</customtag>
<customtag lecnum="2" slug="slides_97">
<impl>slides</impl>
</customtag>
<customtag tag="S2" slug="discuss_95" impl="discuss"/>
<customtag page="54" slug="book_96" impl="book"/>
<customtag lecnum="2" slug="slides_97" impl="slides"/>
</sequential>
<sequential>
<video youtube="0.75:S_1NaY5te8Q,1.0:G_2F9wivspM,1.25:b-r7dISY-Uc,1.50:jjxHom0oXWk" slug="S2V2_Demo-_KVL_KCL" name="S2V2: Demo- KVL, KCL"/>
<customtag tag="S2" slug="discuss_99">
<impl>discuss</impl>
</customtag>
<customtag page="56" slug="book_100">
<impl>book</impl>
</customtag>
<customtag lecnum="2" slug="slides_101">
<impl>slides</impl>
</customtag>
<customtag tag="S2" slug="discuss_99" impl="discuss"/>
<customtag page="56" slug="book_100" impl="book"/>
<customtag lecnum="2" slug="slides_101" impl="slides"/>
</sequential>
<course name="A Simple Course" org="edX" course="simple" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall">
<chapter name="Overview">
<video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/>
<videosequence format="Lecture Sequence" name="A simple sequence">
<html id="toylab" filename="toylab"/>
<video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/>
</videosequence>
<section name="Lecture 2">
<sequential>
<video youtube="1.0:TBvX7HzxexQ"/>
<problem name="L1 Problem 1" points="1" type="lecture" showanswer="attempted" filename="L1_Problem_1" rerandomize="never"/>
</sequential>
</section>
</chapter>
<chapter name="Chapter 2">
<section name="Problem Set 1">
<sequential>
<problem type="lecture" showanswer="attempted" rerandomize="true" title="A simple coding problem" name="Simple coding problem" filename="ps01-simple"/>
</sequential>
</section>
<video name="Lost Video" youtube="1.0:TBvX7HzxexQ"/>
</chapter>
</course>
<b>Lab 2A: Superposition Experiment</b>
<p>Isn't the toy course great?</p>
<?xml version="1.0"?>
<problem>
<p>
<h1>Finger Exercise 1</h1>
</p>
<p>
Here are two definitions: </p>
<ol class="enumerate">
<li>
<p>
Declarative knowledge refers to statements of fact. </p>
</li>
<li>
<p>
Imperative knowledge refers to 'how to' methods. </p>
</li>
</ol>
<p>
Which of the following choices is correct? </p>
<ol class="enumerate">
<li>
<p>
Statement 1 is true, Statement 2 is false </p>
</li>
<li>
<p>
Statement 1 is false, Statement 2 is true </p>
</li>
<li>
<p>
Statement 1 and Statement 2 are both false </p>
</li>
<li>
<p>
Statement 1 and Statement 2 are both true </p>
</li>
</ol>
<p>
<symbolicresponse answer="4">
<textline size="90" math="1"/>
</symbolicresponse>
</p>
</problem>
<problem><style media="all" type="text/css"/>
<text><h2>Paying Off Credit Card Debt</h2>
<p> 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.</p>
<p >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.</p>
<blockquote>
<p><strong>Minimum monthly payment</strong>
= (Minimum monthly payment rate) x (Balance)<br/>
(Minimum monthly payment gets split into interest paid and
principal paid)<br/>
<strong>Interest Paid</strong> = (Annual interest rate) / (12
months) x (Balance)<br/>
<strong>Principal paid</strong> = (Minimum monthly payment) -
(Interest paid)<br/>
<strong>Remaining balance</strong> = Balance - (Principal
paid)</p>
</blockquote>
<p >For month 1, compute the minimum monthly payment by taking 2% of the balance.</p>
<blockquote xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">
<p><strong>Minimum monthly payment</strong>
= .02 x $5000 = $100</p>
<p>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.</p>
<p><strong>Interest paid</strong> = .18/12 x $5000 =
$75<br/>
<strong>Principal paid</strong> = $100 - $75 = $25</p>
<p>The remaining balance at the end of the first month will
be the principal paid this month subtracted from the
balance at the start of the month.</p>
<p><strong>Remaining balance</strong> = $5000 - $25 =
$4975</p>
</blockquote>
<p xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">For month 2, we
repeat the same steps.</p>
<blockquote xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">
<p><strong>Minimum monthly payment</strong>
= .02 x $4975 = $99.50<br/>
<strong>Interest Paid</strong> = .18/12 x $4975 =
$74.63<br/>
<strong>Principal Paid</strong> = $99.50 - $74.63 =
$24.87<br/>
<strong>Remaining Balance</strong> = $4975 - $24.87 =
$4950.13</p>
</blockquote>
<p xmlns:o="urn:schemas-microsoft-com:office:office" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:x="urn:schemas-microsoft-com:office:excel">After 12 months, the
total amount paid is $1167.55, leaving an outstanding balance
of $4708.10. Pretty depressing!</p>
</text></problem>
<course name="Toy Course" graceperiod="1 day 5 hours 59 minutes 59 seconds" showanswer="always" rerandomize="never">
<course name="Toy Course" org="edX" course="toy" graceperiod="1 day 5 hours 59 minutes 59 seconds" slug="2012_Fall" start="2015-07-17T12:00">
<chapter name="Overview">
<video name="Welcome" youtube="0.75:izygArpw-Qo,1.0:p2Q6BrNhdh8,1.25:1EeWXzPdhSA,1.50:rABDYkeK0x8"/>
<videosequence format="Lecture Sequence" name="System Usage Sequence">
<html id="Lab2A" filename="Lab2A"/>
<video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/>
<videosequence format="Lecture Sequence" name="Toy Videos">
<html name="toylab" filename="toylab"/>
<video name="Video Resources" youtube="1.0:1bK-WdDi6Qw"/>
</videosequence>
<video name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
</chapter>
</course>
<script type="text/javascript">
$(document).ready(function() {
$("#r1_slider").slider({
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
schematic: "ctrls", component: "R1", property: "r", analysis: "dc",
})
$("#r2_slider").slider({
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
schematic: "ctrls", component: "R2", property: "r", analysis: "dc",
})
$("#r3_slider").slider({
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
schematic: "ctrls", component: "R3", property: "r", analysis: "dc",
})
$("#r4_slider").slider({
value: 1, min: 1, max: 10, step: 1, slide: schematic.component_slider,
schematic: "ctrls", component: "R4", property: "r", analysis: "dc",
})
$("#slider").slider(); });
</script>
<b>Lab 2A: Superposition Experiment</b>
<br><br><i>Note: This part of the lab is just to develop your intuition about
superposition. There are no responses that need to be checked.</i>
<br/><br/>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?
<center>
<input width="425" type="hidden" height="150" id="schematic1" parts="" analyses="" class="schematic ctrls" name="test2" value="[[&quot;w&quot;,[160,64,184,64]],[&quot;w&quot;,[160,16,184,16]],[&quot;w&quot;,[64,16,112,16]],[&quot;w&quot;,[112,64,88,64]],[&quot;w&quot;,[64,64,88,64]],[&quot;g&quot;,[88,64,0],{},[&quot;0&quot;]],[&quot;w&quot;,[112,64,160,64]],[&quot;w&quot;,[16,64,64,64]],[&quot;r&quot;,[160,16,0],{&quot;name&quot;:&quot;R4&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;0&quot;]],[&quot;r&quot;,[160,16,1],{&quot;name&quot;:&quot;R3&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;2&quot;]],[&quot;i&quot;,[112,64,6],{&quot;name&quot;:&quot;&quot;,&quot;value&quot;:&quot;6A&quot;},[&quot;0&quot;,&quot;2&quot;]],[&quot;r&quot;,[64,16,0],{&quot;name&quot;:&quot;R2&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;0&quot;]],[&quot;r&quot;,[64,16,1],{&quot;name&quot;:&quot;R1&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;3&quot;]],[&quot;v&quot;,[16,16,0],{&quot;name&quot;:&quot;&quot;,&quot;value&quot;:&quot;8V&quot;},[&quot;3&quot;,&quot;0&quot;]],[&quot;view&quot;,-24,0,2]]"/>
Figure 1. Example multi-source circuit
</center>
<br/><br/>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.
<center>
<table><tr><td>
<input style="display:inline;" width="425" type="hidden" height="150" id="schematic2" parts="" analyses="" class="schematic ctrls" name="test2" value="[[&quot;w&quot;,[160,64,184,64]],[&quot;w&quot;,[160,16,184,16]],[&quot;w&quot;,[64,16,112,16]],[&quot;w&quot;,[112,64,88,64]],[&quot;w&quot;,[64,64,88,64]],[&quot;g&quot;,[88,64,0],{},[&quot;0&quot;]],[&quot;w&quot;,[112,64,160,64]],[&quot;w&quot;,[16,64,64,64]],[&quot;r&quot;,[160,16,0],{&quot;name&quot;:&quot;R4&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;0&quot;]],[&quot;r&quot;,[160,16,1],{&quot;name&quot;:&quot;R3&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;2&quot;]],[&quot;r&quot;,[64,16,0],{&quot;name&quot;:&quot;R2&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;0&quot;]],[&quot;r&quot;,[64,16,1],{&quot;name&quot;:&quot;R1&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;3&quot;]],[&quot;v&quot;,[16,16,0],{&quot;name&quot;:&quot;&quot;,&quot;value&quot;:&quot;8V&quot;},[&quot;3&quot;,&quot;0&quot;]],[&quot;view&quot;,-24,0,2]]"/>
(a) Subcircuit for analyzing contribution of voltage source
</td><td>
<input width="425" type="hidden" height="150" id="schematic3" parts="" analyses="" class="schematic ctrls" name="test2" value="[[&quot;w&quot;,[16,16,16,64]],[&quot;w&quot;,[160,64,184,64]],[&quot;w&quot;,[160,16,184,16]],[&quot;w&quot;,[64,16,112,16]],[&quot;w&quot;,[112,64,88,64]],[&quot;w&quot;,[64,64,88,64]],[&quot;g&quot;,[88,64,0],{},[&quot;0&quot;]],[&quot;w&quot;,[112,64,160,64]],[&quot;w&quot;,[16,64,64,64]],[&quot;r&quot;,[160,16,0],{&quot;name&quot;:&quot;R4&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;0&quot;]],[&quot;r&quot;,[160,16,1],{&quot;name&quot;:&quot;R3&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;1&quot;,&quot;2&quot;]],[&quot;i&quot;,[112,64,6],{&quot;name&quot;:&quot;&quot;,&quot;value&quot;:&quot;6A&quot;},[&quot;0&quot;,&quot;2&quot;]],[&quot;r&quot;,[64,16,0],{&quot;name&quot;:&quot;R2&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;0&quot;]],[&quot;r&quot;,[64,16,1],{&quot;name&quot;:&quot;R1&quot;,&quot;r&quot;:&quot;1&quot;},[&quot;2&quot;,&quot;3&quot;]],[&quot;view&quot;,-24,0,2]]"/>
(b) Subcircuit for analyzing contribution of current source
</td></tr></table>
<br>Figure 2. Decomposition of Figure 1 into subcircuits
</center>
<br/>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:
<ul style="margin-left:2em;margin-top:1em;margin-right:2em;margin-bottom:1em;">
<i>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.)</i>
</ul>
<br>
<center>
<table><tr valign="top">
<td>
<table>
<tr valign="top">
<td>R1</td>
<td>
<div id="r1_slider" style="width:200px; height:10px; margin-left:15px"></div>
</td>
</tr>
<tr valign="top">
<td>R2</td>
<td>
<div id="r2_slider" style="width:200px; height:10px; margin-left:15px; margin-top:10px;"></div>
</td>
</tr>
<tr valign="top">
<td>R3</td>
<td>
<div id="r3_slider" style="width:200px; height:10px; margin-left:15px; margin-top:10px;"></div>
</td>
</tr>
<tr valign="top">
<td>R4</td>
<td>
<div id="r4_slider" style="width:200px; height:10px; margin-left:15px; margin-top:10px;"></div>
</td>
</tr>
</table>
</td></tr></table>
</center>
<b>Lab 2A: Superposition Experiment</b>
<p>Isn't the toy course great?</p>
......@@ -63,7 +63,13 @@ You should be familiar with the following. If you're not, go read some docs...
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.
......@@ -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`
......
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])
......@@ -119,16 +119,18 @@ 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)
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
......@@ -138,10 +140,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
......
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?
......@@ -20,12 +20,16 @@ from module_render import toc_for_course, get_module, get_section
from models import StudentModuleCache
from student.models import UserProfile
from multicourse import multicourse_settings
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
log = logging.getLogger("mitx.courseware")
......@@ -196,65 +200,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)
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):
'''
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 <problem>.
We do the jump by (1) reading course.xml to find the first
instance of <problem> 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
<sequential>).
@ensure_csrf_cookie
def jump_to(request, location):
'''
# get coursename if stored
coursename = multicourse_settings.get_coursename_from_request(request)
Show the page that contains a specific location.
# begin by getting course.xml tree
xml = content_parser.course_file(request.user, coursename)
If the location is invalid, return a 404.
# look for problem of given name
pxml = xml.xpath('//problem[@filename="%s"]' % probname)
if pxml:
pxml = pxml[0]
If the location is valid, but not present in a course, ?
# 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()
If the location is valid, but in a course the current user isn't registered for, ?
TODO -- let the index view deal with it?
'''
# Complain if the location isn't valid
try:
location = Location(location)
except InvalidLocationError:
raise Http404("Invalid location")
position = None
if parent.tag == 'sequential':
position = parent.index(pxml) + 1 # position in sequence
# 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))
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})
......
Transitional for moving to new settings scheme.
To use:
django-admin.py runserver --settings=envs.dev --pythonpath=.
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
......@@ -10,5 +12,5 @@ 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
......@@ -61,7 +61,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
......
......@@ -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'
......@@ -34,12 +38,6 @@ 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_env="dev",
......@@ -47,8 +45,12 @@ LOGGING = get_logger_config(TEST_ROOT / "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",
......
......@@ -24,11 +24,11 @@ urlpatterns = ('',
url(r'^event$', 'track.views.user_track'),
url(r'^t/(?P<template>[^/]*)$', '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<error>[^/]*)$', '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<key>[^/]*)$', 'student.views.activate_account'),
url(r'^activate/(?P<key>[^/]*)$', 'student.views.activate_account', name="activate"),
url(r'^password_reset/$', 'student.views.password_reset', name='password_reset'),
## Obsolete Django views for password resets
......@@ -97,7 +97,8 @@ if settings.PERFSTATS:
if settings.COURSEWARE_ENABLED:
urlpatterns += (
url(r'^masquerade/', include('masquerade.urls')),
url(r'^jumpto/(?P<probname>[^/]+)/$', 'courseware.views.jump_to'),
url(r'^jump_to/(?P<location>.*)$', 'courseware.views.jump_to', name="jump_to"),
url(r'^modx/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.modx_dispatch'), #reset_problem'),
url(r'^xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.xqueue_callback'),
url(r'^change_setting$', 'student.views.change_setting'),
......
......@@ -51,6 +51,7 @@ 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
[:lms, :cms, :common].each do |system|
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment