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 @@ ...@@ -3,7 +3,9 @@
### ###
from django.core.management.base import BaseCommand, CommandError 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 unnamed_modules = 0
...@@ -21,4 +23,4 @@ class Command(BaseCommand): ...@@ -21,4 +23,4 @@ class Command(BaseCommand):
course_dirs = args[1:] course_dirs = args[1:]
else: else:
course_dirs = None 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 ...@@ -12,7 +12,7 @@ from django.contrib.auth.models import User
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
import xmodule.modulestore.django import xmodule.modulestore.django
from xmodule.modulestore import Location from xmodule.modulestore import Location
from contentstore import import_from_xml from xmodule.modulestore.xml_importer import import_from_xml
import copy import copy
...@@ -74,7 +74,7 @@ class ContentStoreTestCase(TestCase): ...@@ -74,7 +74,7 @@ class ContentStoreTestCase(TestCase):
return resp return resp
def _activate_user(self, email): 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''' No error checking'''
activation_key = registration(email).activation_key activation_key = registration(email).activation_key
...@@ -102,7 +102,7 @@ class AuthTestCase(ContentStoreTestCase): ...@@ -102,7 +102,7 @@ class AuthTestCase(ContentStoreTestCase):
resp = self.client.get(url) resp = self.client.get(url)
self.assertEqual(resp.status_code, expected) self.assertEqual(resp.status_code, expected)
return resp return resp
def test_public_pages_load(self): def test_public_pages_load(self):
"""Make sure pages that don't require login load without error.""" """Make sure pages that don't require login load without error."""
pages = ( pages = (
...@@ -196,7 +196,7 @@ class EditTestCase(ContentStoreTestCase): ...@@ -196,7 +196,7 @@ class EditTestCase(ContentStoreTestCase):
xmodule.modulestore.django.modulestore().collection.drop() xmodule.modulestore.django.modulestore().collection.drop()
def check_edit_item(self, test_course_name): 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)): for descriptor in modulestore().get_items(Location(None, None, None, None, None)):
print "Checking ", descriptor.location.url() print "Checking ", descriptor.location.url()
......
...@@ -5,7 +5,8 @@ from django.conf import settings ...@@ -5,7 +5,8 @@ from django.conf import settings
from fs.osfs import OSFS from fs.osfs import OSFS
from git import Repo, PushInfo 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 xmodule.modulestore import Location
from .exceptions import GithubSyncError from .exceptions import GithubSyncError
...@@ -56,7 +57,8 @@ def import_from_github(repo_settings): ...@@ -56,7 +57,8 @@ def import_from_github(repo_settings):
git_repo = setup_repo(repo_settings) git_repo = setup_repo(repo_settings)
git_repo.head.reset('origin/%s' % repo_settings['branch'], index=True, working_tree=True) 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] return git_repo.head.commit.hexsha, module_store.courses[course_dir]
......
...@@ -57,7 +57,7 @@ class GithubSyncTestCase(TestCase): ...@@ -57,7 +57,7 @@ class GithubSyncTestCase(TestCase):
""" """
self.assertEquals('Toy Course', self.import_course.metadata['display_name']) self.assertEquals('Toy Course', self.import_course.metadata['display_name'])
self.assertIn( 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()]) [child.location for child in self.import_course.get_children()])
self.assertEquals(1, len(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 ...@@ -30,6 +30,7 @@ from django_future.csrf import ensure_csrf_cookie
from student.models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment from student.models import Registration, UserProfile, PendingNameChange, PendingEmailChange, CourseEnrollment
from util.cache import cache_if_anonymous from util.cache import cache_if_anonymous
from xmodule.course_module import CourseDescriptor from xmodule.course_module import CourseDescriptor
from xmodule.modulestore.exceptions import ItemNotFoundError
from xmodule.modulestore.django import modulestore from xmodule.modulestore.django import modulestore
from xmodule.modulestore.exceptions import ItemNotFoundError from xmodule.modulestore.exceptions import ItemNotFoundError
......
...@@ -12,8 +12,8 @@ log = logging.getLogger(__name__) ...@@ -12,8 +12,8 @@ log = logging.getLogger(__name__)
def process_includes(fn): def process_includes(fn):
""" """
Wraps a XModuleDescriptor.from_xml method, and modifies xml_data to replace 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 any immediate child <include> items with the contents of the file that they
supposed to include are supposed to include
""" """
@wraps(fn) @wraps(fn)
def from_xml(cls, xml_data, system, org=None, course=None): def from_xml(cls, xml_data, system, org=None, course=None):
...@@ -25,15 +25,21 @@ def process_includes(fn): ...@@ -25,15 +25,21 @@ def process_includes(fn):
try: try:
ifp = system.resources_fs.open(file) ifp = system.resources_fs.open(file)
except Exception: except Exception:
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True))) msg = 'Error in problem xml include: %s\n' % (
log.exception('Cannot find file %s in %s' % (file, dir)) etree.tostring(next_include, pretty_print=True))
msg += 'Cannot find file %s in %s' % (file, dir)
log.exception(msg)
system.error_handler(msg)
raise raise
try: try:
# read in and convert to XML # read in and convert to XML
incxml = etree.XML(ifp.read()) incxml = etree.XML(ifp.read())
except Exception: except Exception:
log.exception('Error in problem xml include: %s' % (etree.tostring(next_include, pretty_print=True))) msg = 'Error in problem xml include: %s\n' % (
log.exception('Cannot parse XML in %s' % (file)) etree.tostring(next_include, pretty_print=True))
msg += 'Cannot parse XML in %s' % (file)
log.exception(msg)
system.error_handler(msg)
raise raise
# insert new XML into tree in place of inlcude # insert new XML into tree in place of inlcude
parent = next_include.getparent() parent = next_include.getparent()
...@@ -50,8 +56,8 @@ class SemanticSectionDescriptor(XModuleDescriptor): ...@@ -50,8 +56,8 @@ class SemanticSectionDescriptor(XModuleDescriptor):
@process_includes @process_includes
def from_xml(cls, xml_data, system, org=None, course=None): 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) xml_object = etree.fromstring(xml_data)
...@@ -76,7 +82,6 @@ class TranslateCustomTagDescriptor(XModuleDescriptor): ...@@ -76,7 +82,6 @@ class TranslateCustomTagDescriptor(XModuleDescriptor):
xml_object = etree.fromstring(xml_data) xml_object = etree.fromstring(xml_data)
tag = xml_object.tag tag = xml_object.tag
xml_object.tag = 'customtag' xml_object.tag = 'customtag'
impl = etree.SubElement(xml_object, 'impl') xml_object.attrib['impl'] = tag
impl.text = tag
return system.process_xml(etree.tostring(xml_object)) return system.process_xml(etree.tostring(xml_object))
...@@ -67,7 +67,8 @@ class ComplexEncoder(json.JSONEncoder): ...@@ -67,7 +67,8 @@ class ComplexEncoder(json.JSONEncoder):
class CapaModule(XModule): 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' icon_class = 'problem'
...@@ -77,8 +78,10 @@ class CapaModule(XModule): ...@@ -77,8 +78,10 @@ class CapaModule(XModule):
js_module_name = "Problem" js_module_name = "Problem"
css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]} css = {'scss': [resource_string(__name__, 'css/capa/display.scss')]}
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): def __init__(self, system, location, definition, instance_state=None,
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition, instance_state,
shared_state, **kwargs)
self.attempts = 0 self.attempts = 0
self.max_attempts = None self.max_attempts = None
...@@ -133,7 +136,8 @@ class CapaModule(XModule): ...@@ -133,7 +136,8 @@ class CapaModule(XModule):
seed = None seed = None
try: 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: except Exception:
msg = 'cannot create LoncapaProblem %s' % self.location.url() msg = 'cannot create LoncapaProblem %s' % self.location.url()
log.exception(msg) log.exception(msg)
...@@ -141,15 +145,20 @@ class CapaModule(XModule): ...@@ -141,15 +145,20 @@ class CapaModule(XModule):
msg = '<p>%s</p>' % msg.replace('<', '&lt;') msg = '<p>%s</p>' % msg.replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;') msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
# create a dummy problem with error message instead of failing # 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) problem_text = ('<problem><text><font color="red" size="+2">'
self.lcp = LoncapaProblem(problem_text, self.location.html_id(), instance_state, seed=seed, system=self.system) '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: else:
raise raise
@property @property
def rerandomize(self): 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') rerandomize = self.metadata.get('rerandomize', 'always')
if rerandomize in ("", "always", "true"): if rerandomize in ("", "always", "true"):
...@@ -203,7 +212,10 @@ class CapaModule(XModule): ...@@ -203,7 +212,10 @@ class CapaModule(XModule):
except Exception, err: except Exception, err:
if self.system.DEBUG: if self.system.DEBUG:
log.exception(err) 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>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;') msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
html = msg html = msg
...@@ -215,8 +227,8 @@ class CapaModule(XModule): ...@@ -215,8 +227,8 @@ class CapaModule(XModule):
'weight': self.weight, 'weight': self.weight,
} }
# We using strings as truthy values, because the terminology of the check button # We using strings as truthy values, because the terminology of the
# is context-specific. # check button is context-specific.
check_button = "Grade" if self.max_attempts else "Check" check_button = "Grade" if self.max_attempts else "Check"
reset_button = True reset_button = True
save_button = True save_button = True
...@@ -242,7 +254,8 @@ class CapaModule(XModule): ...@@ -242,7 +254,8 @@ class CapaModule(XModule):
if not self.lcp.done: if not self.lcp.done:
reset_button = False 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": if self.max_attempts is None and self.rerandomize != "always":
save_button = False save_button = False
...@@ -517,11 +530,13 @@ class CapaModule(XModule): ...@@ -517,11 +530,13 @@ class CapaModule(XModule):
self.lcp.do_reset() self.lcp.do_reset()
if self.rerandomize == "always": 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.seed = None
self.lcp = LoncapaProblem(self.definition['data'], 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() event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info) self.system.track_function('reset_problem', event_info)
...@@ -537,6 +552,7 @@ class CapaDescriptor(RawDescriptor): ...@@ -537,6 +552,7 @@ class CapaDescriptor(RawDescriptor):
module_class = CapaModule module_class = CapaModule
# VS[compat]
# TODO (cpennington): Delete this method once all fall 2012 course are being # TODO (cpennington): Delete this method once all fall 2012 course are being
# edited in the cms # edited in the cms
@classmethod @classmethod
...@@ -545,3 +561,7 @@ class CapaDescriptor(RawDescriptor): ...@@ -545,3 +561,7 @@ class CapaDescriptor(RawDescriptor):
'problems/' + path[8:], 'problems/' + path[8:],
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__) ...@@ -10,6 +10,7 @@ log = logging.getLogger(__name__)
class CourseDescriptor(SequenceDescriptor): class CourseDescriptor(SequenceDescriptor):
module_class = SequenceModule module_class = SequenceModule
metadata_attributes = SequenceDescriptor.metadata_attributes + ('org', 'course')
def __init__(self, system, definition=None, **kwargs): def __init__(self, system, definition=None, **kwargs):
super(CourseDescriptor, self).__init__(system, definition, **kwargs) super(CourseDescriptor, self).__init__(system, definition, **kwargs)
...@@ -17,23 +18,40 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -17,23 +18,40 @@ class CourseDescriptor(SequenceDescriptor):
try: try:
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M") self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
except KeyError: except KeyError:
self.start = time.gmtime(0) # The epoch self.start = time.gmtime(0) #The epoch
log.critical("Course loaded without a start date. " + str(self.id)) log.critical("Course loaded without a start date. %s", self.id)
except ValueError, e: except ValueError as e:
self.start = time.gmtime(0) # The epoch self.start = time.gmtime(0) #The epoch
log.critical("Course loaded with a bad start date. " + str(self.id) + " '" + str(e) + "'") log.critical("Course loaded with a bad start date. %s '%s'",
self.id, e)
def has_started(self): def has_started(self):
return time.gmtime() > self.start return time.gmtime() > self.start
@classmethod @staticmethod
def id_to_location(cls, course_id): 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('/') org, course, name = course_id.split('/')
return Location('i4x', org, course, 'course', name) 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 @property
def id(self): def id(self):
return "/".join([self.location.org, self.location.course, self.location.name]) return self.location_to_id(self.location)
@property @property
def start_date_text(self): 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): ...@@ -12,8 +12,10 @@ class HtmlModule(XModule):
def get_html(self): def get_html(self):
return self.html return self.html
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): def __init__(self, system, location, definition,
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition,
instance_state, shared_state, **kwargs)
self.html = self.definition['data'] self.html = self.definition['data']
...@@ -42,3 +44,8 @@ class HtmlDescriptor(RawDescriptor): ...@@ -42,3 +44,8 @@ class HtmlDescriptor(RawDescriptor):
def file_to_xml(cls, file_object): def file_to_xml(cls, file_object):
parser = etree.HTMLParser() parser = etree.HTMLParser()
return etree.parse(file_object, parser).getroot() 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 ...@@ -2,9 +2,12 @@ from x_module import XModuleDescriptor, DescriptorSystem
class MakoDescriptorSystem(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 self.render_template = render_template
super(MakoDescriptorSystem, self).__init__(*args, **kwargs)
class MakoModuleDescriptor(XModuleDescriptor): class MakoModuleDescriptor(XModuleDescriptor):
...@@ -19,7 +22,9 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -19,7 +22,9 @@ class MakoModuleDescriptor(XModuleDescriptor):
def __init__(self, system, definition=None, **kwargs): def __init__(self, system, definition=None, **kwargs):
if getattr(system, 'render_template', None) is None: 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) super(MakoModuleDescriptor, self).__init__(system, definition, **kwargs)
def get_context(self): def get_context(self):
...@@ -29,4 +34,5 @@ class MakoModuleDescriptor(XModuleDescriptor): ...@@ -29,4 +34,5 @@ class MakoModuleDescriptor(XModuleDescriptor):
return {'module': self} return {'module': self}
def get_html(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): ...@@ -45,13 +45,28 @@ class Location(_LocationBase):
""" """
return re.sub('_+', '_', INVALID_CHARS.sub('_', value)) 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. Create a new location that is a clone of the specifed one.
location - Can be any of the following types: 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] list: should be of the form [tag, org, course, category, name, revision]
dict: should be of the form { dict: should be of the form {
'tag': tag, 'tag': tag,
'org': org, 'org': org,
...@@ -62,16 +77,19 @@ class Location(_LocationBase): ...@@ -62,16 +77,19 @@ class Location(_LocationBase):
} }
Location: another Location object 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 Components may be set to None, which may be interpreted by some contexts
wildcard selection 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 location = loc_or_tag
else: else:
location = (loc_or_tag, org, course, category, name, revision) location = (loc_or_tag, org, course, category, name, revision)
...@@ -131,9 +149,11 @@ class Location(_LocationBase): ...@@ -131,9 +149,11 @@ class Location(_LocationBase):
def html_id(self): 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): def dict(self):
""" """
...@@ -154,7 +174,8 @@ class Location(_LocationBase): ...@@ -154,7 +174,8 @@ class Location(_LocationBase):
class ModuleStore(object): 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): def get_item(self, location, depth=0):
""" """
...@@ -164,13 +185,16 @@ class ModuleStore(object): ...@@ -164,13 +185,16 @@ class ModuleStore(object):
If any segment of the location is None except revision, raises If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError 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 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 depth (int): An argument that some module stores may use to prefetch
for more efficient results later in the request. The depth is counted in the number of descendents of the queried modules for more efficient results later
calls to get_children() to cache. None indicates to cache all descendents 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 raise NotImplementedError
...@@ -182,9 +206,10 @@ class ModuleStore(object): ...@@ -182,9 +206,10 @@ class ModuleStore(object):
location: Something that can be passed to Location location: Something that can be passed to Location
depth: An argument that some module stores may use to prefetch descendents of the queried modules depth: An argument that some module stores may use to prefetch
for more efficient results later in the request. The depth is counted in the number of calls descendents of the queried modules for more efficient results later
to get_children() to cache. None indicates to cache all descendents 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 raise NotImplementedError
...@@ -229,3 +254,25 @@ class ModuleStore(object): ...@@ -229,3 +254,25 @@ class ModuleStore(object):
''' '''
raise NotImplementedError 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): ...@@ -13,3 +13,6 @@ class InsufficientSpecificationError(Exception):
class InvalidLocationError(Exception): class InvalidLocationError(Exception):
pass pass
class NoPathToItem(Exception):
pass
...@@ -6,32 +6,47 @@ from fs.osfs import OSFS ...@@ -6,32 +6,47 @@ from fs.osfs import OSFS
from itertools import repeat from itertools import repeat
from importlib import import_module from importlib import import_module
from xmodule.errorhandlers import strict_error_handler
from xmodule.x_module import XModuleDescriptor from xmodule.x_module import XModuleDescriptor
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from xmodule.course_module import CourseDescriptor
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from . import ModuleStore, Location 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 # 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, # there is only one revision for each item. Once we start versioning inside the CMS,
# that assumption will have to change # that assumption will have to change
class CachingDescriptorSystem(MakoDescriptorSystem): class CachingDescriptorSystem(MakoDescriptorSystem):
""" """
A system that has a cache of module json that it will use to load modules 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 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 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 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.modulestore = modulestore
self.module_data = module_data self.module_data = module_data
self.default_class = default_class self.default_class = default_class
...@@ -127,19 +142,22 @@ class MongoModuleStore(ModuleStore): ...@@ -127,19 +142,22 @@ class MongoModuleStore(ModuleStore):
""" """
Load an XModuleDescriptor from item, using the children stored in data_cache 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( system = CachingDescriptorSystem(
self, self,
data_cache, data_cache,
self.default_class, self.default_class,
resource_fs, resource_fs,
render_to_string strict_error_handler,
render_to_string,
) )
return system.load_item(item['location']) return system.load_item(item['location'])
def _load_items(self, items, depth=0): 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) data_cache = self._cache_children(items, depth)
...@@ -153,6 +171,14 @@ class MongoModuleStore(ModuleStore): ...@@ -153,6 +171,14 @@ class MongoModuleStore(ModuleStore):
course_filter = Location("i4x", category="course") course_filter = Location("i4x", category="course")
return self.get_items(course_filter) 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): def get_item(self, location, depth=0):
""" """
Returns an XModuleDescriptor instance for the item at location. Returns an XModuleDescriptor instance for the item at location.
...@@ -176,10 +202,7 @@ class MongoModuleStore(ModuleStore): ...@@ -176,10 +202,7 @@ class MongoModuleStore(ModuleStore):
if key != 'revision' and val is None: if key != 'revision' and val is None:
raise InsufficientSpecificationError(location) raise InsufficientSpecificationError(location)
item = self.collection.find_one( item = self._find_one(location)
location_to_query(location),
sort=[('revision', pymongo.ASCENDING)],
)
if item is None: if item is None:
raise ItemNotFoundError(location) raise ItemNotFoundError(location)
return self._load_items([item], depth)[0] return self._load_items([item], depth)[0]
...@@ -237,7 +260,7 @@ class MongoModuleStore(ModuleStore): ...@@ -237,7 +260,7 @@ class MongoModuleStore(ModuleStore):
def update_metadata(self, location, metadata): 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 metadata
location: Something that can be passed to Location location: Something that can be passed to Location
...@@ -250,3 +273,101 @@ class MongoModuleStore(ModuleStore): ...@@ -250,3 +273,101 @@ class MongoModuleStore(ModuleStore):
{'_id': Location(location).dict()}, {'_id': Location(location).dict()},
{'$set': {'metadata': metadata}} {'$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(): ...@@ -13,14 +13,51 @@ def test_string_roundtrip():
check_string_roundtrip("tag://org/course/category/name/revision") check_string_roundtrip("tag://org/course/category/name/revision")
input_dict = {
'tag': 'tag',
'course': 'course',
'category': 'category',
'name': 'name',
'org': 'org'
}
input_list = ['tag', 'org', 'course', 'category', 'name']
input_str = "tag://org/course/category/name"
input_str_rev = "tag://org/course/category/name/revision"
valid = (input_list, input_dict, input_str, input_str_rev)
invalid_dict = {
'tag': 'tag',
'course': 'course',
'category': 'category',
'name': 'name/more_name',
'org': 'org'
}
invalid_dict2 = {
'tag': 'tag',
'course': 'course',
'category': 'category',
'name': 'name ', # extra space
'org': 'org'
}
invalid = ("foo", ["foo"], ["foo", "bar"],
["foo", "bar", "baz", "blat", "foo/bar"],
"tag://org/course/category/name with spaces/revision",
invalid_dict,
invalid_dict2)
def test_is_valid():
for v in valid:
assert_equals(Location.is_valid(v), True)
for v in invalid:
assert_equals(Location.is_valid(v), False)
def test_dict(): def test_dict():
input_dict = {
'tag': 'tag',
'course': 'course',
'category': 'category',
'name': 'name',
'org': 'org'
}
assert_equals("tag://org/course/category/name", Location(input_dict).url()) assert_equals("tag://org/course/category/name", Location(input_dict).url())
assert_equals(dict(revision=None, **input_dict), Location(input_dict).dict()) assert_equals(dict(revision=None, **input_dict), Location(input_dict).dict())
...@@ -30,7 +67,6 @@ def test_dict(): ...@@ -30,7 +67,6 @@ def test_dict():
def test_list(): def test_list():
input_list = ['tag', 'org', 'course', 'category', 'name']
assert_equals("tag://org/course/category/name", Location(input_list).url()) assert_equals("tag://org/course/category/name", Location(input_list).url())
assert_equals(input_list + [None], Location(input_list).list()) assert_equals(input_list + [None], Location(input_list).list())
...@@ -65,3 +101,13 @@ def test_equality(): ...@@ -65,3 +101,13 @@ def test_equality():
Location('tag', 'org', 'course', 'category', 'name1'), Location('tag', 'org', 'course', 'category', 'name1'),
Location('tag', 'org', 'course', 'category', 'name') 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 ...@@ -3,6 +3,7 @@ from fs.osfs import OSFS
from importlib import import_module from importlib import import_module
from lxml import etree from lxml import etree
from path import path from path import path
from xmodule.errorhandlers import logging_error_handler
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from cStringIO import StringIO from cStringIO import StringIO
...@@ -12,153 +13,188 @@ import re ...@@ -12,153 +13,188 @@ import re
from . import ModuleStore, Location from . import ModuleStore, Location
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False, etree.set_default_parser(
remove_comments=True, remove_blank_text=True)) etree.XMLParser(dtd_validation=False, load_dtd=False,
remove_comments=True, remove_blank_text=True))
log = logging.getLogger('mitx.' + __name__) 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): def clean_out_mako_templating(xml_string):
xml_string = xml_string.replace('%include', 'include') xml_string = xml_string.replace('%include', 'include')
xml_string = re.sub("(?m)^\s*%.*$", '', xml_string) xml_string = re.sub("(?m)^\s*%.*$", '', xml_string)
return 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): class XMLModuleStore(ModuleStore):
""" """
An XML backed 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 Initialize an XMLModuleStore from data_dir
data_dir: path to data directory containing the course directories 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 default_class: dot-separated string defining the default descriptor
course_dirs: If specified, the list of course_dirs to load. Otherwise, load class to use if none is specified in entry_points
all course dirs
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.eager = eager
self.data_dir = path(data_dir) self.data_dir = path(data_dir)
self.modules = {} # location -> XModuleDescriptor self.modules = {} # location -> XModuleDescriptor
self.courses = {} # course_dir -> XModuleDescriptor for the course self.courses = {} # course_dir -> XModuleDescriptor for the course
self.error_handler = error_handler
if default_class is None: if default_class is None:
self.default_class = None self.default_class = None
else: else:
module_path, _, class_name = default_class.rpartition('.') 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) class_ = getattr(import_module(module_path), class_name)
self.default_class = class_ 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('XMLModuleStore: eager=%s, data_dir = %s' % (eager, self.data_dir))
#log.debug('default_class = %s' % self.default_class) #log.debug('default_class = %s' % self.default_class)
for course_dir in os.listdir(self.data_dir): # If we are specifically asked for missing courses, that should
if course_dirs is not None and course_dir not in course_dirs: # be an error. If we are asked for "all" courses, find the ones
continue # that have a course.xml
if course_dirs is None:
if not os.path.exists(self.data_dir / course_dir / "course.xml"): course_dirs = [d for d in os.listdir(self.data_dir) if
continue os.path.exists(self.data_dir / d / "course.xml")]
for course_dir in course_dirs:
try: try:
course_descriptor = self.load_course(course_dir) course_descriptor = self.load_course(course_dir)
self.courses[course_dir] = course_descriptor self.courses[course_dir] = course_descriptor
except: 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): def load_course(self, course_dir):
""" """
Load a course into this module store Load a course into this module store
course_path: Course directory name 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: with open(self.data_dir / course_dir / "course.xml") as course_file:
# TODO (cpennington): Remove this once all fall 2012 courses have been imported # VS[compat]
# into the cms from xml # 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_file = StringIO(clean_out_mako_templating(course_file.read()))
course_data = etree.parse(course_file).getroot() course_data = etree.parse(course_file).getroot()
org = course_data.get('org') org = course_data.get('org')
if org is None: if org is None:
log.error( log.error("No 'org' attribute set for course in {dir}. "
"No 'org' attribute set for course in {dir}. Using default 'edx'".format( "Using default 'edx'".format(dir=course_dir))
dir=course_dir))
org = 'edx' org = 'edx'
course = course_data.get('course') course = course_data.get('course')
if course is None: if course is None:
log.error( log.error("No 'course' attribute set for course in {dir}."
"No 'course' attribute set for course in {dir}. Using default '{default}'".format( " Using default '{default}'".format(
dir=course_dir, dir=course_dir,
default=course_dir default=course_dir
)) ))
course = course_dir course = course_dir
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): system = ImportSystem(self, org, course, course_dir,
def __init__(self, xmlstore): self.error_handler)
"""
xmlstore: the XMLModuleStore to store the loaded modules in course_descriptor = system.process_xml(etree.tostring(course_data))
""" log.debug('========> Done with course import from {0}'.format(course_dir))
self.unnamed_modules = 0
self.used_slugs = set()
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')
return course_descriptor return course_descriptor
def get_item(self, location, depth=0): def get_item(self, location, depth=0):
...@@ -169,7 +205,9 @@ class XMLModuleStore(ModuleStore): ...@@ -169,7 +205,9 @@ class XMLModuleStore(ModuleStore):
If any segment of the location is None except revision, raises If any segment of the location is None except revision, raises
xmodule.modulestore.exceptions.InsufficientSpecificationError 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 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__) ...@@ -8,7 +8,7 @@ log = logging.getLogger(__name__)
class RawDescriptor(MakoModuleDescriptor, XmlDescriptor): 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" mako_template = "widgets/raw-edit.html"
...@@ -31,8 +31,11 @@ class RawDescriptor(MakoModuleDescriptor, XmlDescriptor): ...@@ -31,8 +31,11 @@ class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
except etree.XMLSyntaxError as err: except etree.XMLSyntaxError as err:
lines = self.definition['data'].split('\n') lines = self.definition['data'].split('\n')
line, offset = err.position 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=lines[line-1][offset - 40:offset + 40], "Context: '{context}'".format(
loc=self.location context=lines[line-1][offset - 40:offset + 40],
)) loc=self.location))
log.exception(msg)
self.system.error_handler(msg)
# no workaround possible, so just re-raise
raise raise
...@@ -20,12 +20,15 @@ class_priority = ['video', 'problem'] ...@@ -20,12 +20,15 @@ class_priority = ['video', 'problem']
class SequenceModule(XModule): class SequenceModule(XModule):
''' Layout module which lays out content in a temporal sequence ''' 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')]} css = {'scss': [resource_string(__name__, 'css/sequence/display.scss')]}
js_module_name = "Sequence" js_module_name = "Sequence"
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): def __init__(self, system, location, definition, instance_state=None,
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition,
instance_state, shared_state, **kwargs)
self.position = 1 self.position = 1
if instance_state is not None: if instance_state is not None:
...@@ -92,7 +95,8 @@ class SequenceModule(XModule): ...@@ -92,7 +95,8 @@ class SequenceModule(XModule):
self.rendered = True self.rendered = True
def get_icon_class(self): 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' new_class = 'other'
for c in class_priority: for c in class_priority:
if c in child_classes: if c in child_classes:
...@@ -114,5 +118,20 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor): ...@@ -114,5 +118,20 @@ class SequenceDescriptor(MakoModuleDescriptor, XmlDescriptor):
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
xml_object = etree.Element('sequential') xml_object = etree.Element('sequential')
for child in self.get_children(): 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 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): ...@@ -21,19 +21,23 @@ class CustomTagModule(XModule):
course.xml:: course.xml::
... ...
<customtag page="234"><impl>book</impl></customtag> <customtag page="234" impl="book"/>
... ...
Renders to:: Renders to::
More information given in <a href="/book/234">the text</a> More information given in <a href="/book/234">the text</a>
""" """
def __init__(self, system, location, definition, instance_state=None, shared_state=None, **kwargs): def __init__(self, system, location, definition,
XModule.__init__(self, system, location, definition, instance_state, shared_state, **kwargs) instance_state=None, shared_state=None, **kwargs):
XModule.__init__(self, system, location, definition,
instance_state, shared_state, **kwargs)
xmltree = etree.fromstring(self.definition['data']) xmltree = etree.fromstring(self.definition['data'])
template_name = xmltree.find('impl').text template_name = xmltree.attrib['impl']
params = dict(xmltree.items()) 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) self.html = Template(template.read()).render(**params)
def get_html(self): def get_html(self):
......
...@@ -60,7 +60,7 @@ class VideoModule(XModule): ...@@ -60,7 +60,7 @@ class VideoModule(XModule):
return None return None
def get_instance_state(self): def get_instance_state(self):
log.debug(u"STATE POSITION {0}".format(self.position)) #log.debug(u"STATE POSITION {0}".format(self.position))
return json.dumps({'position': self.position}) return json.dumps({'position': self.position})
def video_list(self): def video_list(self):
......
...@@ -3,6 +3,7 @@ import pkg_resources ...@@ -3,6 +3,7 @@ import pkg_resources
import logging import logging
from xmodule.modulestore import Location from xmodule.modulestore import Location
from functools import partial from functools import partial
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
...@@ -31,23 +32,28 @@ class Plugin(object): ...@@ -31,23 +32,28 @@ class Plugin(object):
def load_class(cls, identifier, default=None): def load_class(cls, identifier, default=None):
""" """
Loads a single class instance specified by identifier. If identifier Loads a single class instance specified by identifier. If identifier
specifies more than a single class, then logs a warning and returns the first specifies more than a single class, then logs a warning and returns the
class identified. first class identified.
If default is not None, will return default if no entry_point matching identifier If default is not None, will return default if no entry_point matching
is found. Otherwise, will raise a ModuleMissingError identifier is found. Otherwise, will raise a ModuleMissingError
""" """
if cls._plugin_cache is None: if cls._plugin_cache is None:
cls._plugin_cache = {} cls._plugin_cache = {}
if identifier not in cls._plugin_cache: if identifier not in cls._plugin_cache:
identifier = identifier.lower() 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: 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, entry_point=cls.entry_point,
id=identifier, 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 len(classes) == 0:
if default is not None: if default is not None:
...@@ -79,9 +85,12 @@ class HTMLSnippet(object): ...@@ -79,9 +85,12 @@ class HTMLSnippet(object):
def get_javascript(cls): def get_javascript(cls):
""" """
Return a dictionary containing some of the following keys: Return a dictionary containing some of the following keys:
coffee: A list of coffeescript fragments that should be compiled and coffee: A list of coffeescript fragments that should be compiled and
placed on the page 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 All of these will be loaded onto the page in the CMS
""" """
...@@ -91,12 +100,15 @@ class HTMLSnippet(object): ...@@ -91,12 +100,15 @@ class HTMLSnippet(object):
def get_css(cls): def get_css(cls):
""" """
Return a dictionary containing some of the following keys: 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 css: A list of css fragments that should be applied to the html
sass: A list of sass fragments that should be applied to the html contents contents of the snippet
of the snippet
scss: A list of scss fragments that should be applied to the html contents sass: A list of sass fragments that should be applied to the html
of the snippet contents of the snippet
scss: A list of scss fragments that should be applied to the html
contents of the snippet
""" """
return cls.css return cls.css
...@@ -104,47 +116,70 @@ class HTMLSnippet(object): ...@@ -104,47 +116,70 @@ class HTMLSnippet(object):
""" """
Return the html used to display this snippet 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__)) .format(self.__class__))
class XModule(HTMLSnippet): class XModule(HTMLSnippet):
''' Implements a generic learning module. ''' 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. See the HTML module for a simple example.
''' '''
# The default implementation of get_icon_class returns the icon_class attribute of the class # The default implementation of get_icon_class returns the icon_class
# This attribute can be overridden by subclasses, and the function can also be overridden # attribute of the class
# if the icon class depends on the data in the module #
# 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' 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 Construct a new xmodule
system: A ModuleSystem allowing access to external resources system: A ModuleSystem allowing access to external resources
location: Something Location-like that identifies this xmodule 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). definition: A dictionary containing 'data' and 'children'. Both are
This defines all of the data necessary for a problem to display that is intrinsic to the problem. optional
It should not include any data that would vary between two courses using the same problem
'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.) (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 'children': is a list of Location-like values for child modules that
current student accessing the system, or None if no state has been saved this module depends on
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 instance_state: A string of serialized json that contains the state of
state is only shared per-student, not across different students this module for current student accessing the system, or None if
kwargs: Optional arguments. Subclasses should always accept kwargs and pass them no state has been saved
to the parent class constructor.
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: 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). metadata: SCAFFOLDING - This dictionary will be split into
A dictionary containing data that specifies information that is particular several different types of metadata in the future (course
to a problem in the context of a 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.system = system
self.location = Location(location) self.location = Location(location)
...@@ -158,24 +193,23 @@ class XModule(HTMLSnippet): ...@@ -158,24 +193,23 @@ class XModule(HTMLSnippet):
self._loaded_children = None self._loaded_children = None
def get_name(self): def get_name(self):
name = self.__xmltree.get('name') return self.name
if name:
return name
else:
raise "We should iterate through children and find a default name"
def get_children(self): def get_children(self):
''' '''
Return module instances for all the children of this module. Return module instances for all the children of this module.
''' '''
if self._loaded_children is None: 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 return self._loaded_children
def get_display_items(self): def get_display_items(self):
''' '''
Returns a list of descendent module instances that will display immediately Returns a list of descendent module instances that will display
inside this module immediately inside this module
''' '''
items = [] items = []
for child in self.get_children(): for child in self.get_children():
...@@ -185,8 +219,8 @@ class XModule(HTMLSnippet): ...@@ -185,8 +219,8 @@ class XModule(HTMLSnippet):
def displayable_items(self): def displayable_items(self):
''' '''
Returns list of displayable modules contained by this module. If this module Returns list of displayable modules contained by this module. If this
is visible, should return [self] module is visible, should return [self]
''' '''
return [self] return [self]
...@@ -217,16 +251,21 @@ class XModule(HTMLSnippet): ...@@ -217,16 +251,21 @@ class XModule(HTMLSnippet):
def max_score(self): def max_score(self):
''' Maximum score. Two notes: ''' 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 * This is generic; in abstract, a problem could be 3/5 points on one
should get fixed), and (b) break some analytics we plan to put in place. 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 return None
def get_progress(self): def get_progress(self):
''' Return a progress.Progress object that represents how far the student has gone ''' Return a progress.Progress object that represents how far the
in this module. Must be implemented to get correct progress tracking behavior in student has gone in this module. Must be implemented to get correct
nesting modules like sequence and vertical. progress tracking behavior in nesting modules like sequence and
vertical.
If this module has no notion of progress, return None. If this module has no notion of progress, return None.
''' '''
...@@ -240,13 +279,14 @@ class XModule(HTMLSnippet): ...@@ -240,13 +279,14 @@ class XModule(HTMLSnippet):
class XModuleDescriptor(Plugin, HTMLSnippet): class XModuleDescriptor(Plugin, HTMLSnippet):
""" """
An XModuleDescriptor is a specification for an element of a course. This could An XModuleDescriptor is a specification for an element of a course. This
be a problem, an organizational element (a group of content), or a segment of video, could be a problem, an organizational element (a group of content), or a
for example. segment of video, for example.
XModuleDescriptors are independent and agnostic to the current student state on a XModuleDescriptors are independent and agnostic to the current student state
problem. They handle the editing interface used by instructors to create a problem, on a problem. They handle the editing interface used by instructors to
and can generate XModules (which do know about student state). create a problem, and can generate XModules (which do know about student
state).
""" """
entry_point = "xmodule.v1" entry_point = "xmodule.v1"
module_class = XModule module_class = XModule
...@@ -255,46 +295,58 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -255,46 +295,58 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
inheritable_metadata = ( inheritable_metadata = (
'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize', 'graded', 'start', 'due', 'graceperiod', 'showanswer', 'rerandomize',
# This is used by the XMLModuleStore to provide for locations for static files, # TODO: This is used by the XMLModuleStore to provide for locations for
# and will need to be removed when that code is removed # static files, and will need to be removed when that code is removed
'data_dir' 'data_dir'
) )
# A list of descriptor attributes that must be equal for the descriptors to be # A list of descriptor attributes that must be equal for the descriptors to
# equal # be equal
equality_attributes = ('definition', 'metadata', 'location', 'shared_state_key', '_inherited_metadata') equality_attributes = ('definition', 'metadata', 'location',
'shared_state_key', '_inherited_metadata')
# ============================= STRUCTURAL MANIPULATION =========================== # ============================= STRUCTURAL MANIPULATION ===================
def __init__(self, def __init__(self,
system, system,
definition=None, definition=None,
**kwargs): **kwargs):
""" """
Construct a new XModuleDescriptor. The only required arguments are the Construct a new XModuleDescriptor. The only required arguments are the
system, used for interaction with external resources, and the definition, system, used for interaction with external resources, and the
which specifies all the data needed to edit and display the problem (but none definition, which specifies all the data needed to edit and display the
of the associated metadata that handles recordkeeping around the problem). 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 This allows for maximal flexibility to add to the interface while
backwards compatibility. preserving backwards compatibility.
system: An XModuleSystem for interacting with external resources system: A DescriptorSystem for interacting with external resources
definition: A dict containing `data` and `children` representing the problem definition
definition: A dict containing `data` and `children` representing the
problem definition
Current arguments passed in kwargs: 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 location: A xmodule.modulestore.Location object indicating the name
modules of this type 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: metadata: A dictionary containing the following optional keys:
goals: A list of strings of learning goals associated with this module goals: A list of strings of learning goals associated with this
display_name: The name to use for displaying this module to the user module
display_name: The name to use for displaying this module to the
user
format: The format of this module ('Homework', 'Lab', etc) format: The format of this module ('Homework', 'Lab', etc)
graded (bool): Whether this module is should be graded or not graded (bool): Whether this module is should be graded or not
start (string): The date for which this module will be available start (string): The date for which this module will be available
due (string): The due date for this module 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 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.system = system
self.metadata = kwargs.get('metadata', {}) self.metadata = kwargs.get('metadata', {})
...@@ -321,7 +373,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -321,7 +373,8 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
self.metadata[attr] = metadata[attr] self.metadata[attr] = metadata[attr]
def get_children(self): 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: if self._child_instances is None:
self._child_instances = [] self._child_instances = []
for child_loc in self.definition.get('children', []): for child_loc in self.definition.get('children', []):
...@@ -333,8 +386,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -333,8 +386,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
def xmodule_constructor(self, system): def xmodule_constructor(self, system):
""" """
Returns a constructor for an XModule. This constructor takes two arguments: Returns a constructor for an XModule. This constructor takes two
instance_state and shared_state, and returns a fully nstantiated XModule arguments: instance_state and shared_state, and returns a fully
instantiated XModule
""" """
return partial( return partial(
self.module_class, self.module_class,
...@@ -344,7 +398,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -344,7 +398,7 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
metadata=self.metadata metadata=self.metadata
) )
# ================================= JSON PARSING =================================== # ================================= JSON PARSING ===========================
@staticmethod @staticmethod
def load_from_json(json_data, system, default_class=None): def load_from_json(json_data, system, default_class=None):
""" """
...@@ -366,13 +420,14 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -366,13 +420,14 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
Creates an instance of this descriptor from the supplied json_data. Creates an instance of this descriptor from the supplied json_data.
This may be overridden by subclasses This may be overridden by subclasses
json_data: A json object specifying the definition and any optional keyword arguments for json_data: A json object specifying the definition and any optional
the XModuleDescriptor keyword arguments for the XModuleDescriptor
system: An XModuleSystem for interacting with external resources
system: A DescriptorSystem for interacting with external resources
""" """
return cls(system=system, **json_data) return cls(system=system, **json_data)
# ================================= XML PARSING ==================================== # ================================= XML PARSING ============================
@staticmethod @staticmethod
def load_from_xml(xml_data, def load_from_xml(xml_data,
system, system,
...@@ -384,16 +439,19 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -384,16 +439,19 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
on the contents of xml_data. on the contents of xml_data.
xml_data must be a string containing valid xml xml_data must be a string containing valid xml
system is an XMLParsingSystem 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( class_ = XModuleDescriptor.load_class(
etree.fromstring(xml_data).tag, etree.fromstring(xml_data).tag,
default_class default_class
) )
# leave next line in code, commented out - useful for low-level debugging # 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_)) # 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) return class_.from_xml(xml_data, system, org, course)
@classmethod @classmethod
...@@ -402,35 +460,42 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -402,35 +460,42 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
Creates an instance of this descriptor from the supplied xml_data. Creates an instance of this descriptor from the supplied xml_data.
This may be overridden by subclasses This may be overridden by subclasses
xml_data: A string of xml that will be translated into data and children for xml_data: A string of xml that will be translated into data and children
this module for this module
system is an XMLParsingSystem 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): def export_to_xml(self, resource_fs):
""" """
Returns an xml string representing this module, and all modules underneath it. Returns an xml string representing this module, and all modules
May also write required resources out to resource_fs 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), Assumes that modules have single parentage (that no module appears twice
and that it is thus safe to nest modules as xml children as appropriate. 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 The returned XML should be able to be parsed back into an identical
using the from_xml method with the same system, org, and course 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): 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 [('{}', '{}')] return [('{}', '{}')]
# =============================== BUILTIN METHODS =========================== # =============================== BUILTIN METHODS ==========================
def __eq__(self, other): def __eq__(self, other):
eq = (self.__class__ == other.__class__ and eq = (self.__class__ == other.__class__ and
all(getattr(self, attr, None) == getattr(other, attr, None) all(getattr(self, attr, None) == getattr(other, attr, None)
...@@ -438,38 +503,72 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -438,38 +503,72 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
if not eq: if not eq:
for attr in self.equality_attributes: 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 return eq
def __repr__(self): 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__, class_=self.__class__.__name__,
system=self.system, system=self.system,
definition=self.definition, definition=self.definition,
location=self.location, location=self.location,
metadata=self.metadata metadata=self.metadata
) ))
class DescriptorSystem(object): 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 load_item: Takes a Location and returns an XModuleDescriptor
resources_fs: A Filesystem object that contains all of the resources_fs: A Filesystem object that contains all of the
resources needed for the course 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.load_item = load_item
self.resources_fs = resources_fs self.resources_fs = resources_fs
self.error_handler = error_handler
class XMLParsingSystem(DescriptorSystem): 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 self.process_xml = process_xml
...@@ -487,24 +586,33 @@ class ModuleSystem(object): ...@@ -487,24 +586,33 @@ class ModuleSystem(object):
''' '''
def __init__(self, ajax_url, track_function, def __init__(self, ajax_url, track_function,
get_module, render_template, replace_urls, 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. Create a closure around the system environment.
ajax_url - the url where ajax calls to the encapsulating module go. ajax_url - the url where ajax calls to the encapsulating module go.
track_function - function of (event_type, event), intended for logging track_function - function of (event_type, event), intended for logging
or otherwise tracking the event. or otherwise tracking the event.
TODO: Not used, and has inconsistent args in different TODO: Not used, and has inconsistent args in different
files. Update or remove. files. Update or remove.
get_module - function that takes (location) and returns a corresponding get_module - function that takes (location) and returns a corresponding
module instance object. module instance object.
render_template - a function that takes (template_file, context), and returns
rendered html. render_template - a function that takes (template_file, context), and
user - The user to base the random number generator seed off of for this request returns rendered html.
filestore - A filestore ojbect. Defaults to an instance of OSFS based at
settings.DATA_DIR. 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 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.ajax_url = ajax_url
self.xqueue_callback_url = xqueue_callback_url self.xqueue_callback_url = xqueue_callback_url
...@@ -529,4 +637,3 @@ class ModuleSystem(object): ...@@ -529,4 +637,3 @@ class ModuleSystem(object):
def __str__(self): def __str__(self):
return str(self.__dict__) return str(self.__dict__)
...@@ -9,17 +9,22 @@ import os ...@@ -9,17 +9,22 @@ import os
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
# TODO (cpennington): This was implemented in an attempt to improve performance, # 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). # 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 # We should check if it hurts, and whether there's a better way of doing lazy loading
class LazyLoadingDict(MutableMapping): class LazyLoadingDict(MutableMapping):
""" """
A dictionary object that lazily loads it's contents from a provided A dictionary object that lazily loads its contents from a provided
function on reads (of members that haven't already been set) function on reads (of members that haven't already been set).
""" """
def __init__(self, loader): 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._contents = {}
self._loaded = False self._loaded = False
self._loader = loader self._loader = loader
...@@ -70,10 +75,17 @@ _AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata') ...@@ -70,10 +75,17 @@ _AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata')
class AttrMap(_AttrMapBase): class AttrMap(_AttrMapBase):
""" """
A class that specifies a metadata_key, a function to transform an xml attribute to be placed in that key, A class that specifies a metadata_key, and two functions:
and to transform that key value
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) return _AttrMapBase.__new__(_cls, metadata_key, to_metadata, from_metadata)
...@@ -88,15 +100,35 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -88,15 +100,35 @@ class XmlDescriptor(XModuleDescriptor):
# The attributes will be removed from the definition xml passed # The attributes will be removed from the definition xml passed
# to definition_from_xml, and from the xml returned by definition_to_xml # to definition_from_xml, and from the xml returned by definition_to_xml
metadata_attributes = ('format', 'graceperiod', 'showanswer', 'rerandomize', 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 # A dictionary mapping xml attribute names AttrMaps that describe how
# that return the metadata key and value # to import and export them
xml_attribute_map = { xml_attribute_map = {
'graded': AttrMap('graded', lambda val: val == 'true', lambda val: str(val).lower()), # type conversion: want True/False in python, "true"/"false" in xml
'name': AttrMap('display_name'), '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 @classmethod
def definition_from_xml(cls, xml_object, system): def definition_from_xml(cls, xml_object, system):
""" """
...@@ -105,12 +137,14 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -105,12 +137,14 @@ class XmlDescriptor(XModuleDescriptor):
xml_object: An etree Element 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 @classmethod
def clean_metadata_from_xml(cls, xml_object): 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: for attr in cls.metadata_attributes:
if xml_object.get(attr) is not None: if xml_object.get(attr) is not None:
...@@ -134,7 +168,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -134,7 +168,7 @@ class XmlDescriptor(XModuleDescriptor):
xml_data: A string of xml that will be translated into data and children for xml_data: A string of xml that will be translated into data and children for
this module 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 org and course are optional strings that will be used in the generated modules
url identifiers url identifiers
""" """
...@@ -145,9 +179,11 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -145,9 +179,11 @@ class XmlDescriptor(XModuleDescriptor):
for attr in cls.metadata_attributes: for attr in cls.metadata_attributes:
val = xml_object.get(attr) val = xml_object.get(attr)
if val is not None: 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)) attr_map = cls.xml_attribute_map.get(attr, AttrMap(attr))
metadata[attr_map.metadata_key] = attr_map.to_metadata(val) metadata[attr_map.metadata_key] = attr_map.to_metadata(val)
return metadata return metadata
def definition_loader(): def definition_loader():
...@@ -157,6 +193,7 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -157,6 +193,7 @@ class XmlDescriptor(XModuleDescriptor):
else: else:
filepath = cls._format_filepath(xml_object.tag, filename) filepath = cls._format_filepath(xml_object.tag, filename)
# VS[compat]
# TODO (cpennington): If the file doesn't exist at the right path, # 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 # give the class a chance to fix it up. The file will be written out again
# in the correct format. # in the correct format.
...@@ -169,17 +206,21 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -169,17 +206,21 @@ class XmlDescriptor(XModuleDescriptor):
filepath = candidate filepath = candidate
break break
log.debug('filepath=%s, resources_fs=%s' % (filepath, system.resources_fs))
try: try:
with system.resources_fs.open(filepath) as file: with system.resources_fs.open(filepath) as file:
definition_xml = cls.file_to_xml(file) definition_xml = cls.file_to_xml(file)
except (ResourceNotFoundError, etree.XMLSyntaxError): 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} return {'data': 'Error loading file contents at path %s' % filepath}
cls.clean_metadata_from_xml(definition_xml) cls.clean_metadata_from_xml(definition_xml)
return cls.definition_from_xml(definition_xml, system) 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( return cls(
system, system,
LazyLoadingDict(definition_loader), LazyLoadingDict(definition_loader),
...@@ -187,60 +228,90 @@ class XmlDescriptor(XModuleDescriptor): ...@@ -187,60 +228,90 @@ class XmlDescriptor(XModuleDescriptor):
org, org,
course, course,
xml_object.tag, xml_object.tag,
xml_object.get('slug')], slug],
metadata=LazyLoadingDict(metadata_loader), metadata=LazyLoadingDict(metadata_loader),
) )
@classmethod @classmethod
def _format_filepath(cls, category, name): 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): def export_to_xml(self, resource_fs):
""" """
Returns an xml string representing this module, and all modules underneath it. Returns an xml string representing this module, and all modules
May also write required resources out to resource_fs 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), Assumes that modules have single parentage (that no module appears twice
and that it is thus safe to nest modules as xml children as appropriate. 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 The returned XML should be able to be parsed back into an identical
using the from_xml method with the same system, org, and course 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) xml_object = self.definition_to_xml(resource_fs)
self.__class__.clean_metadata_from_xml(xml_object) self.__class__.clean_metadata_from_xml(xml_object)
# Put content in a separate file if it's large (has more than 5 descendent tags) # Set the tag first, so it's right if writing to a file
if len(list(xml_object.iter())) > 5: 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) filepath = self.__class__._format_filepath(self.category, self.name)
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True) resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
with resource_fs.open(filepath, 'w') as file: with resource_fs.open(filepath, 'w') as file:
file.write(etree.tostring(xml_object, pretty_print=True)) file.write(etree.tostring(xml_object, pretty_print=True))
# ...and remove all of its children here
for child in xml_object: for child in xml_object:
xml_object.remove(child) 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.set('filename', self.name)
xml_object.tag = self.category
# Add the metadata
xml_object.set('url_name', self.name)
for attr in self.metadata_attributes: for attr in self.metadata_attributes:
attr_map = self.xml_attribute_map.get(attr, AttrMap(attr)) attr_map = self.xml_attribute_map.get(attr, AttrMap(attr))
metadata_key = attr_map.metadata_key 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 continue
val = attr_map.from_metadata(self.metadata[metadata_key]) val = attr_map.from_metadata(self.metadata[metadata_key])
xml_object.set(attr, val) xml_object.set(attr, val)
# Now we just have to make it beautiful
return etree.tostring(xml_object, pretty_print=True) return etree.tostring(xml_object, pretty_print=True)
def definition_to_xml(self, resource_fs): def definition_to_xml(self, resource_fs):
""" """
Return a new etree Element object created from this modules definition. 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 @@ ...@@ -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"/> <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"> <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_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"/> <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> </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"/> <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 @@ ...@@ -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"> <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"/> <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>
<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> </sequential>
</chapter> </chapter>
</sequential> </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 @@ ...@@ -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 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"> <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"/> <problem filename="S1E3_AC_power" slug="S1E3_AC_power" name="S1E3: AC power"/>
<customtag tag="S1E3" slug="discuss_67"> <customtag tag="S1E3" slug="discuss_67" impl="discuss"/>
<impl>discuss</impl>
</customtag>
<html slug="html_68"> S1E4 has been removed. </html> <html slug="html_68"> S1E4 has been removed. </html>
</vertical> </vertical>
<vertical filename="vertical_89" slug="vertical_89" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/> <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"> <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"/> <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> <html slug="html_95">Minor correction: Six elements (five resistors)</html>
<customtag tag="S1" slug="discuss_96"> <customtag tag="S1" slug="discuss_96" impl="discuss"/>
<impl>discuss</impl>
</customtag>
</vertical> </vertical>
</sequential> </sequential>
...@@ -3,5 +3,4 @@ ...@@ -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_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"/> <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>
<vertical filename="vertical_16" slug="vertical_16" graceperiod="1 day 12 hours 59 minutes 59 seconds" showanswer="attempted" rerandomize="never"/>
</sequential> </sequential>
<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"/> <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"> <customtag tag="S1" slug="discuss_59" impl="discuss"/>
<impl>discuss</impl> <customtag page="29" slug="book_60" impl="book"/>
</customtag> <customtag lecnum="1" slug="slides_61" impl="slides"/>
<customtag page="29" slug="book_60">
<impl>book</impl>
</customtag>
<customtag lecnum="1" slug="slides_61">
<impl>slides</impl>
</customtag>
</sequential> </sequential>
...@@ -3,13 +3,7 @@ ...@@ -3,13 +3,7 @@
<h1> </h1> <h1> </h1>
</html> </html>
<video youtube="1.50:vl9xrfxcr38,1.25:qxNX4REGqx4,1.0:BGU1poJDgOY,0.75:8rK9vnpystQ" slug="S1V14_Summary" name="S1V14: Summary"/> <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"> <customtag tag="S1" slug="discuss_91" impl="discuss"/>
<impl>discuss</impl> <customtag page="70" slug="book_92" impl="book"/>
</customtag> <customtag lecnum="1" slug="slides_93" impl="slides"/>
<customtag page="70" slug="book_92">
<impl>book</impl>
</customtag>
<customtag lecnum="1" slug="slides_93">
<impl>slides</impl>
</customtag>
</sequential> </sequential>
<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"/> <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"> <customtag tag="S2" slug="discuss_95" impl="discuss"/>
<impl>discuss</impl> <customtag page="54" slug="book_96" impl="book"/>
</customtag> <customtag lecnum="2" slug="slides_97" impl="slides"/>
<customtag page="54" slug="book_96">
<impl>book</impl>
</customtag>
<customtag lecnum="2" slug="slides_97">
<impl>slides</impl>
</customtag>
</sequential> </sequential>
<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"/> <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"> <customtag tag="S2" slug="discuss_99" impl="discuss"/>
<impl>discuss</impl> <customtag page="56" slug="book_100" impl="book"/>
</customtag> <customtag lecnum="2" slug="slides_101" impl="slides"/>
<customtag page="56" slug="book_100">
<impl>book</impl>
</customtag>
<customtag lecnum="2" slug="slides_101">
<impl>slides</impl>
</customtag>
</sequential> </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"> <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="Toy Videos">
<videosequence format="Lecture Sequence" name="System Usage Sequence"> <html name="toylab" filename="toylab"/>
<html id="Lab2A" filename="Lab2A"/> <video name="Video Resources" youtube="1.0:1bK-WdDi6Qw"/>
<video name="S0V1: Video Resources" youtube="0.75:EuzkdzfR0i8,1.0:1bK-WdDi6Qw,1.25:0v1VzoDVUTM,1.50:Bxk_-ZJb240"/>
</videosequence> </videosequence>
<video name="Welcome" youtube="1.0:p2Q6BrNhdh8"/>
</chapter> </chapter>
</course> </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>
...@@ -13,17 +13,17 @@ You should be familiar with the following. If you're not, go read some docs... ...@@ -13,17 +13,17 @@ You should be familiar with the following. If you're not, go read some docs...
- css - css
- git - git
- mako templates -- we use these instead of django templates, because they support embedding real python. - mako templates -- we use these instead of django templates, because they support embedding real python.
## Other relevant terms ## Other relevant terms
- CAPA -- lon-capa.org -- content management system that has defined a standard for online learning and assessment materials. Many of our materials follow this standard. - CAPA -- lon-capa.org -- content management system that has defined a standard for online learning and assessment materials. Many of our materials follow this standard.
- TODO: add more details / link to relevant docs. lon-capa.org is not immediately intuitive. - TODO: add more details / link to relevant docs. lon-capa.org is not immediately intuitive.
- lcp = loncapa problem - lcp = loncapa problem
## Parts of the system ## Parts of the system
- LMS -- Learning Management System. The student-facing parts of the system. Handles student accounts, displaying videos, tutorials, exercies, problems, etc. - LMS -- Learning Management System. The student-facing parts of the system. Handles student accounts, displaying videos, tutorials, exercies, problems, etc.
- CMS -- Course Management System. The instructor-facing parts of the system. Allows instructors to see and modify their course, add lectures, problems, reorder things, etc. - CMS -- Course Management System. The instructor-facing parts of the system. Allows instructors to see and modify their course, add lectures, problems, reorder things, etc.
...@@ -42,7 +42,7 @@ You should be familiar with the following. If you're not, go read some docs... ...@@ -42,7 +42,7 @@ You should be familiar with the following. If you're not, go read some docs...
## High Level Entities in the code ## High Level Entities in the code
### Common libraries ### Common libraries
- xmodule: generic learning modules. *x* can be sequence, video, template, html, - xmodule: generic learning modules. *x* can be sequence, video, template, html,
vertical, capa, etc. These are the things that one puts inside sections vertical, capa, etc. These are the things that one puts inside sections
...@@ -51,7 +51,7 @@ You should be familiar with the following. If you're not, go read some docs... ...@@ -51,7 +51,7 @@ You should be familiar with the following. If you're not, go read some docs...
- XModuleDescriptor: This defines the problem and all data and UI needed to edit - XModuleDescriptor: This defines the problem and all data and UI needed to edit
that problem. It is unaware of any student data, but can be used to retrieve that problem. It is unaware of any student data, but can be used to retrieve
an XModule, which is aware of that student state. an XModule, which is aware of that student state.
- XModule: The XModule is a problem instance that is particular to a student. It knows - XModule: The XModule is a problem instance that is particular to a student. It knows
how to render itself to html to display the problem, how to score itself, how to render itself to html to display the problem, how to score itself,
and how to handle ajax calls from the front end. and how to handle ajax calls from the front end.
...@@ -59,19 +59,25 @@ You should be familiar with the following. If you're not, go read some docs... ...@@ -59,19 +59,25 @@ You should be familiar with the following. If you're not, go read some docs...
- Both XModule and XModuleDescriptor take system context parameters. These are named - Both XModule and XModuleDescriptor take system context parameters. These are named
ModuleSystem and DescriptorSystem respectively. These help isolate the XModules ModuleSystem and DescriptorSystem respectively. These help isolate the XModules
from any interactions with external resources that they require. from any interactions with external resources that they require.
For instance, the DescriptorSystem has a function to load an XModuleDescriptor For instance, the DescriptorSystem has a function to load an XModuleDescriptor
from a Location object, and the ModuleSystem knows how to render things, from a Location object, and the ModuleSystem knows how to render things,
track events, and complain about 404s 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` - in `common/lib/xmodule`
- capa modules -- defines `LoncapaProblem` and many related things. - capa modules -- defines `LoncapaProblem` and many related things.
- in `common/lib/capa` - in `common/lib/capa`
### LMS ### LMS
The LMS is a django site, with root in `lms/`. It runs in many different environments--the settings files are in `lms/envs`. The LMS is a django site, with root in `lms/`. It runs in many different environments--the settings files are in `lms/envs`.
- We use the Django Auth system, including the is_staff and is_superuser flags. User profiles and related code lives in `lms/djangoapps/student/`. There is support for groups of students (e.g. 'want emails about future courses', 'have unenrolled', etc) in `lms/djangoapps/student/models.py`. - We use the Django Auth system, including the is_staff and is_superuser flags. User profiles and related code lives in `lms/djangoapps/student/`. There is support for groups of students (e.g. 'want emails about future courses', 'have unenrolled', etc) in `lms/djangoapps/student/models.py`.
...@@ -79,19 +85,19 @@ The LMS is a django site, with root in `lms/`. It runs in many different enviro ...@@ -79,19 +85,19 @@ The LMS is a django site, with root in `lms/`. It runs in many different enviro
- `lms/djangoapps/courseware/models.py` - `lms/djangoapps/courseware/models.py`
- Core rendering path: - Core rendering path:
- `lms/urls.py` points to `courseware.views.index`, which gets module info from the course xml file, pulls list of `StudentModule` objects for this user (to avoid multiple db hits). - `lms/urls.py` points to `courseware.views.index`, which gets module info from the course xml file, pulls list of `StudentModule` objects for this user (to avoid multiple db hits).
- Calls `render_accordion` to render the "accordion"--the display of the course structure. - Calls `render_accordion` to render the "accordion"--the display of the course structure.
- To render the current module, calls `module_render.py:render_x_module()`, which gets the `StudentModule` instance, and passes the `StudentModule` state and other system context to the module constructor the get an instance of the appropriate module class for this user. - To render the current module, calls `module_render.py:render_x_module()`, which gets the `StudentModule` instance, and passes the `StudentModule` state and other system context to the module constructor the get an instance of the appropriate module class for this user.
- calls the module's `.get_html()` method. If the module has nested submodules, render_x_module() will be called again for each. - calls the module's `.get_html()` method. If the module has nested submodules, render_x_module() will be called again for each.
- ajax calls go to `module_render.py:modx_dispatch()`, which passes it to the module's `handle_ajax()` function, and then updates the grade and state if they changed. - ajax calls go to `module_render.py:modx_dispatch()`, which passes it to the module's `handle_ajax()` function, and then updates the grade and state if they changed.
- [This diagram](https://github.com/MITx/mitx/wiki/MITx-Architecture) visually shows how the clients communicate with problems + modules. - [This diagram](https://github.com/MITx/mitx/wiki/MITx-Architecture) visually shows how the clients communicate with problems + modules.
- See `lms/urls.py` for the wirings of urls to views. - See `lms/urls.py` for the wirings of urls to views.
- Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`. - Tracking: there is support for basic tracking of client-side events in `lms/djangoapps/track`.
...@@ -110,7 +116,7 @@ environments, defined in `cms/envs`. ...@@ -110,7 +116,7 @@ environments, defined in `cms/envs`.
- _mako_ -- we use this for templates, and have wrapper called mitxmako that makes mako look like the django templating calls. - _mako_ -- we use this for templates, and have wrapper called mitxmako that makes mako look like the django templating calls.
We use a fork of django-pipeline to make sure that the js and css always reflect the latest `*.coffee` and `*.sass` files (We're hoping to get our changes merged in the official version soon). This works differently in development and production. Test uses the production settings. We use a fork of django-pipeline to make sure that the js and css always reflect the latest `*.coffee` and `*.sass` files (We're hoping to get our changes merged in the official version soon). This works differently in development and production. Test uses the production settings.
In production, the django `collectstatic` command recompiles everything and puts all the generated static files in a static/ dir. A starting point in the code is `django-pipeline/pipeline/packager.py:pack`. In production, the django `collectstatic` command recompiles everything and puts all the generated static files in a static/ dir. A starting point in the code is `django-pipeline/pipeline/packager.py:pack`.
...@@ -127,8 +133,6 @@ See `testing.md`. ...@@ -127,8 +133,6 @@ See `testing.md`.
## TODO: ## TODO:
- update lms/envs/README.txt
- describe our production environment - describe our production environment
- describe the front-end architecture, tools, etc. Starting point: `lms/static` - 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): ...@@ -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, instance_module is a StudentModule specific to this module for this student,
or None if this is an anonymous user or None if this is an anonymous user
shared_module is a StudentModule specific to all modules with the same 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 share state
''' '''
descriptor = modulestore().get_item(location) 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) shared_state_key = getattr(descriptor, 'shared_state_key', None)
if shared_state_key is not 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: else:
shared_module = None shared_module = None
...@@ -138,10 +140,12 @@ def get_module(user, request, location, student_module_cache, position=None): ...@@ -138,10 +140,12 @@ def get_module(user, request, location, student_module_cache, position=None):
# TODO (vshnayder): fix hardcoded urls (use reverse) # TODO (vshnayder): fix hardcoded urls (use reverse)
# Setup system context for module instance # Setup system context for module instance
ajax_url = settings.MITX_ROOT_URL + '/modx/' + descriptor.location.url() + '/' 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): 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 return module
# TODO (cpennington): When modules are shared between courses, the static # 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 ...@@ -20,12 +20,16 @@ from module_render import toc_for_course, get_module, get_section
from models import StudentModuleCache from models import StudentModuleCache
from student.models import UserProfile from student.models import UserProfile
from multicourse import multicourse_settings 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 util.cache import cache, cache_if_anonymous
from student.models import UserTestGroup, CourseEnrollment from student.models import UserTestGroup, CourseEnrollment
from courseware import grades from courseware import grades
from courseware.courses import check_course from courseware.courses import check_course
from xmodule.modulestore.django import modulestore
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
...@@ -196,65 +200,59 @@ def index(request, course_id, chapter=None, section=None, ...@@ -196,65 +200,59 @@ def index(request, course_id, chapter=None, section=None,
if look_for_module: if look_for_module:
# TODO (cpennington): Pass the right course in here # TODO (cpennington): Pass the right course in here
section = get_section(course, chapter, section) section_descriptor = get_section(course, chapter, section)
student_module_cache = StudentModuleCache(request.user, section) if section_descriptor is not None:
module, _, _, _ = get_module(request.user, request, section.location, student_module_cache) student_module_cache = StudentModuleCache(request.user,
context['content'] = module.get_html() 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) result = render_to_response('courseware.html', context)
return result return result
@ensure_csrf_cookie
def jump_to(request, probname=None): def jump_to(request, location):
'''
Jump to viewing a specific problem. The problem is specified by a
problem name - currently the filename (minus .xml) of the problem.
Maybe this should change to a more generic tag, eg "name" given as
an attribute in <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>).
''' '''
# get coursename if stored Show the page that contains a specific location.
coursename = multicourse_settings.get_coursename_from_request(request)
# begin by getting course.xml tree If the location is invalid, return a 404.
xml = content_parser.course_file(request.user, coursename)
# look for problem of given name If the location is valid, but not present in a course, ?
pxml = xml.xpath('//problem[@filename="%s"]' % probname)
if pxml:
pxml = pxml[0]
# get the parent element If the location is valid, but in a course the current user isn't registered for, ?
parent = pxml.getparent() TODO -- let the index view deal with it?
'''
# figure out chapter and section names # Complain if the location isn't valid
chapter = None try:
section = None location = Location(location)
branch = parent except InvalidLocationError:
for k in range(4): # max depth of recursion raise Http404("Invalid location")
if branch.tag == 'section':
section = branch.get('name')
if branch.tag == 'chapter':
chapter = branch.get('name')
branch = branch.getparent()
position = None # Complain if there's not data for this location
if parent.tag == 'sequential': try:
position = parent.index(pxml) + 1 # position in sequence (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 @ensure_csrf_cookie
def course_info(request, course_id): 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) course = check_course(course_id)
return render_to_response('info.html', {'course': course}) return render_to_response('info.html', {'course': course})
......
Transitional for moving to new settings scheme. Transitional for moving to new settings scheme.
To use: 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 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 of what you send it for an explicit --settings flag. It still works, but might
have odd side effects. Using django-admin.py avoids that problem. have odd side effects. Using django-admin.py avoids that problem.
django-admin.py is installed by default when you install Django. django-admin.py is installed by default when you install Django.
To use with gunicorn_django in debug mode: To use with gunicorn_django in debug mode:
gunicorn_django envs/dev.py gunicorn_django lms/envs/dev.py
...@@ -6,16 +6,16 @@ MITX_FEATURES[...]. Modules that extend this one can change the feature ...@@ -6,16 +6,16 @@ MITX_FEATURES[...]. Modules that extend this one can change the feature
configuration in an environment specific config file and re-calculate those configuration in an environment specific config file and re-calculate those
values. values.
We should make a method that calls all these config methods so that you just We should make a method that calls all these config methods so that you just
make one call at the end of your site-specific dev file to reset all the make one call at the end of your site-specific dev file to reset all the
dependent variables (like INSTALLED_APPS) for you. dependent variables (like INSTALLED_APPS) for you.
Longer TODO: Longer TODO:
1. Right now our treatment of static content in general and in particular 1. Right now our treatment of static content in general and in particular
course-specific static content is haphazard. course-specific static content is haphazard.
2. We should have a more disciplined approach to feature flagging, even if it 2. We should have a more disciplined approach to feature flagging, even if it
just means that we stick them in a dict called MITX_FEATURES. just means that we stick them in a dict called MITX_FEATURES.
3. We need to handle configuration for multiple courses. This could be as 3. We need to handle configuration for multiple courses. This could be as
multiple sites, but we do need a way to map their data assets. multiple sites, but we do need a way to map their data assets.
""" """
import sys import sys
...@@ -42,7 +42,7 @@ MITX_FEATURES = { ...@@ -42,7 +42,7 @@ MITX_FEATURES = {
'SAMPLE' : False, 'SAMPLE' : False,
'USE_DJANGO_PIPELINE' : True, 'USE_DJANGO_PIPELINE' : True,
'DISPLAY_HISTOGRAMS_TO_STAFF' : True, 'DISPLAY_HISTOGRAMS_TO_STAFF' : True,
'REROUTE_ACTIVATION_EMAIL' : False, # nonempty string = address for all activation emails 'REROUTE_ACTIVATION_EMAIL' : False, # nonempty string = address for all activation emails
'DEBUG_LEVEL' : 0, # 0 = lowest level, least verbose, 255 = max level, most verbose 'DEBUG_LEVEL' : 0, # 0 = lowest level, least verbose, 255 = max level, most verbose
## DO NOT SET TO True IN THIS FILE ## DO NOT SET TO True IN THIS FILE
...@@ -61,7 +61,7 @@ PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/lms ...@@ -61,7 +61,7 @@ PROJECT_ROOT = path(__file__).abspath().dirname().dirname() # /mitx/lms
REPO_ROOT = PROJECT_ROOT.dirname() REPO_ROOT = PROJECT_ROOT.dirname()
COMMON_ROOT = REPO_ROOT / "common" COMMON_ROOT = REPO_ROOT / "common"
ENV_ROOT = REPO_ROOT.dirname() # virtualenv dir /mitx is in 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" COURSES_ROOT = ENV_ROOT / "data"
# FIXME: To support multiple courses, we should walk the courses dir at startup # FIXME: To support multiple courses, we should walk the courses dir at startup
...@@ -85,7 +85,7 @@ MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates', ...@@ -85,7 +85,7 @@ MAKO_TEMPLATES['main'] = [PROJECT_ROOT / 'templates',
COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates', COMMON_ROOT / 'lib' / 'capa' / 'capa' / 'templates',
COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates'] COMMON_ROOT / 'djangoapps' / 'pipeline_mako' / 'templates']
# This is where Django Template lookup is defined. There are a few of these # This is where Django Template lookup is defined. There are a few of these
# still left lying around. # still left lying around.
TEMPLATE_DIRS = ( TEMPLATE_DIRS = (
PROJECT_ROOT / "templates", PROJECT_ROOT / "templates",
...@@ -103,8 +103,8 @@ TEMPLATE_CONTEXT_PROCESSORS = ( ...@@ -103,8 +103,8 @@ TEMPLATE_CONTEXT_PROCESSORS = (
) )
# FIXME: # FIXME:
# We should have separate S3 staged URLs in case we need to make changes to # We should have separate S3 staged URLs in case we need to make changes to
# these assets and test them. # these assets and test them.
LIB_URL = '/static/js/' LIB_URL = '/static/js/'
...@@ -120,7 +120,7 @@ STATIC_GRAB = False ...@@ -120,7 +120,7 @@ STATIC_GRAB = False
DEV_CONTENT = True DEV_CONTENT = True
# FIXME: Should we be doing this truncation? # FIXME: Should we be doing this truncation?
TRACK_MAX_EVENT = 10000 TRACK_MAX_EVENT = 10000
DEBUG_TRACK_LOG = False DEBUG_TRACK_LOG = False
MITX_ROOT_URL = '' MITX_ROOT_URL = ''
...@@ -129,7 +129,7 @@ COURSE_NAME = "6.002_Spring_2012" ...@@ -129,7 +129,7 @@ COURSE_NAME = "6.002_Spring_2012"
COURSE_NUMBER = "6.002x" COURSE_NUMBER = "6.002x"
COURSE_TITLE = "Circuits and Electronics" COURSE_TITLE = "Circuits and Electronics"
### Dark code. Should be enabled in local settings for devel. ### Dark code. Should be enabled in local settings for devel.
ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome) ENABLE_MULTICOURSE = False # set to False to disable multicourse display (see lib.util.views.mitxhome)
QUICKEDIT = False QUICKEDIT = False
...@@ -211,9 +211,9 @@ USE_L10N = True ...@@ -211,9 +211,9 @@ USE_L10N = True
MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage' MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
#################################### AWS ####################################### #################################### AWS #######################################
# S3BotoStorage insists on a timeout for uploaded assets. We should make it # S3BotoStorage insists on a timeout for uploaded assets. We should make it
# permanent instead, but rather than trying to figure out exactly where that # permanent instead, but rather than trying to figure out exactly where that
# setting is, I'm just bumping the expiration time to something absurd (100 # setting is, I'm just bumping the expiration time to something absurd (100
# years). This is only used if DEFAULT_FILE_STORAGE is overriden to use S3 # years). This is only used if DEFAULT_FILE_STORAGE is overriden to use S3
# in the global settings.py # in the global settings.py
AWS_QUERYSTRING_EXPIRE = 10 * 365 * 24 * 60 * 60 # 10 years AWS_QUERYSTRING_EXPIRE = 10 * 365 * 24 * 60 * 60 # 10 years
...@@ -279,7 +279,7 @@ MIDDLEWARE_CLASSES = ( ...@@ -279,7 +279,7 @@ MIDDLEWARE_CLASSES = (
# Instead of AuthenticationMiddleware, we use a cached backed version # Instead of AuthenticationMiddleware, we use a cached backed version
#'django.contrib.auth.middleware.AuthenticationMiddleware', #'django.contrib.auth.middleware.AuthenticationMiddleware',
'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware', 'cache_toolbox.middleware.CacheBackedAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'track.middleware.TrackMiddleware', 'track.middleware.TrackMiddleware',
'mitxmako.middleware.MakoMiddleware', 'mitxmako.middleware.MakoMiddleware',
...@@ -502,7 +502,7 @@ INSTALLED_APPS = ( ...@@ -502,7 +502,7 @@ INSTALLED_APPS = (
# For testing # For testing
'django_jasmine', 'django_jasmine',
# For Askbot # For Askbot
'django.contrib.sitemaps', 'django.contrib.sitemaps',
'django.contrib.admin', 'django.contrib.admin',
'django_countries', 'django_countries',
......
...@@ -12,17 +12,21 @@ from .logsettings import get_logger_config ...@@ -12,17 +12,21 @@ from .logsettings import get_logger_config
import os import os
from path import path from path import path
INSTALLED_APPS = [ # can't test start dates with this True, but on the other hand,
app # can test everything else :)
for app MITX_FEATURES['DISABLE_START_DATES'] = True
in INSTALLED_APPS
if not app.startswith('askbot') # 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 # Nose Test Runner
INSTALLED_APPS += ['django_nose'] INSTALLED_APPS += ('django_nose',)
NOSE_ARGS = ['--cover-erase', '--with-xunit', '--with-xcoverage', '--cover-html', 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'): for app in os.listdir(PROJECT_ROOT / 'djangoapps'):
NOSE_ARGS += ['--cover-package', app] NOSE_ARGS += ['--cover-package', app]
TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
...@@ -30,25 +34,23 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner' ...@@ -30,25 +34,23 @@ TEST_RUNNER = 'django_nose.NoseTestSuiteRunner'
# Local Directories # Local Directories
TEST_ROOT = path("test_root") TEST_ROOT = path("test_root")
# Want static files in the same dir for running on jenkins. # Want static files in the same dir for running on jenkins.
STATIC_ROOT = TEST_ROOT / "staticfiles" STATIC_ROOT = TEST_ROOT / "staticfiles"
COURSES_ROOT = TEST_ROOT / "data" COURSES_ROOT = TEST_ROOT / "data"
DATA_DIR = COURSES_ROOT DATA_DIR = COURSES_ROOT
MAKO_TEMPLATES['course'] = [DATA_DIR]
MAKO_TEMPLATES['sections'] = [DATA_DIR / 'sections'] LOGGING = get_logger_config(TEST_ROOT / "log",
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", logging_env="dev",
tracking_filename="tracking.log", tracking_filename="tracking.log",
debug=True) debug=True)
COMMON_TEST_DATA_ROOT = COMMON_ROOT / "test" / "data" 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 = [ STATICFILES_DIRS = [
COMMON_ROOT / "static", COMMON_ROOT / "static",
PROJECT_ROOT / "static", PROJECT_ROOT / "static",
...@@ -67,7 +69,7 @@ DATABASES = { ...@@ -67,7 +69,7 @@ DATABASES = {
} }
CACHES = { CACHES = {
# This is the cache used for most things. Askbot will not work without a # This is the cache used for most things. Askbot will not work without a
# functioning cache -- it relies on caching to load its settings in places. # functioning cache -- it relies on caching to load its settings in places.
# In staging/prod envs, the sessions also live here. # In staging/prod envs, the sessions also live here.
'default': { 'default': {
......
...@@ -13,23 +13,23 @@ if settings.DEBUG: ...@@ -13,23 +13,23 @@ if settings.DEBUG:
urlpatterns = ('', urlpatterns = ('',
url(r'^$', 'student.views.index', name="root"), # Main marketing page, or redirect to courseware url(r'^$', 'student.views.index', name="root"), # Main marketing page, or redirect to courseware
url(r'^dashboard$', 'student.views.dashboard', name="dashboard"), url(r'^dashboard$', 'student.views.dashboard', name="dashboard"),
url(r'^change_email$', 'student.views.change_email_request'), url(r'^change_email$', 'student.views.change_email_request'),
url(r'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'), url(r'^email_confirm/(?P<key>[^/]*)$', 'student.views.confirm_email_change'),
url(r'^change_name$', 'student.views.change_name_request'), url(r'^change_name$', 'student.views.change_name_request'),
url(r'^accept_name_change$', 'student.views.accept_name_change'), url(r'^accept_name_change$', 'student.views.accept_name_change'),
url(r'^reject_name_change$', 'student.views.reject_name_change'), url(r'^reject_name_change$', 'student.views.reject_name_change'),
url(r'^pending_name_changes$', 'student.views.pending_name_changes'), url(r'^pending_name_changes$', 'student.views.pending_name_changes'),
url(r'^event$', 'track.views.user_track'), 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'^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'^login/(?P<error>[^/]*)$', 'student.views.login_user'),
url(r'^logout$', 'student.views.logout_user', name='logout'), url(r'^logout$', 'student.views.logout_user', name='logout'),
url(r'^create_account$', 'student.views.create_account'), 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'), url(r'^password_reset/$', 'student.views.password_reset', name='password_reset'),
## Obsolete Django views for password resets ## Obsolete Django views for password resets
## TODO: Replace with Mako-ized views ## TODO: Replace with Mako-ized views
...@@ -44,48 +44,48 @@ urlpatterns = ('', ...@@ -44,48 +44,48 @@ urlpatterns = ('',
name='auth_password_reset_complete'), name='auth_password_reset_complete'),
url(r'^password_reset_done/$', django.contrib.auth.views.password_reset_done, url(r'^password_reset_done/$', django.contrib.auth.views.password_reset_done,
name='auth_password_reset_done'), name='auth_password_reset_done'),
url(r'^heartbeat$', include('heartbeat.urls')), url(r'^heartbeat$', include('heartbeat.urls')),
url(r'^university_profile/(?P<org_id>[^/]+)$', 'courseware.views.university_profile', name="university_profile"), url(r'^university_profile/(?P<org_id>[^/]+)$', 'courseware.views.university_profile', name="university_profile"),
#Semi-static views (these need to be rendered and have the login bar, but don't change) #Semi-static views (these need to be rendered and have the login bar, but don't change)
url(r'^404$', 'static_template_view.views.render', url(r'^404$', 'static_template_view.views.render',
{'template': '404.html'}, name="404"), {'template': '404.html'}, name="404"),
url(r'^about$', 'static_template_view.views.render', url(r'^about$', 'static_template_view.views.render',
{'template': 'about.html'}, name="about_edx"), {'template': 'about.html'}, name="about_edx"),
url(r'^jobs$', 'static_template_view.views.render', url(r'^jobs$', 'static_template_view.views.render',
{'template': 'jobs.html'}, name="jobs"), {'template': 'jobs.html'}, name="jobs"),
url(r'^contact$', 'static_template_view.views.render', url(r'^contact$', 'static_template_view.views.render',
{'template': 'contact.html'}, name="contact"), {'template': 'contact.html'}, name="contact"),
url(r'^press$', 'student.views.press', name="press"), url(r'^press$', 'student.views.press', name="press"),
url(r'^faq$', 'static_template_view.views.render', url(r'^faq$', 'static_template_view.views.render',
{'template': 'faq.html'}, name="faq_edx"), {'template': 'faq.html'}, name="faq_edx"),
url(r'^help$', 'static_template_view.views.render', url(r'^help$', 'static_template_view.views.render',
{'template': 'help.html'}, name="help_edx"), {'template': 'help.html'}, name="help_edx"),
url(r'^tos$', 'static_template_view.views.render', url(r'^tos$', 'static_template_view.views.render',
{'template': 'tos.html'}, name="tos"), {'template': 'tos.html'}, name="tos"),
url(r'^privacy$', 'static_template_view.views.render', url(r'^privacy$', 'static_template_view.views.render',
{'template': 'privacy.html'}, name="privacy_edx"), {'template': 'privacy.html'}, name="privacy_edx"),
# TODO: (bridger) The copyright has been removed until it is updated for edX # TODO: (bridger) The copyright has been removed until it is updated for edX
# url(r'^copyright$', 'static_template_view.views.render', # url(r'^copyright$', 'static_template_view.views.render',
# {'template': 'copyright.html'}, name="copyright"), # {'template': 'copyright.html'}, name="copyright"),
url(r'^honor$', 'static_template_view.views.render', url(r'^honor$', 'static_template_view.views.render',
{'template': 'honor.html'}, name="honor"), {'template': 'honor.html'}, name="honor"),
#Press releases #Press releases
url(r'^press/mit-and-harvard-announce-edx$', 'static_template_view.views.render', url(r'^press/mit-and-harvard-announce-edx$', 'static_template_view.views.render',
{'template': 'press_releases/MIT_and_Harvard_announce_edX.html'}, name="press/mit-and-harvard-announce-edx"), {'template': 'press_releases/MIT_and_Harvard_announce_edX.html'}, name="press/mit-and-harvard-announce-edx"),
url(r'^press/uc-berkeley-joins-edx$', 'static_template_view.views.render', url(r'^press/uc-berkeley-joins-edx$', 'static_template_view.views.render',
{'template': 'press_releases/UC_Berkeley_joins_edX.html'}, name="press/uc-berkeley-joins-edx"), {'template': 'press_releases/UC_Berkeley_joins_edX.html'}, name="press/uc-berkeley-joins-edx"),
# Should this always update to point to the latest press release? # Should this always update to point to the latest press release?
(r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}), (r'^pressrelease$', 'django.views.generic.simple.redirect_to', {'url': '/press/uc-berkeley-joins-edx'}),
(r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}), (r'^favicon\.ico$', 'django.views.generic.simple.redirect_to', {'url': '/static/images/favicon.ico'}),
# TODO: These urls no longer work. They need to be updated before they are re-enabled # TODO: These urls no longer work. They need to be updated before they are re-enabled
# url(r'^send_feedback$', 'util.views.send_feedback'), # url(r'^send_feedback$', 'util.views.send_feedback'),
# url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'), # url(r'^reactivate/(?P<key>[^/]*)$', 'student.views.reactivation_email'),
...@@ -97,45 +97,46 @@ if settings.PERFSTATS: ...@@ -97,45 +97,46 @@ if settings.PERFSTATS:
if settings.COURSEWARE_ENABLED: if settings.COURSEWARE_ENABLED:
urlpatterns += ( urlpatterns += (
url(r'^masquerade/', include('masquerade.urls')), 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'^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'^xqueue/(?P<userid>[^/]*)/(?P<id>.*?)/(?P<dispatch>[^/]*)$', 'courseware.module_render.xqueue_callback'),
url(r'^change_setting$', 'student.views.change_setting'), url(r'^change_setting$', 'student.views.change_setting'),
# TODO: These views need to be updated before they work # TODO: These views need to be updated before they work
# url(r'^calculate$', 'util.views.calculate'), # url(r'^calculate$', 'util.views.calculate'),
# url(r'^gradebook$', 'courseware.views.gradebook'), # url(r'^gradebook$', 'courseware.views.gradebook'),
# TODO: We should probably remove the circuit package. I believe it was only used in the old way of saving wiki circuits for the wiki # TODO: We should probably remove the circuit package. I believe it was only used in the old way of saving wiki circuits for the wiki
# url(r'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'), # url(r'^edit_circuit/(?P<circuit>[^/]*)$', 'circuit.views.edit_circuit'),
# url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'), # url(r'^save_circuit/(?P<circuit>[^/]*)$', 'circuit.views.save_circuit'),
url(r'^courses/?$', 'courseware.views.courses', name="courses"), url(r'^courses/?$', 'courseware.views.courses', name="courses"),
url(r'^change_enrollment$', url(r'^change_enrollment$',
'student.views.change_enrollment_view', name="change_enrollment"), 'student.views.change_enrollment_view', name="change_enrollment"),
#About the course #About the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/about$',
'courseware.views.course_about', name="about_course"), 'courseware.views.course_about', name="about_course"),
#Inside the course #Inside the course
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/info$',
'courseware.views.course_info', name="info"), 'courseware.views.course_info', name="info"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book$',
'staticbook.views.index', name="book"), 'staticbook.views.index', name="book"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<page>[^/]*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book/(?P<page>[^/]*)$',
'staticbook.views.index'), 'staticbook.views.index'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book-shifted/(?P<page>[^/]*)$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/book-shifted/(?P<page>[^/]*)$',
'staticbook.views.index_shifted'), 'staticbook.views.index_shifted'),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/?$',
'courseware.views.index', name="courseware"), 'courseware.views.index', name="courseware"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/courseware/(?P<chapter>[^/]*)/(?P<section>[^/]*)/$',
'courseware.views.index', name="courseware_section"), 'courseware.views.index', name="courseware_section"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile$',
'courseware.views.profile', name="profile"), 'courseware.views.profile', name="profile"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/profile/(?P<student_id>[^/]*)/$',
'courseware.views.profile'), 'courseware.views.profile'),
) )
# Multicourse wiki # Multicourse wiki
if settings.WIKI_ENABLED: if settings.WIKI_ENABLED:
urlpatterns += ( urlpatterns += (
...@@ -163,9 +164,9 @@ urlpatterns = patterns(*urlpatterns) ...@@ -163,9 +164,9 @@ urlpatterns = patterns(*urlpatterns)
if settings.DEBUG: if settings.DEBUG:
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
#Custom error pages #Custom error pages
handler404 = 'static_template_view.views.render_404' handler404 = 'static_template_view.views.render_404'
handler500 = 'static_template_view.views.render_500' handler500 = 'static_template_view.views.render_500'
...@@ -51,6 +51,7 @@ default_options = { ...@@ -51,6 +51,7 @@ default_options = {
task :predjango do task :predjango do
sh("find . -type f -name *.pyc -delete") sh("find . -type f -name *.pyc -delete")
sh('pip install -e common/lib/xmodule') sh('pip install -e common/lib/xmodule')
sh('git submodule update --init')
end end
[:lms, :cms, :common].each do |system| [:lms, :cms, :common].each do |system|
...@@ -150,14 +151,14 @@ end ...@@ -150,14 +151,14 @@ end
task :package do task :package do
FileUtils.mkdir_p(BUILD_DIR) FileUtils.mkdir_p(BUILD_DIR)
Dir.chdir(BUILD_DIR) do Dir.chdir(BUILD_DIR) do
afterremove = Tempfile.new('afterremove') afterremove = Tempfile.new('afterremove')
afterremove.write <<-AFTERREMOVE.gsub(/^\s*/, '') afterremove.write <<-AFTERREMOVE.gsub(/^\s*/, '')
#! /bin/bash #! /bin/bash
set -e set -e
set -x set -x
# to be a little safer this rm is executed # to be a little safer this rm is executed
# as the makeitso user # as the makeitso user
......
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