Merge remote-tracking branch 'origin/master' into dormsbee/multicourse
Conflicts: common/lib/xmodule/xmodule/capa_module.py common/lib/xmodule/xmodule/modulestore/xml.py lms/djangoapps/courseware/views.py lms/templates/index.html lms/templates/info.html lms/templates/main.html lms/templates/navigation.html requirements.txt
This commit is contained in:
@@ -8,6 +8,10 @@ setup(
|
||||
package_data={
|
||||
'xmodule': ['js/module/*']
|
||||
},
|
||||
requires=[
|
||||
'capa',
|
||||
'mitxmako'
|
||||
],
|
||||
|
||||
# See http://guide.python-distribute.org/creation.html#entry-points
|
||||
# for a description of entry_points
|
||||
|
||||
@@ -180,7 +180,12 @@ class CapaModule(XModule):
|
||||
score = d['score']
|
||||
total = d['total']
|
||||
if total > 0:
|
||||
return Progress(score, total)
|
||||
try:
|
||||
return Progress(score, total)
|
||||
except Exception as err:
|
||||
if self.system.DEBUG:
|
||||
return None
|
||||
raise
|
||||
return None
|
||||
|
||||
def get_html(self):
|
||||
@@ -194,7 +199,18 @@ class CapaModule(XModule):
|
||||
'''Return html for the problem. Adds check, reset, save buttons
|
||||
as necessary based on the problem config and state.'''
|
||||
|
||||
html = self.lcp.get_html()
|
||||
try:
|
||||
html = self.lcp.get_html()
|
||||
except Exception, err:
|
||||
if self.system.DEBUG:
|
||||
log.exception(err)
|
||||
msg = '[courseware.capa.capa_module] <font size="+1" color="red">Failed to generate HTML for problem %s</font>' % (self.location.url())
|
||||
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<','<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<','<')
|
||||
html = msg
|
||||
else:
|
||||
raise
|
||||
|
||||
content = {'name': self.metadata['display_name'],
|
||||
'html': html,
|
||||
'weight': self.weight,
|
||||
@@ -395,14 +411,18 @@ class CapaModule(XModule):
|
||||
correct_map = self.lcp.grade_answers(answers)
|
||||
except StudentInputError as inst:
|
||||
# TODO (vshnayder): why is this line here?
|
||||
self.lcp = LoncapaProblem(self.definition['data'],
|
||||
id=lcp_id, state=old_state, system=self.system)
|
||||
#self.lcp = LoncapaProblem(self.definition['data'],
|
||||
# id=lcp_id, state=old_state, system=self.system)
|
||||
traceback.print_exc()
|
||||
return {'success': inst.message}
|
||||
except:
|
||||
except Exception, err:
|
||||
# TODO: why is this line here?
|
||||
self.lcp = LoncapaProblem(self.definition['data'],
|
||||
id=lcp_id, state=old_state, system=self.system)
|
||||
#self.lcp = LoncapaProblem(self.definition['data'],
|
||||
# id=lcp_id, state=old_state, system=self.system)
|
||||
if self.system.DEBUG:
|
||||
msg = "Error checking problem: " + str(err)
|
||||
msg += '\nTraceback:\n' + traceback.format_exc()
|
||||
return {'success':msg}
|
||||
traceback.print_exc()
|
||||
raise Exception("error in capa_module")
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import logging
|
||||
from lxml import etree
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
@@ -26,3 +27,8 @@ class HtmlDescriptor(RawDescriptor):
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/module/html.coffee')]}
|
||||
js_module = 'HTML'
|
||||
|
||||
@classmethod
|
||||
def file_to_xml(cls, file_object):
|
||||
parser = etree.HTMLParser()
|
||||
return etree.parse(file_object, parser).getroot()
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
class @HTML
|
||||
constructor: (@id) ->
|
||||
@edit_box = $("##{@id} .edit-box")
|
||||
@preview = $("##{@id} .preview")
|
||||
constructor: (@element) ->
|
||||
@edit_box = $(".edit-box", @element)
|
||||
@preview = $(".preview", @element)
|
||||
@edit_box.on('input', =>
|
||||
@preview.empty().append(@edit_box.val())
|
||||
)
|
||||
|
||||
save: -> {text: @edit_box.val()}
|
||||
save: -> @edit_box.val()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
class @Raw
|
||||
constructor: (@id) ->
|
||||
@edit_box = $("##{@id} .edit-box")
|
||||
@preview = $("##{@id} .preview")
|
||||
constructor: (@element) ->
|
||||
@edit_box = $(".edit-box", @element)
|
||||
@preview = $(".preview", @element)
|
||||
@edit_box.on('input', =>
|
||||
@preview.empty().text(@edit_box.val())
|
||||
)
|
||||
|
||||
@@ -6,6 +6,9 @@ that are stored in a database an accessible using their Location as an identifie
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from .exceptions import InvalidLocationError
|
||||
import logging
|
||||
|
||||
log = logging.getLogger('mitx.' + 'modulestore')
|
||||
|
||||
URL_RE = re.compile("""
|
||||
(?P<tag>[^:]+)://
|
||||
@@ -74,11 +77,13 @@ class Location(_LocationBase):
|
||||
def check_list(list_):
|
||||
for val in list_:
|
||||
if val is not None and INVALID_CHARS.search(val) is not None:
|
||||
log.debug('invalid characters val="%s", list_="%s"' % (val,list_))
|
||||
raise InvalidLocationError(location)
|
||||
|
||||
if isinstance(location, basestring):
|
||||
match = URL_RE.match(location)
|
||||
if match is None:
|
||||
log.debug('location is instance of %s but no URL match' % basestring)
|
||||
raise InvalidLocationError(location)
|
||||
else:
|
||||
groups = match.groupdict()
|
||||
@@ -86,6 +91,7 @@ class Location(_LocationBase):
|
||||
return _LocationBase.__new__(_cls, **groups)
|
||||
elif isinstance(location, (list, tuple)):
|
||||
if len(location) not in (5, 6):
|
||||
log.debug('location has wrong length')
|
||||
raise InvalidLocationError(location)
|
||||
|
||||
if len(location) == 5:
|
||||
@@ -153,6 +159,18 @@ class ModuleStore(object):
|
||||
location is found
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_items(self, location, default_class=None):
|
||||
"""
|
||||
Returns a list of XModuleDescriptor instances for the items
|
||||
that match location. Any element of location that is None is treated
|
||||
as a wildcard that matches any value
|
||||
|
||||
location: Something that can be passed to Location
|
||||
default_class: An XModuleDescriptor subclass to use if no plugin matching the
|
||||
location is found
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
# TODO (cpennington): Replace with clone_item
|
||||
def create_item(self, location, editor):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pymongo
|
||||
from bson.objectid import ObjectId
|
||||
from importlib import import_module
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
@@ -8,6 +9,19 @@ from . import ModuleStore, Location
|
||||
from .exceptions import ItemNotFoundError, InsufficientSpecificationError
|
||||
|
||||
|
||||
# TODO (cpennington): This code currently operates under the assumption that
|
||||
# there is only one revision for each item. Once we start versioning inside the CMS,
|
||||
# that assumption will have to change
|
||||
|
||||
|
||||
def location_to_query(loc):
|
||||
query = {}
|
||||
for key, val in Location(loc).dict().iteritems():
|
||||
if val is not None:
|
||||
query['_id.{key}'.format(key=key)] = val
|
||||
|
||||
return query
|
||||
|
||||
class MongoModuleStore(ModuleStore):
|
||||
"""
|
||||
A Mongodb backed ModuleStore
|
||||
@@ -17,7 +31,6 @@ class MongoModuleStore(ModuleStore):
|
||||
host=host,
|
||||
port=port
|
||||
)[db][collection]
|
||||
self.collection.ensure_index('location')
|
||||
|
||||
# Force mongo to report errors, at the expense of performance
|
||||
self.collection.safe = True
|
||||
@@ -26,6 +39,18 @@ class MongoModuleStore(ModuleStore):
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
|
||||
# TODO (cpennington): Pass a proper resources_fs to the system
|
||||
self.system = MakoDescriptorSystem(
|
||||
load_item=self.get_item,
|
||||
resources_fs=None,
|
||||
render_template=render_to_string
|
||||
)
|
||||
|
||||
def _load_item(self, item):
|
||||
item['location'] = item['_id']
|
||||
del item['_id']
|
||||
return XModuleDescriptor.load_from_json(item, self.system, self.default_class)
|
||||
|
||||
def get_item(self, location):
|
||||
"""
|
||||
Returns an XModuleDescriptor instance for the item at location.
|
||||
@@ -39,24 +64,26 @@ class MongoModuleStore(ModuleStore):
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
|
||||
query = {}
|
||||
for key, val in Location(location).dict().iteritems():
|
||||
if key != 'revision' and val is None:
|
||||
raise InsufficientSpecificationError(location)
|
||||
|
||||
if val is not None:
|
||||
query['location.{key}'.format(key=key)] = val
|
||||
|
||||
item = self.collection.find_one(
|
||||
query,
|
||||
location_to_query(location),
|
||||
sort=[('revision', pymongo.ASCENDING)],
|
||||
)
|
||||
if item is None:
|
||||
raise ItemNotFoundError(location)
|
||||
return self._load_item(item)
|
||||
|
||||
# TODO (cpennington): Pass a proper resources_fs to the system
|
||||
return XModuleDescriptor.load_from_json(
|
||||
item, MakoDescriptorSystem(load_item=self.get_item, resources_fs=None, render_template=render_to_string), self.default_class)
|
||||
def get_items(self, location, default_class=None):
|
||||
print location_to_query(location)
|
||||
items = self.collection.find(
|
||||
location_to_query(location),
|
||||
sort=[('revision', pymongo.ASCENDING)],
|
||||
)
|
||||
|
||||
return [self._load_item(item) for item in items]
|
||||
|
||||
def create_item(self, location):
|
||||
"""
|
||||
@@ -65,7 +92,7 @@ class MongoModuleStore(ModuleStore):
|
||||
location: Something that can be passed to Location
|
||||
"""
|
||||
self.collection.insert({
|
||||
'location': Location(location).dict(),
|
||||
'_id': Location(location).dict(),
|
||||
})
|
||||
|
||||
def update_item(self, location, data):
|
||||
@@ -80,8 +107,9 @@ class MongoModuleStore(ModuleStore):
|
||||
# See http://www.mongodb.org/display/DOCS/Updating for
|
||||
# atomic update syntax
|
||||
self.collection.update(
|
||||
{'location': Location(location).dict()},
|
||||
{'$set': {'definition.data': data}}
|
||||
{'_id': Location(location).dict()},
|
||||
{'$set': {'definition.data': data}},
|
||||
|
||||
)
|
||||
|
||||
def update_children(self, location, children):
|
||||
@@ -96,7 +124,7 @@ class MongoModuleStore(ModuleStore):
|
||||
# See http://www.mongodb.org/display/DOCS/Updating for
|
||||
# atomic update syntax
|
||||
self.collection.update(
|
||||
{'location': Location(location).dict()},
|
||||
{'_id': Location(location).dict()},
|
||||
{'$set': {'definition.children': children}}
|
||||
)
|
||||
|
||||
@@ -112,6 +140,6 @@ class MongoModuleStore(ModuleStore):
|
||||
# See http://www.mongodb.org/display/DOCS/Updating for
|
||||
# atomic update syntax
|
||||
self.collection.update(
|
||||
{'location': Location(location).dict()},
|
||||
{'_id': Location(location).dict()},
|
||||
{'$set': {'metadata': metadata}}
|
||||
)
|
||||
|
||||
@@ -13,7 +13,7 @@ from .exceptions import ItemNotFoundError
|
||||
etree.set_default_parser(etree.XMLParser(dtd_validation=False, load_dtd=False,
|
||||
remove_comments=True, remove_blank_text=True))
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
class XMLModuleStore(ModuleStore):
|
||||
@@ -38,9 +38,13 @@ class XMLModuleStore(ModuleStore):
|
||||
self.default_class = None
|
||||
else:
|
||||
module_path, _, class_name = default_class.rpartition('.')
|
||||
log.debug('module_path = %s' % module_path)
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
|
||||
log.debug('XMLModuleStore: eager=%s, data_dir = %s' % (eager,self.data_dir))
|
||||
log.debug('default_class = %s' % self.default_class)
|
||||
|
||||
for course_dir in os.listdir(self.data_dir):
|
||||
if not os.path.exists(self.data_dir + "/" + course_dir + "/course.xml"):
|
||||
continue
|
||||
@@ -97,9 +101,11 @@ class XMLModuleStore(ModuleStore):
|
||||
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, modulestore.default_class)
|
||||
log.debug('==> importing module location %s' % repr(module.location))
|
||||
modulestore.modules[module.location] = module
|
||||
|
||||
if modulestore.eager:
|
||||
@@ -117,6 +123,7 @@ class XMLModuleStore(ModuleStore):
|
||||
|
||||
course_descriptor = ImportSystem(self).process_xml(etree.tostring(course_data))
|
||||
course_descriptor.metadata['data_dir'] = course_dir
|
||||
log.debug('========> Done with course import')
|
||||
return course_descriptor
|
||||
|
||||
def get_item(self, location):
|
||||
|
||||
@@ -350,6 +350,8 @@ class XModuleDescriptor(Plugin):
|
||||
etree.fromstring(xml_data).tag,
|
||||
default_class
|
||||
)
|
||||
# leave next line in code, commented out - useful for low-level debugging
|
||||
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (etree.fromstring(xml_data).tag,class_))
|
||||
return class_.from_xml(xml_data, system, org, course)
|
||||
|
||||
@classmethod
|
||||
|
||||
@@ -90,6 +90,16 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
if xml_object.get(attr) is not None:
|
||||
del xml_object.attrib[attr]
|
||||
|
||||
@classmethod
|
||||
def file_to_xml(cls, file_object):
|
||||
"""
|
||||
Used when this module wants to parse a file object to xml
|
||||
that will be converted to the definition.
|
||||
|
||||
Returns an lxml Element
|
||||
"""
|
||||
return etree.parse(file_object).getroot()
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
@@ -125,9 +135,10 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
definition_xml = copy.deepcopy(xml_object)
|
||||
else:
|
||||
filepath = cls._format_filepath(xml_object.tag, filename)
|
||||
log.debug('filepath=%s, resources_fs=%s' % (filepath,system.resources_fs))
|
||||
with system.resources_fs.open(filepath) as file:
|
||||
try:
|
||||
definition_xml = etree.parse(file).getroot()
|
||||
definition_xml = cls.file_to_xml(file)
|
||||
except:
|
||||
log.exception("Failed to parse xml in file %s" % filepath)
|
||||
raise
|
||||
@@ -147,8 +158,8 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _format_filepath(cls, type, name):
|
||||
return '{type}/{name}.{ext}'.format(type=type, name=name, ext=cls.filename_extension)
|
||||
def _format_filepath(cls, category, name):
|
||||
return u'{category}/{name}.{ext}'.format(category=category, name=name, ext=cls.filename_extension)
|
||||
|
||||
def export_to_xml(self, resource_fs):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user