Merge branch 'master' of github.com:dementrock/mitx into discussion2
This commit is contained in:
@@ -23,4 +23,7 @@ class Command(BaseCommand):
|
||||
course_dirs = args[1:]
|
||||
else:
|
||||
course_dirs = None
|
||||
print "Importing. Data_dir={data}, course_dirs={courses}".format(
|
||||
data=data_dir,
|
||||
courses=course_dirs)
|
||||
import_from_xml(modulestore(), data_dir, course_dirs)
|
||||
|
||||
@@ -214,7 +214,10 @@ def preview_module_system(request, preview_id, descriptor):
|
||||
get_module=partial(get_preview_module, request, preview_id),
|
||||
render_template=render_from_lms,
|
||||
debug=True,
|
||||
replace_urls=replace_urls
|
||||
replace_urls=replace_urls,
|
||||
# TODO (vshnayder): All CMS users get staff view by default
|
||||
# is that what we want?
|
||||
is_staff=True,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -76,3 +76,6 @@ CACHES = {
|
||||
'KEY_FUNCTION': 'util.memcache.safe_key',
|
||||
}
|
||||
}
|
||||
|
||||
# Make the keyedcache startup warnings go away
|
||||
CACHE_TIMEOUT = 0
|
||||
|
||||
@@ -76,8 +76,9 @@ def add_histogram(get_html, module):
|
||||
histogram = grade_histogram(module_id)
|
||||
render_histogram = len(histogram) > 0
|
||||
|
||||
# TODO: fixme - no filename in module.xml in general (this code block for edx4edx)
|
||||
# the following if block is for summer 2012 edX course development; it will change when the CMS comes online
|
||||
# TODO: fixme - no filename in module.xml in general (this code block
|
||||
# for edx4edx) the following if block is for summer 2012 edX course
|
||||
# development; it will change when the CMS comes online
|
||||
if settings.MITX_FEATURES.get('DISPLAY_EDIT_LINK') and settings.DEBUG and module_xml.get('filename') is not None:
|
||||
coursename = multicourse_settings.get_coursename_from_request(request)
|
||||
github_url = multicourse_settings.get_course_github_url(coursename)
|
||||
@@ -88,10 +89,8 @@ def add_histogram(get_html, module):
|
||||
else:
|
||||
edit_link = False
|
||||
|
||||
# Cast module.definition and module.metadata to dicts so that json can dump them
|
||||
# even though they are lazily loaded
|
||||
staff_context = {'definition': json.dumps(dict(module.definition), indent=4),
|
||||
'metadata': json.dumps(dict(module.metadata), indent=4),
|
||||
staff_context = {'definition': dict(module.definition),
|
||||
'metadata': dict(module.metadata),
|
||||
'element_id': module.location.html_id(),
|
||||
'edit_link': edit_link,
|
||||
'histogram': json.dumps(histogram),
|
||||
|
||||
@@ -329,7 +329,7 @@ class LoncapaProblem(object):
|
||||
# path is an absolute path or a path relative to the data dir
|
||||
dir = os.path.join(self.system.filestore.root_path, dir)
|
||||
abs_dir = os.path.normpath(dir)
|
||||
log.debug("appending to path: %s" % abs_dir)
|
||||
#log.debug("appending to path: %s" % abs_dir)
|
||||
path.append(abs_dir)
|
||||
|
||||
return path
|
||||
|
||||
14
common/lib/xmodule/html_checker.py
Normal file
14
common/lib/xmodule/html_checker.py
Normal file
@@ -0,0 +1,14 @@
|
||||
from lxml import etree
|
||||
|
||||
def check_html(html):
|
||||
'''
|
||||
Check whether the passed in html string can be parsed by lxml.
|
||||
Return bool success.
|
||||
'''
|
||||
parser = etree.HTMLParser()
|
||||
try:
|
||||
etree.fromstring(html, parser)
|
||||
return True
|
||||
except Exception as err:
|
||||
pass
|
||||
return False
|
||||
58
common/lib/xmodule/lazy_dict.py
Normal file
58
common/lib/xmodule/lazy_dict.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from collections import MutableMapping
|
||||
|
||||
class LazyLoadingDict(MutableMapping):
|
||||
"""
|
||||
A dictionary object that lazily loads its contents from a provided
|
||||
function on reads (of members that haven't already been set).
|
||||
"""
|
||||
|
||||
def __init__(self, loader):
|
||||
'''
|
||||
On the first read from this dictionary, it will call loader() to
|
||||
populate its contents. loader() must return something dict-like. Any
|
||||
elements set before the first read will be preserved.
|
||||
'''
|
||||
self._contents = {}
|
||||
self._loaded = False
|
||||
self._loader = loader
|
||||
self._deleted = set()
|
||||
|
||||
def __getitem__(self, name):
|
||||
if not (self._loaded or name in self._contents or name in self._deleted):
|
||||
self.load()
|
||||
|
||||
return self._contents[name]
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self._contents[name] = value
|
||||
self._deleted.discard(name)
|
||||
|
||||
def __delitem__(self, name):
|
||||
del self._contents[name]
|
||||
self._deleted.add(name)
|
||||
|
||||
def __contains__(self, name):
|
||||
self.load()
|
||||
return name in self._contents
|
||||
|
||||
def __len__(self):
|
||||
self.load()
|
||||
return len(self._contents)
|
||||
|
||||
def __iter__(self):
|
||||
self.load()
|
||||
return iter(self._contents)
|
||||
|
||||
def __repr__(self):
|
||||
self.load()
|
||||
return repr(self._contents)
|
||||
|
||||
def load(self):
|
||||
if self._loaded:
|
||||
return
|
||||
|
||||
loaded_contents = self._loader()
|
||||
loaded_contents.update(self._contents)
|
||||
self._contents = loaded_contents
|
||||
self._loaded = True
|
||||
|
||||
@@ -25,6 +25,7 @@ setup(
|
||||
"discuss = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"html = xmodule.html_module:HtmlDescriptor",
|
||||
"image = xmodule.backcompat_module:TranslateCustomTagDescriptor",
|
||||
"error = xmodule.error_module:ErrorDescriptor",
|
||||
"problem = xmodule.capa_module:CapaDescriptor",
|
||||
"problemset = xmodule.vertical_module:VerticalDescriptor",
|
||||
"section = xmodule.backcompat_module:SemanticSectionDescriptor",
|
||||
|
||||
20
common/lib/xmodule/stringify.py
Normal file
20
common/lib/xmodule/stringify.py
Normal file
@@ -0,0 +1,20 @@
|
||||
from itertools import chain
|
||||
from lxml import etree
|
||||
|
||||
def stringify_children(node):
|
||||
'''
|
||||
Return all contents of an xml tree, without the outside tags.
|
||||
e.g. if node is parse of
|
||||
"<html a="b" foo="bar">Hi <div>there <span>Bruce</span><b>!</b></div><html>"
|
||||
should return
|
||||
"Hi <div>there <span>Bruce</span><b>!</b></div>"
|
||||
|
||||
fixed from
|
||||
http://stackoverflow.com/questions/4624062/get-all-text-inside-a-tag-in-lxml
|
||||
'''
|
||||
parts = ([node.text] +
|
||||
list(chain(*([etree.tostring(c), c.tail]
|
||||
for c in node.getchildren())
|
||||
)))
|
||||
# filter removes possible Nones in texts and tails
|
||||
return ''.join(filter(None, parts))
|
||||
@@ -31,7 +31,8 @@ i4xs = ModuleSystem(
|
||||
user=Mock(),
|
||||
filestore=fs.osfs.OSFS(os.path.dirname(os.path.realpath(__file__))),
|
||||
debug=True,
|
||||
xqueue_callback_url='/'
|
||||
xqueue_callback_url='/',
|
||||
is_staff=False
|
||||
)
|
||||
|
||||
|
||||
|
||||
87
common/lib/xmodule/tests/test_import.py
Normal file
87
common/lib/xmodule/tests/test_import.py
Normal file
@@ -0,0 +1,87 @@
|
||||
from path import path
|
||||
import unittest
|
||||
|
||||
from lxml import etree
|
||||
|
||||
from xmodule.x_module import XMLParsingSystem, XModuleDescriptor
|
||||
from xmodule.errortracker import null_error_tracker
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
class ImportTestCase(unittest.TestCase):
|
||||
'''Make sure module imports work properly, including for malformed inputs'''
|
||||
|
||||
@staticmethod
|
||||
def get_system():
|
||||
'''Get a dummy system'''
|
||||
# Shouldn't need any system params, because the initial parse should fail
|
||||
def load_item(loc):
|
||||
raise Exception("Shouldn't be called")
|
||||
|
||||
resources_fs = None
|
||||
|
||||
def process_xml(xml):
|
||||
raise Exception("Shouldn't be called")
|
||||
|
||||
|
||||
def render_template(template, context):
|
||||
raise Exception("Shouldn't be called")
|
||||
|
||||
system = XMLParsingSystem(load_item, resources_fs,
|
||||
null_error_tracker, process_xml)
|
||||
system.render_template = render_template
|
||||
|
||||
return system
|
||||
|
||||
def test_fallback(self):
|
||||
'''Make sure that malformed xml loads as an ErrorDescriptor.'''
|
||||
|
||||
bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>'''
|
||||
|
||||
system = self.get_system()
|
||||
|
||||
descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course',
|
||||
None)
|
||||
|
||||
self.assertEqual(descriptor.__class__.__name__,
|
||||
'ErrorDescriptor')
|
||||
|
||||
def test_reimport(self):
|
||||
'''Make sure an already-exported error xml tag loads properly'''
|
||||
|
||||
bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>'''
|
||||
system = self.get_system()
|
||||
descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course',
|
||||
None)
|
||||
resource_fs = None
|
||||
tag_xml = descriptor.export_to_xml(resource_fs)
|
||||
re_import_descriptor = XModuleDescriptor.load_from_xml(tag_xml, system,
|
||||
'org', 'course',
|
||||
None)
|
||||
self.assertEqual(re_import_descriptor.__class__.__name__,
|
||||
'ErrorDescriptor')
|
||||
|
||||
self.assertEqual(descriptor.definition['data'],
|
||||
re_import_descriptor.definition['data'])
|
||||
|
||||
def test_fixed_xml_tag(self):
|
||||
"""Make sure a tag that's been fixed exports as the original tag type"""
|
||||
|
||||
# create a error tag with valid xml contents
|
||||
root = etree.Element('error')
|
||||
good_xml = '''<sequential display_name="fixed"><video url="hi"/></sequential>'''
|
||||
root.text = good_xml
|
||||
|
||||
xml_str_in = etree.tostring(root)
|
||||
|
||||
# load it
|
||||
system = self.get_system()
|
||||
descriptor = XModuleDescriptor.load_from_xml(xml_str_in, system, 'org', 'course',
|
||||
None)
|
||||
# export it
|
||||
resource_fs = None
|
||||
xml_str_out = descriptor.export_to_xml(resource_fs)
|
||||
|
||||
# Now make sure the exported xml is a sequential
|
||||
xml_out = etree.fromstring(xml_str_out)
|
||||
self.assertEqual(xml_out.tag, 'sequential')
|
||||
|
||||
9
common/lib/xmodule/tests/test_stringify.py
Normal file
9
common/lib/xmodule/tests/test_stringify.py
Normal file
@@ -0,0 +1,9 @@
|
||||
from nose.tools import assert_equals
|
||||
from lxml import etree
|
||||
from stringify import stringify_children
|
||||
|
||||
def test_stringify():
|
||||
html = '''<html a="b" foo="bar">Hi <div x="foo">there <span>Bruce</span><b>!</b></div></html>'''
|
||||
xml = etree.fromstring(html)
|
||||
out = stringify_children(xml)
|
||||
assert_equals(out, '''Hi <div x="foo">there <span>Bruce</span><b>!</b></div>''')
|
||||
@@ -32,21 +32,25 @@ def process_includes(fn):
|
||||
# read in and convert to XML
|
||||
incxml = etree.XML(ifp.read())
|
||||
|
||||
# insert new XML into tree in place of inlcude
|
||||
# insert new XML into tree in place of include
|
||||
parent.insert(parent.index(next_include), incxml)
|
||||
except Exception:
|
||||
msg = "Error in problem xml include: %s" % (etree.tostring(next_include, pretty_print=True))
|
||||
log.exception(msg)
|
||||
parent = next_include.getparent()
|
||||
# Log error
|
||||
msg = "Error in problem xml include: %s" % (
|
||||
etree.tostring(next_include, pretty_print=True))
|
||||
# tell the tracker
|
||||
system.error_tracker(msg)
|
||||
|
||||
# work around
|
||||
parent = next_include.getparent()
|
||||
errorxml = etree.Element('error')
|
||||
messagexml = etree.SubElement(errorxml, 'message')
|
||||
messagexml.text = msg
|
||||
stackxml = etree.SubElement(errorxml, 'stacktrace')
|
||||
stackxml.text = traceback.format_exc()
|
||||
|
||||
# insert error XML in place of include
|
||||
parent.insert(parent.index(next_include), errorxml)
|
||||
|
||||
parent.remove(next_include)
|
||||
|
||||
next_include = xml_object.find('include')
|
||||
|
||||
@@ -5,6 +5,7 @@ import json
|
||||
import logging
|
||||
import traceback
|
||||
import re
|
||||
import sys
|
||||
|
||||
from datetime import timedelta
|
||||
from lxml import etree
|
||||
@@ -91,7 +92,8 @@ class CapaModule(XModule):
|
||||
display_due_date_string = self.metadata.get('due', None)
|
||||
if display_due_date_string is not None:
|
||||
self.display_due_date = dateutil.parser.parse(display_due_date_string)
|
||||
#log.debug("Parsed " + display_due_date_string + " to " + str(self.display_due_date))
|
||||
#log.debug("Parsed " + display_due_date_string +
|
||||
# " to " + str(self.display_due_date))
|
||||
else:
|
||||
self.display_due_date = None
|
||||
|
||||
@@ -99,7 +101,8 @@ class CapaModule(XModule):
|
||||
if grace_period_string is not None and self.display_due_date:
|
||||
self.grace_period = parse_timedelta(grace_period_string)
|
||||
self.close_date = self.display_due_date + self.grace_period
|
||||
#log.debug("Then parsed " + grace_period_string + " to closing date" + str(self.close_date))
|
||||
#log.debug("Then parsed " + grace_period_string +
|
||||
# " to closing date" + str(self.close_date))
|
||||
else:
|
||||
self.grace_period = None
|
||||
self.close_date = self.display_due_date
|
||||
@@ -138,10 +141,16 @@ class CapaModule(XModule):
|
||||
try:
|
||||
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
|
||||
instance_state, seed=seed, system=self.system)
|
||||
except Exception:
|
||||
msg = 'cannot create LoncapaProblem %s' % self.location.url()
|
||||
log.exception(msg)
|
||||
except Exception as err:
|
||||
msg = 'cannot create LoncapaProblem {loc}: {err}'.format(
|
||||
loc=self.location.url(), err=err)
|
||||
# TODO (vshnayder): do modules need error handlers too?
|
||||
# We shouldn't be switching on DEBUG.
|
||||
if self.system.DEBUG:
|
||||
log.error(msg)
|
||||
# TODO (vshnayder): This logic should be general, not here--and may
|
||||
# want to preserve the data instead of replacing it.
|
||||
# e.g. in the CMS
|
||||
msg = '<p>%s</p>' % msg.replace('<', '<')
|
||||
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '<')
|
||||
# create a dummy problem with error message instead of failing
|
||||
@@ -152,7 +161,8 @@ class CapaModule(XModule):
|
||||
problem_text, self.location.html_id(),
|
||||
instance_state, seed=seed, system=self.system)
|
||||
else:
|
||||
raise
|
||||
# add extra info and raise
|
||||
raise Exception(msg), None, sys.exc_info()[2]
|
||||
|
||||
@property
|
||||
def rerandomize(self):
|
||||
@@ -191,6 +201,7 @@ class CapaModule(XModule):
|
||||
try:
|
||||
return Progress(score, total)
|
||||
except Exception as err:
|
||||
# TODO (vshnayder): why is this still here? still needed?
|
||||
if self.system.DEBUG:
|
||||
return None
|
||||
raise
|
||||
@@ -210,6 +221,7 @@ class CapaModule(XModule):
|
||||
try:
|
||||
html = self.lcp.get_html()
|
||||
except Exception, err:
|
||||
# TODO (vshnayder): another switch on DEBUG.
|
||||
if self.system.DEBUG:
|
||||
log.exception(err)
|
||||
msg = (
|
||||
@@ -561,6 +573,7 @@ class CapaDescriptor(RawDescriptor):
|
||||
'problems/' + path[8:],
|
||||
path[8:],
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
'''Problems always written in their own files'''
|
||||
|
||||
@@ -20,15 +20,22 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
self._grader = None
|
||||
self._grade_cutoffs = None
|
||||
|
||||
msg = None
|
||||
try:
|
||||
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
|
||||
except KeyError:
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
log.critical("Course loaded without a start date. %s", self.id)
|
||||
msg = "Course loaded without a start date. id = %s" % self.id
|
||||
log.critical(msg)
|
||||
except ValueError as e:
|
||||
self.start = time.gmtime(0) #The epoch
|
||||
log.critical("Course loaded with a bad start date. %s '%s'",
|
||||
self.id, e)
|
||||
msg = "Course loaded with a bad start date. %s '%s'" % (self.id, e)
|
||||
log.critical(msg)
|
||||
|
||||
# Don't call the tracker from the exception handler.
|
||||
if msg is not None:
|
||||
system.error_tracker(msg)
|
||||
|
||||
|
||||
def has_started(self):
|
||||
return time.gmtime() > self.start
|
||||
@@ -104,3 +111,4 @@ class CourseDescriptor(SequenceDescriptor):
|
||||
@property
|
||||
def org(self):
|
||||
return self.location.org
|
||||
|
||||
|
||||
24
common/lib/xmodule/xmodule/editing_module.py
Normal file
24
common/lib/xmodule/xmodule/editing_module.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from pkg_resources import resource_string
|
||||
from lxml import etree
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class EditingDescriptor(MakoModuleDescriptor):
|
||||
"""
|
||||
Module that provides a raw editing view of its data and children. It does not
|
||||
perform any validation on its definition---just passes it along to the browser.
|
||||
|
||||
This class is intended to be used as a mixin.
|
||||
"""
|
||||
mako_template = "widgets/raw-edit.html"
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/raw/edit.coffee')]}
|
||||
js_module_name = "RawDescriptor"
|
||||
|
||||
def get_context(self):
|
||||
return {
|
||||
'module': self,
|
||||
'data': self.definition['data'],
|
||||
}
|
||||
80
common/lib/xmodule/xmodule/error_module.py
Normal file
80
common/lib/xmodule/xmodule/error_module.py
Normal file
@@ -0,0 +1,80 @@
|
||||
from pkg_resources import resource_string
|
||||
from lxml import etree
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.editing_module import EditingDescriptor
|
||||
|
||||
import logging
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ErrorModule(XModule):
|
||||
def get_html(self):
|
||||
'''Show an error.
|
||||
TODO (vshnayder): proper style, divs, etc.
|
||||
'''
|
||||
if not self.system.is_staff:
|
||||
return self.system.render_template('module-error.html', {})
|
||||
|
||||
# staff get to see all the details
|
||||
return self.system.render_template('module-error-staff.html', {
|
||||
'data' : self.definition['data'],
|
||||
# TODO (vshnayder): need to get non-syntax errors in here somehow
|
||||
'error' : self.definition.get('error', 'Error not available')
|
||||
})
|
||||
|
||||
class ErrorDescriptor(EditingDescriptor):
|
||||
"""
|
||||
Module that provides a raw editing view of broken xml.
|
||||
"""
|
||||
module_class = ErrorModule
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None, err=None):
|
||||
'''Create an instance of this descriptor from the supplied data.
|
||||
|
||||
Does not try to parse the data--just stores it.
|
||||
|
||||
Takes an extra, optional, parameter--the error that caused an
|
||||
issue.
|
||||
'''
|
||||
|
||||
definition = {}
|
||||
if err is not None:
|
||||
definition['error'] = err
|
||||
|
||||
try:
|
||||
# If this is already an error tag, don't want to re-wrap it.
|
||||
xml_obj = etree.fromstring(xml_data)
|
||||
if xml_obj.tag == 'error':
|
||||
xml_data = xml_obj.text
|
||||
except etree.XMLSyntaxError as err:
|
||||
# Save the error to display later--overrides other problems
|
||||
definition['error'] = err
|
||||
|
||||
definition['data'] = xml_data
|
||||
# TODO (vshnayder): Do we need a unique slug here? Just pick a random
|
||||
# 64-bit num?
|
||||
location = ['i4x', org, course, 'error', 'slug']
|
||||
metadata = {} # stays in the xml_data
|
||||
|
||||
return cls(system, definition, location=location, metadata=metadata)
|
||||
|
||||
def export_to_xml(self, resource_fs):
|
||||
'''
|
||||
If the definition data is invalid xml, export it wrapped in an "error"
|
||||
tag. If it is valid, export without the wrapper.
|
||||
|
||||
NOTE: There may still be problems with the valid xml--it could be
|
||||
missing required attributes, could have the wrong tags, refer to missing
|
||||
files, etc. That would just get re-wrapped on import.
|
||||
'''
|
||||
try:
|
||||
xml = etree.fromstring(self.definition['data'])
|
||||
return etree.tostring(xml)
|
||||
except etree.XMLSyntaxError:
|
||||
# still not valid.
|
||||
root = etree.Element('error')
|
||||
root.text = self.definition['data']
|
||||
return etree.tostring(root)
|
||||
@@ -1,45 +0,0 @@
|
||||
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
|
||||
38
common/lib/xmodule/xmodule/errortracker.py
Normal file
38
common/lib/xmodule/xmodule/errortracker.py
Normal file
@@ -0,0 +1,38 @@
|
||||
import logging
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
ErrorLog = namedtuple('ErrorLog', 'tracker errors')
|
||||
|
||||
def in_exception_handler():
|
||||
'''Is there an active exception?'''
|
||||
return sys.exc_info() != (None, None, None)
|
||||
|
||||
|
||||
def make_error_tracker():
|
||||
'''Return an ErrorLog (named tuple), with fields (tracker, errors), where
|
||||
the logger appends a tuple (message, exception_str) to the errors on every
|
||||
call. exception_str is in the format returned by traceback.format_exception.
|
||||
|
||||
error_list is a simple list. If the caller modifies it, info
|
||||
will be lost.
|
||||
'''
|
||||
errors = []
|
||||
|
||||
def error_tracker(msg):
|
||||
'''Log errors'''
|
||||
exc_str = ''
|
||||
if in_exception_handler():
|
||||
exc_str = ''.join(traceback.format_exception(*sys.exc_info()))
|
||||
|
||||
errors.append((msg, exc_str))
|
||||
|
||||
return ErrorLog(error_tracker, errors)
|
||||
|
||||
def null_error_tracker(msg):
|
||||
'''A dummy error tracker that just ignores the messages'''
|
||||
pass
|
||||
@@ -1,13 +1,18 @@
|
||||
import copy
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from lxml import etree
|
||||
|
||||
from xmodule.x_module import XModule
|
||||
from xmodule.raw_module import RawDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
from xmodule.editing_module import EditingDescriptor
|
||||
from stringify import stringify_children
|
||||
from html_checker import check_html
|
||||
|
||||
log = logging.getLogger("mitx.courseware")
|
||||
|
||||
|
||||
class HtmlModule(XModule):
|
||||
def get_html(self):
|
||||
return self.html
|
||||
@@ -19,33 +24,110 @@ class HtmlModule(XModule):
|
||||
self.html = self.definition['data']
|
||||
|
||||
|
||||
class HtmlDescriptor(RawDescriptor):
|
||||
class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module for putting raw html in a course
|
||||
"""
|
||||
mako_template = "widgets/html-edit.html"
|
||||
module_class = HtmlModule
|
||||
filename_extension = "html"
|
||||
filename_extension = "xml"
|
||||
|
||||
# TODO (cpennington): Delete this method once all fall 2012 course are being
|
||||
# edited in the cms
|
||||
# VS[compat] TODO (cpennington): Delete this method once all fall 2012 course
|
||||
# are being edited in the cms
|
||||
@classmethod
|
||||
def backcompat_paths(cls, path):
|
||||
if path.endswith('.html.html'):
|
||||
path = path[:-5]
|
||||
origpath = path
|
||||
if path.endswith('.html.xml'):
|
||||
path = path[:-9] + '.html' #backcompat--look for html instead of xml
|
||||
candidates = []
|
||||
while os.sep in path:
|
||||
candidates.append(path)
|
||||
_, _, path = path.partition(os.sep)
|
||||
|
||||
# also look for .html versions instead of .xml
|
||||
if origpath.endswith('.xml'):
|
||||
candidates.append(origpath[:-4] + '.html')
|
||||
return candidates
|
||||
|
||||
# NOTE: html descriptors are special. We do not want to parse and
|
||||
# export them ourselves, because that can break things (e.g. lxml
|
||||
# adds body tags when it exports, but they should just be html
|
||||
# snippets that will be included in the middle of pages.
|
||||
|
||||
@classmethod
|
||||
def file_to_xml(cls, file_object):
|
||||
parser = etree.HTMLParser()
|
||||
return etree.parse(file_object, parser).getroot()
|
||||
def load_definition(cls, xml_object, system, location):
|
||||
'''Load a descriptor from the specified xml_object:
|
||||
|
||||
If there is a filename attribute, load it as a string, and
|
||||
log a warning if it is not parseable by etree.HTMLParser.
|
||||
|
||||
If there is not a filename attribute, the definition is the body
|
||||
of the xml_object, without the root tag (do not want <html> in the
|
||||
middle of a page)
|
||||
'''
|
||||
filename = xml_object.get('filename')
|
||||
if filename is None:
|
||||
definition_xml = copy.deepcopy(xml_object)
|
||||
cls.clean_metadata_from_xml(definition_xml)
|
||||
return {'data' : stringify_children(definition_xml)}
|
||||
else:
|
||||
filepath = cls._format_filepath(xml_object.tag, filename)
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): If the file doesn't exist at the right path,
|
||||
# give the class a chance to fix it up. The file will be written out
|
||||
# again in the correct format. This should go away once the CMS is
|
||||
# online and has imported all current (fall 2012) courses from xml
|
||||
if not system.resources_fs.exists(filepath):
|
||||
candidates = cls.backcompat_paths(filepath)
|
||||
#log.debug("candidates = {0}".format(candidates))
|
||||
for candidate in candidates:
|
||||
if system.resources_fs.exists(candidate):
|
||||
filepath = candidate
|
||||
break
|
||||
|
||||
try:
|
||||
with system.resources_fs.open(filepath) as file:
|
||||
html = file.read()
|
||||
# Log a warning if we can't parse the file, but don't error
|
||||
if not check_html(html):
|
||||
msg = "Couldn't parse html in {0}.".format(filepath)
|
||||
log.warning(msg)
|
||||
system.error_tracker("Warning: " + msg)
|
||||
return {'data' : html}
|
||||
except (ResourceNotFoundError) as err:
|
||||
msg = 'Unable to load file contents at path {0}: {1} '.format(
|
||||
filepath, err)
|
||||
# add more info and re-raise
|
||||
raise Exception(msg), None, sys.exc_info()[2]
|
||||
|
||||
@classmethod
|
||||
def split_to_file(cls, xml_object):
|
||||
# never include inline html
|
||||
'''Never include inline html'''
|
||||
return True
|
||||
|
||||
|
||||
# TODO (vshnayder): make export put things in the right places.
|
||||
|
||||
def definition_to_xml(self, resource_fs):
|
||||
'''If the contents are valid xml, write them to filename.xml. Otherwise,
|
||||
write just the <html filename=""> tag to filename.xml, and the html
|
||||
string to filename.html.
|
||||
'''
|
||||
try:
|
||||
return etree.fromstring(self.definition['data'])
|
||||
except etree.XMLSyntaxError:
|
||||
pass
|
||||
|
||||
# Not proper format. Write html to file, return an empty tag
|
||||
filepath = u'{category}/{name}.html'.format(category=self.category,
|
||||
name=self.name)
|
||||
|
||||
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as file:
|
||||
file.write(self.definition['data'])
|
||||
|
||||
elt = etree.Element('html')
|
||||
elt.set("filename", self.name)
|
||||
return elt
|
||||
|
||||
|
||||
@@ -2,10 +2,10 @@ from x_module import XModuleDescriptor, DescriptorSystem
|
||||
|
||||
|
||||
class MakoDescriptorSystem(DescriptorSystem):
|
||||
def __init__(self, load_item, resources_fs, error_handler,
|
||||
render_template):
|
||||
def __init__(self, load_item, resources_fs, error_tracker,
|
||||
render_template, **kwargs):
|
||||
super(MakoDescriptorSystem, self).__init__(
|
||||
load_item, resources_fs, error_handler)
|
||||
load_item, resources_fs, error_tracker, **kwargs)
|
||||
|
||||
self.render_template = render_template
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ that are stored in a database an accessible using their Location as an identifie
|
||||
|
||||
import re
|
||||
from collections import namedtuple
|
||||
from .exceptions import InvalidLocationError
|
||||
from .exceptions import InvalidLocationError, InsufficientSpecificationError
|
||||
import logging
|
||||
|
||||
log = logging.getLogger('mitx.' + 'modulestore')
|
||||
@@ -38,15 +38,15 @@ class Location(_LocationBase):
|
||||
'''
|
||||
__slots__ = ()
|
||||
|
||||
@classmethod
|
||||
def clean(cls, value):
|
||||
@staticmethod
|
||||
def clean(value):
|
||||
"""
|
||||
Return value, made into a form legal for locations
|
||||
"""
|
||||
return re.sub('_+', '_', INVALID_CHARS.sub('_', value))
|
||||
|
||||
@classmethod
|
||||
def is_valid(cls, value):
|
||||
@staticmethod
|
||||
def is_valid(value):
|
||||
'''
|
||||
Check if the value is a valid location, in any acceptable format.
|
||||
'''
|
||||
@@ -56,6 +56,21 @@ class Location(_LocationBase):
|
||||
return False
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def ensure_fully_specified(location):
|
||||
'''Make sure location is valid, and fully specified. Raises
|
||||
InvalidLocationError or InsufficientSpecificationError if not.
|
||||
|
||||
returns a Location object corresponding to location.
|
||||
'''
|
||||
loc = Location(location)
|
||||
for key, val in loc.dict().iteritems():
|
||||
if key != 'revision' and val is None:
|
||||
raise InsufficientSpecificationError(location)
|
||||
return loc
|
||||
|
||||
|
||||
|
||||
def __new__(_cls, loc_or_tag=None, org=None, course=None, category=None,
|
||||
name=None, revision=None):
|
||||
"""
|
||||
@@ -198,6 +213,18 @@ class ModuleStore(object):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_item_errors(self, location):
|
||||
"""
|
||||
Return a list of (msg, exception-or-None) errors that the modulestore
|
||||
encountered when loading the item at location.
|
||||
|
||||
location : something that can be passed to Location
|
||||
|
||||
Raises the same exceptions as get_item if the location isn't found or
|
||||
isn't fully specified.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def get_items(self, location, depth=0):
|
||||
"""
|
||||
Returns a list of XModuleDescriptor instances for the items
|
||||
@@ -254,25 +281,12 @@ class ModuleStore(object):
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
def path_to_location(self, location, course=None, chapter=None, section=None):
|
||||
'''
|
||||
Try to find a course/chapter/section[/position] path to this location.
|
||||
|
||||
raise ItemNotFoundError if the location doesn't exist.
|
||||
def get_parent_locations(self, location):
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
|
||||
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.
|
||||
returns an iterable of things that can be passed to Location.
|
||||
'''
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@@ -6,14 +6,13 @@ from itertools import repeat
|
||||
from path import path
|
||||
|
||||
from importlib import import_module
|
||||
from xmodule.errorhandlers import strict_error_handler
|
||||
from xmodule.errortracker import null_error_tracker
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from mitxmako.shortcuts import render_to_string
|
||||
|
||||
from . import ModuleStore, Location
|
||||
from .exceptions import (ItemNotFoundError, InsufficientSpecificationError,
|
||||
from .exceptions import (ItemNotFoundError,
|
||||
NoPathToItem, DuplicateItemError)
|
||||
|
||||
# TODO (cpennington): This code currently operates under the assumption that
|
||||
@@ -27,7 +26,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
|
||||
from, with a backup of calling to the underlying modulestore for more data
|
||||
"""
|
||||
def __init__(self, modulestore, module_data, default_class, resources_fs,
|
||||
error_handler, render_template):
|
||||
error_tracker, render_template):
|
||||
"""
|
||||
modulestore: the module store that can be used to retrieve additional modules
|
||||
|
||||
@@ -39,13 +38,13 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
|
||||
|
||||
resources_fs: a filesystem, as per MakoDescriptorSystem
|
||||
|
||||
error_handler:
|
||||
error_tracker:
|
||||
|
||||
render_template: a function for rendering templates, as per
|
||||
MakoDescriptorSystem
|
||||
"""
|
||||
super(CachingDescriptorSystem, self).__init__(
|
||||
self.load_item, resources_fs, error_handler, render_template)
|
||||
self.load_item, resources_fs, error_tracker, render_template)
|
||||
self.modulestore = modulestore
|
||||
self.module_data = module_data
|
||||
self.default_class = default_class
|
||||
@@ -80,7 +79,8 @@ class MongoModuleStore(ModuleStore):
|
||||
"""
|
||||
|
||||
# TODO (cpennington): Enable non-filesystem filestores
|
||||
def __init__(self, host, db, collection, fs_root, port=27017, default_class=None):
|
||||
def __init__(self, host, db, collection, fs_root, port=27017, default_class=None,
|
||||
error_tracker=null_error_tracker):
|
||||
self.collection = pymongo.connection.Connection(
|
||||
host=host,
|
||||
port=port
|
||||
@@ -91,13 +91,17 @@ class MongoModuleStore(ModuleStore):
|
||||
|
||||
# Force mongo to maintain an index over _id.* that is in the same order
|
||||
# that is used when querying by a location
|
||||
self.collection.ensure_index(zip(('_id.' + field for field in Location._fields), repeat(1)))
|
||||
self.collection.ensure_index(
|
||||
zip(('_id.' + field for field in Location._fields), repeat(1)))
|
||||
|
||||
# TODO (vshnayder): default arg default_class=None will make this error
|
||||
module_path, _, class_name = default_class.rpartition('.')
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
if default_class is not None:
|
||||
module_path, _, class_name = default_class.rpartition('.')
|
||||
class_ = getattr(import_module(module_path), class_name)
|
||||
self.default_class = class_
|
||||
else:
|
||||
self.default_class = None
|
||||
self.fs_root = path(fs_root)
|
||||
self.error_tracker = error_tracker
|
||||
|
||||
def _clean_item_data(self, item):
|
||||
"""
|
||||
@@ -149,7 +153,7 @@ class MongoModuleStore(ModuleStore):
|
||||
data_cache,
|
||||
self.default_class,
|
||||
resource_fs,
|
||||
strict_error_handler,
|
||||
self.error_tracker,
|
||||
render_to_string,
|
||||
)
|
||||
return system.load_item(item['location'])
|
||||
@@ -172,12 +176,17 @@ class MongoModuleStore(ModuleStore):
|
||||
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(
|
||||
'''Look for a given location in the collection. If revision is not
|
||||
specified, returns the latest. If the item is not present, raise
|
||||
ItemNotFoundError.
|
||||
'''
|
||||
item = self.collection.find_one(
|
||||
location_to_query(location),
|
||||
sort=[('revision', pymongo.ASCENDING)],
|
||||
)
|
||||
if item is None:
|
||||
raise ItemNotFoundError(location)
|
||||
return item
|
||||
|
||||
def get_item(self, location, depth=0):
|
||||
"""
|
||||
@@ -197,14 +206,8 @@ class MongoModuleStore(ModuleStore):
|
||||
calls to get_children() to cache. None indicates to cache all descendents.
|
||||
|
||||
"""
|
||||
|
||||
for key, val in Location(location).dict().iteritems():
|
||||
if key != 'revision' and val is None:
|
||||
raise InsufficientSpecificationError(location)
|
||||
|
||||
location = Location.ensure_fully_specified(location)
|
||||
item = self._find_one(location)
|
||||
if item is None:
|
||||
raise ItemNotFoundError(location)
|
||||
return self._load_items([item], depth)[0]
|
||||
|
||||
def get_items(self, location, depth=0):
|
||||
@@ -282,96 +285,20 @@ class MongoModuleStore(ModuleStore):
|
||||
)
|
||||
|
||||
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.
|
||||
'''Find all locations that are the parents of this location. Needed
|
||||
for path_to_location().
|
||||
|
||||
returns an iterable of things that can be passed to Location.
|
||||
If there is no data at location in this modulestore, raise
|
||||
ItemNotFoundError.
|
||||
|
||||
returns an iterable of things that can be passed to Location. This may
|
||||
be empty if there are no parents.
|
||||
'''
|
||||
location = Location(location)
|
||||
items = self.collection.find({'definition.children': str(location)},
|
||||
location = Location.ensure_fully_specified(location)
|
||||
# Check that it's actually in this modulestore.
|
||||
item = self._find_one(location)
|
||||
# now get the parents
|
||||
items = self.collection.find({'definition.children': location.url()},
|
||||
{'_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)
|
||||
|
||||
96
common/lib/xmodule/xmodule/modulestore/search.py
Normal file
96
common/lib/xmodule/xmodule/modulestore/search.py
Normal file
@@ -0,0 +1,96 @@
|
||||
from itertools import repeat
|
||||
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
|
||||
from .exceptions import (ItemNotFoundError, NoPathToItem)
|
||||
from . import ModuleStore, Location
|
||||
|
||||
|
||||
def path_to_location(modulestore, location, course_name=None):
|
||||
'''
|
||||
Try to find a course_id/chapter/section[/position] path to location in
|
||||
modulestore. 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.
|
||||
'''
|
||||
|
||||
def flatten(xs):
|
||||
'''Convert lisp-style (a, (b, (c, ()))) list 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)
|
||||
|
||||
# get_parent_locations should raise ItemNotFoundError if location
|
||||
# isn't found so we don't have to do it explicitly. Call this
|
||||
# first to make sure the location is there (even if it's a course, and
|
||||
# we would otherwise immediately exit).
|
||||
parents = modulestore.get_parent_locations(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)
|
||||
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)
|
||||
@@ -8,6 +8,7 @@ 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 xmodule.modulestore.search import path_to_location
|
||||
|
||||
# from ~/mitx_all/mitx/common/lib/xmodule/xmodule/modulestore/tests/
|
||||
# to ~/mitx_all/mitx/common/test
|
||||
@@ -28,7 +29,7 @@ DEFAULT_CLASS = 'xmodule.raw_module.RawDescriptor'
|
||||
|
||||
|
||||
class TestMongoModuleStore(object):
|
||||
|
||||
'''Tests!'''
|
||||
@classmethod
|
||||
def setupClass(cls):
|
||||
cls.connection = pymongo.connection.Connection(HOST, PORT)
|
||||
@@ -67,7 +68,7 @@ class TestMongoModuleStore(object):
|
||||
|
||||
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'''
|
||||
Call this directly from failing tests to see what is loaded'''
|
||||
ids = list(self.connection[DB][COLLECTION].find({}, {'_id': True}))
|
||||
|
||||
pprint([Location(i['_id']).url() for i in ids])
|
||||
@@ -93,8 +94,6 @@ class TestMongoModuleStore(object):
|
||||
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")),
|
||||
@@ -117,13 +116,13 @@ class TestMongoModuleStore(object):
|
||||
("edX/toy/2012_Fall", "Overview", "Toy_Videos", None)),
|
||||
)
|
||||
for location, expected in should_work:
|
||||
assert_equals(self.store.path_to_location(location), expected)
|
||||
assert_equals(path_to_location(self.store, location), expected)
|
||||
|
||||
not_found = (
|
||||
"i4x://edX/toy/video/WelcomeX",
|
||||
"i4x://edX/toy/video/WelcomeX", "i4x://edX/toy/course/NotHome"
|
||||
)
|
||||
for location in not_found:
|
||||
assert_raises(ItemNotFoundError, self.store.path_to_location, location)
|
||||
assert_raises(ItemNotFoundError, path_to_location, self.store, 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
|
||||
@@ -132,5 +131,5 @@ class TestMongoModuleStore(object):
|
||||
"i4x://edX/simple/video/Lost_Video",
|
||||
)
|
||||
for location in no_path:
|
||||
assert_raises(NoPathToItem, self.store.path_to_location, location, "toy")
|
||||
assert_raises(NoPathToItem, path_to_location, self.store, location, "toy")
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
||||
from fs.osfs import OSFS
|
||||
from importlib import import_module
|
||||
from lxml import etree
|
||||
from path import path
|
||||
from xmodule.errorhandlers import logging_error_handler
|
||||
from xmodule.errortracker import ErrorLog, make_error_tracker
|
||||
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
from xmodule.mako_module import MakoDescriptorSystem
|
||||
from cStringIO import StringIO
|
||||
import os
|
||||
import re
|
||||
|
||||
from . import ModuleStore, Location
|
||||
from .exceptions import ItemNotFoundError
|
||||
@@ -19,7 +21,6 @@ etree.set_default_parser(
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): Remove this once all fall 2012 courses have been imported
|
||||
# into the cms from xml
|
||||
@@ -29,7 +30,7 @@ def clean_out_mako_templating(xml_string):
|
||||
return xml_string
|
||||
|
||||
class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
def __init__(self, xmlstore, org, course, course_dir, error_handler):
|
||||
def __init__(self, xmlstore, org, course, course_dir, error_tracker, **kwargs):
|
||||
"""
|
||||
A class that handles loading from xml. Does some munging to ensure that
|
||||
all elements have unique slugs.
|
||||
@@ -70,28 +71,28 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
|
||||
# log.debug('-> slug=%s' % slug)
|
||||
xml_data.set('url_name', slug)
|
||||
|
||||
module = XModuleDescriptor.load_from_xml(
|
||||
descriptor = 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
|
||||
#log.debug('==> importing descriptor location %s' %
|
||||
# repr(descriptor.location))
|
||||
descriptor.metadata['data_dir'] = course_dir
|
||||
|
||||
xmlstore.modules[module.location] = module
|
||||
xmlstore.modules[descriptor.location] = descriptor
|
||||
|
||||
if xmlstore.eager:
|
||||
module.get_children()
|
||||
return module
|
||||
descriptor.get_children()
|
||||
return descriptor
|
||||
|
||||
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)
|
||||
error_tracker, render_template, **kwargs)
|
||||
XMLParsingSystem.__init__(self, load_item, resources_fs,
|
||||
error_handler, process_xml)
|
||||
|
||||
error_tracker, process_xml, **kwargs)
|
||||
|
||||
|
||||
class XMLModuleStore(ModuleStore):
|
||||
@@ -99,8 +100,7 @@ class XMLModuleStore(ModuleStore):
|
||||
An XML backed ModuleStore
|
||||
"""
|
||||
def __init__(self, data_dir, default_class=None, eager=False,
|
||||
course_dirs=None,
|
||||
error_handler=logging_error_handler):
|
||||
course_dirs=None):
|
||||
"""
|
||||
Initialize an XMLModuleStore from data_dir
|
||||
|
||||
@@ -114,17 +114,14 @@ class XMLModuleStore(ModuleStore):
|
||||
|
||||
course_dirs: If specified, the list of course_dirs to load. Otherwise,
|
||||
load all course dirs
|
||||
|
||||
error_handler: The error handler used here and in the underlying
|
||||
DescriptorSystem. By default, raise exceptions for all errors.
|
||||
See the comments in x_module.py:DescriptorSystem
|
||||
"""
|
||||
|
||||
self.eager = eager
|
||||
self.data_dir = path(data_dir)
|
||||
self.modules = {} # location -> XModuleDescriptor
|
||||
self.courses = {} # course_dir -> XModuleDescriptor for the course
|
||||
self.error_handler = error_handler
|
||||
self.location_errors = {} # location -> ErrorLog
|
||||
|
||||
|
||||
if default_class is None:
|
||||
self.default_class = None
|
||||
@@ -148,15 +145,18 @@ class XMLModuleStore(ModuleStore):
|
||||
|
||||
for course_dir in course_dirs:
|
||||
try:
|
||||
course_descriptor = self.load_course(course_dir)
|
||||
# make a tracker, then stick in the right place once the course loads
|
||||
# and we know its location
|
||||
errorlog = make_error_tracker()
|
||||
course_descriptor = self.load_course(course_dir, errorlog.tracker)
|
||||
self.courses[course_dir] = course_descriptor
|
||||
self.location_errors[course_descriptor.location] = errorlog
|
||||
except:
|
||||
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, tracker):
|
||||
"""
|
||||
Load a course into this module store
|
||||
course_path: Course directory name
|
||||
@@ -190,13 +190,13 @@ class XMLModuleStore(ModuleStore):
|
||||
))
|
||||
course = course_dir
|
||||
|
||||
system = ImportSystem(self, org, course, course_dir,
|
||||
self.error_handler)
|
||||
system = ImportSystem(self, org, course, course_dir, tracker)
|
||||
|
||||
course_descriptor = system.process_xml(etree.tostring(course_data))
|
||||
log.debug('========> Done with course import from {0}'.format(course_dir))
|
||||
return course_descriptor
|
||||
|
||||
|
||||
def get_item(self, location, depth=0):
|
||||
"""
|
||||
Returns an XModuleDescriptor instance for the item at location.
|
||||
@@ -217,9 +217,28 @@ class XMLModuleStore(ModuleStore):
|
||||
except KeyError:
|
||||
raise ItemNotFoundError(location)
|
||||
|
||||
|
||||
def get_item_errors(self, location):
|
||||
"""
|
||||
Return list of errors for this location, if any. Raise the same
|
||||
errors as get_item if location isn't present.
|
||||
|
||||
NOTE: This only actually works for courses in the xml datastore--
|
||||
will return an empty list for all other modules.
|
||||
"""
|
||||
location = Location(location)
|
||||
# check that item is present
|
||||
self.get_item(location)
|
||||
|
||||
# now look up errors
|
||||
if location in self.location_errors:
|
||||
return self.location_errors[location].errors
|
||||
return []
|
||||
|
||||
def get_courses(self, depth=0):
|
||||
"""
|
||||
Returns a list of course descriptors
|
||||
Returns a list of course descriptors. If there were errors on loading,
|
||||
some of these may be ErrorDescriptors instead.
|
||||
"""
|
||||
return self.courses.values()
|
||||
|
||||
|
||||
@@ -1,27 +1,16 @@
|
||||
from pkg_resources import resource_string
|
||||
from lxml import etree
|
||||
from xmodule.mako_module import MakoModuleDescriptor
|
||||
from xmodule.editing_module import EditingDescriptor
|
||||
from xmodule.xml_module import XmlDescriptor
|
||||
import logging
|
||||
import sys
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
class RawDescriptor(XmlDescriptor, EditingDescriptor):
|
||||
"""
|
||||
Module that provides a raw editing view of its data and children
|
||||
Module that provides a raw editing view of its data and children. It
|
||||
requires that the definition xml is valid.
|
||||
"""
|
||||
mako_template = "widgets/raw-edit.html"
|
||||
|
||||
js = {'coffee': [resource_string(__name__, 'js/src/raw/edit.coffee')]}
|
||||
js_module_name = "RawDescriptor"
|
||||
|
||||
def get_context(self):
|
||||
return {
|
||||
'module': self,
|
||||
'data': self.definition['data'],
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def definition_from_xml(cls, xml_object, system):
|
||||
return {'data': etree.tostring(xml_object)}
|
||||
@@ -30,13 +19,12 @@ class RawDescriptor(MakoModuleDescriptor, XmlDescriptor):
|
||||
try:
|
||||
return etree.fromstring(self.definition['data'])
|
||||
except etree.XMLSyntaxError as err:
|
||||
# Can't recover here, so just add some info and
|
||||
# re-raise
|
||||
lines = self.definition['data'].split('\n')
|
||||
line, offset = err.position
|
||||
msg = ("Unable to create xml for problem {loc}. "
|
||||
"Context: '{context}'".format(
|
||||
context=lines[line - 1][offset - 40:offset + 40],
|
||||
loc=self.location))
|
||||
log.exception(msg)
|
||||
self.system.error_handler(msg)
|
||||
# no workaround possible, so just re-raise
|
||||
raise
|
||||
raise Exception, msg, sys.exc_info()[2]
|
||||
|
||||
@@ -7,16 +7,14 @@ from mako.template import Template
|
||||
class CustomTagModule(XModule):
|
||||
"""
|
||||
This module supports tags of the form
|
||||
<customtag option="val" option2="val2">
|
||||
<impl>$tagname</impl>
|
||||
</customtag>
|
||||
<customtag option="val" option2="val2" impl="tagname"/>
|
||||
|
||||
In this case, $tagname should refer to a file in data/custom_tags, which contains
|
||||
a mako template that uses ${option} and ${option2} for the content.
|
||||
|
||||
For instance:
|
||||
|
||||
data/custom_tags/book::
|
||||
data/mycourse/custom_tags/book::
|
||||
More information given in <a href="/book/${page}">the text</a>
|
||||
|
||||
course.xml::
|
||||
@@ -34,7 +32,18 @@ class CustomTagModule(XModule):
|
||||
instance_state, shared_state, **kwargs)
|
||||
|
||||
xmltree = etree.fromstring(self.definition['data'])
|
||||
template_name = xmltree.attrib['impl']
|
||||
if 'impl' in xmltree.attrib:
|
||||
template_name = xmltree.attrib['impl']
|
||||
else:
|
||||
# VS[compat] backwards compatibility with old nested customtag structure
|
||||
child_impl = xmltree.find('impl')
|
||||
if child_impl is not None:
|
||||
template_name = child_impl.text
|
||||
else:
|
||||
# TODO (vshnayder): better exception type
|
||||
raise Exception("Could not find impl attribute in customtag {0}"
|
||||
.format(location))
|
||||
|
||||
params = dict(xmltree.items())
|
||||
with self.system.filestore.open(
|
||||
'custom_tags/{name}'.format(name=template_name)) as template:
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from lxml import etree
|
||||
from lxml.etree import XMLSyntaxError
|
||||
import pkg_resources
|
||||
import logging
|
||||
from fs.errors import ResourceNotFoundError
|
||||
from functools import partial
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
|
||||
from functools import partial
|
||||
|
||||
log = logging.getLogger('mitx.' + __name__)
|
||||
|
||||
@@ -192,9 +194,6 @@ class XModule(HTMLSnippet):
|
||||
self.metadata = kwargs.get('metadata', {})
|
||||
self._loaded_children = None
|
||||
|
||||
def get_name(self):
|
||||
return self.name
|
||||
|
||||
def get_children(self):
|
||||
'''
|
||||
Return module instances for all the children of this module.
|
||||
@@ -443,16 +442,30 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
system is an XMLParsingSystem
|
||||
|
||||
org and course are optional strings that will be used in the generated
|
||||
modules url identifiers
|
||||
module's url identifiers
|
||||
"""
|
||||
class_ = XModuleDescriptor.load_class(
|
||||
etree.fromstring(xml_data).tag,
|
||||
default_class
|
||||
)
|
||||
# leave next line, commented out - useful for low-level debugging
|
||||
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
|
||||
# etree.fromstring(xml_data).tag,class_))
|
||||
return class_.from_xml(xml_data, system, org, course)
|
||||
try:
|
||||
class_ = XModuleDescriptor.load_class(
|
||||
etree.fromstring(xml_data).tag,
|
||||
default_class
|
||||
)
|
||||
# leave next line, commented out - useful for low-level debugging
|
||||
# log.debug('[XModuleDescriptor.load_from_xml] tag=%s, class_=%s' % (
|
||||
# etree.fromstring(xml_data).tag,class_))
|
||||
|
||||
descriptor = class_.from_xml(xml_data, system, org, course)
|
||||
except Exception as err:
|
||||
# Didn't load properly. Fall back on loading as an error
|
||||
# descriptor. This should never error due to formatting.
|
||||
|
||||
# Put import here to avoid circular import errors
|
||||
from xmodule.error_module import ErrorDescriptor
|
||||
msg = "Error loading from xml."
|
||||
log.exception(msg)
|
||||
system.error_tracker(msg)
|
||||
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course, err)
|
||||
|
||||
return descriptor
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
@@ -521,16 +534,19 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
|
||||
|
||||
|
||||
class DescriptorSystem(object):
|
||||
def __init__(self, load_item, resources_fs, error_handler):
|
||||
def __init__(self, load_item, resources_fs, error_tracker, **kwargs):
|
||||
"""
|
||||
load_item: Takes a Location and returns an XModuleDescriptor
|
||||
|
||||
resources_fs: A Filesystem object that contains all of the
|
||||
resources needed for the course
|
||||
|
||||
error_handler: A hook for handling errors in loading the descriptor.
|
||||
Must be a function of (error_msg, exc_info=None).
|
||||
See errorhandlers.py for some simple ones.
|
||||
error_tracker: A hook for tracking errors in loading the descriptor.
|
||||
Used for example to get a list of all non-fatal problems on course
|
||||
load, and display them to the user.
|
||||
|
||||
A function of (error_msg). errortracker.py provides a
|
||||
handy make_error_tracker() function.
|
||||
|
||||
Patterns for using the error handler:
|
||||
try:
|
||||
@@ -539,10 +555,8 @@ class DescriptorSystem(object):
|
||||
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
|
||||
self.system.error_tracker(msg)
|
||||
# work around
|
||||
return 'Oops, couldn't load grommet'
|
||||
|
||||
OR, if not in an exception context:
|
||||
@@ -550,25 +564,27 @@ class DescriptorSystem(object):
|
||||
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.system.error_tracker(msg)
|
||||
|
||||
NOTE: To avoid duplication, do not call the tracker on errors
|
||||
that you're about to re-raise---let the caller track them.
|
||||
"""
|
||||
|
||||
self.load_item = load_item
|
||||
self.resources_fs = resources_fs
|
||||
self.error_handler = error_handler
|
||||
self.error_tracker = error_tracker
|
||||
|
||||
|
||||
class XMLParsingSystem(DescriptorSystem):
|
||||
def __init__(self, load_item, resources_fs, error_handler, process_xml):
|
||||
def __init__(self, load_item, resources_fs, error_tracker, process_xml, **kwargs):
|
||||
"""
|
||||
load_item, resources_fs, error_handler: see DescriptorSystem
|
||||
load_item, resources_fs, error_tracker: see DescriptorSystem
|
||||
|
||||
process_xml: Takes an xml string, and returns a XModuleDescriptor
|
||||
created from that xml
|
||||
"""
|
||||
DescriptorSystem.__init__(self, load_item, resources_fs, error_handler)
|
||||
DescriptorSystem.__init__(self, load_item, resources_fs, error_tracker,
|
||||
**kwargs)
|
||||
self.process_xml = process_xml
|
||||
|
||||
|
||||
@@ -584,10 +600,18 @@ class ModuleSystem(object):
|
||||
Note that these functions can be closures over e.g. a django request
|
||||
and user, or other environment-specific info.
|
||||
'''
|
||||
def __init__(self, ajax_url, track_function,
|
||||
get_module, render_template, replace_urls,
|
||||
user=None, filestore=None, debug=False,
|
||||
xqueue_callback_url=None, xqueue_default_queuename="null"):
|
||||
def __init__(self,
|
||||
ajax_url,
|
||||
track_function,
|
||||
get_module,
|
||||
render_template,
|
||||
replace_urls,
|
||||
user=None,
|
||||
filestore=None,
|
||||
debug=False,
|
||||
xqueue_callback_url=None,
|
||||
xqueue_default_queuename="null",
|
||||
is_staff=False):
|
||||
'''
|
||||
Create a closure around the system environment.
|
||||
|
||||
@@ -613,6 +637,9 @@ class ModuleSystem(object):
|
||||
replace_urls - TEMPORARY - A function like static_replace.replace_urls
|
||||
that capa_module can use to fix up the static urls in
|
||||
ajax results.
|
||||
|
||||
is_staff - Is the user making the request a staff user?
|
||||
TODO (vshnayder): this will need to change once we have real user roles.
|
||||
'''
|
||||
self.ajax_url = ajax_url
|
||||
self.xqueue_callback_url = xqueue_callback_url
|
||||
@@ -624,6 +651,7 @@ class ModuleSystem(object):
|
||||
self.DEBUG = self.debug = debug
|
||||
self.seed = user.id if user is not None else 0
|
||||
self.replace_urls = replace_urls
|
||||
self.is_staff = is_staff
|
||||
|
||||
def get(self, attr):
|
||||
''' provide uniform access to attributes (like etree).'''
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from collections import MutableMapping
|
||||
from xmodule.x_module import XModuleDescriptor
|
||||
from xmodule.modulestore import Location
|
||||
from lxml import etree
|
||||
@@ -8,74 +7,12 @@ import traceback
|
||||
from collections import namedtuple
|
||||
from fs.errors import ResourceNotFoundError
|
||||
import os
|
||||
import sys
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
# TODO (cpennington): This was implemented in an attempt to improve performance,
|
||||
# but the actual improvement wasn't measured (and it was implemented late at night).
|
||||
# We should check if it hurts, and whether there's a better way of doing lazy loading
|
||||
|
||||
|
||||
class LazyLoadingDict(MutableMapping):
|
||||
"""
|
||||
A dictionary object that lazily loads its contents from a provided
|
||||
function on reads (of members that haven't already been set).
|
||||
"""
|
||||
|
||||
def __init__(self, loader):
|
||||
'''
|
||||
On the first read from this dictionary, it will call loader() to
|
||||
populate its contents. loader() must return something dict-like. Any
|
||||
elements set before the first read will be preserved.
|
||||
'''
|
||||
self._contents = {}
|
||||
self._loaded = False
|
||||
self._loader = loader
|
||||
self._deleted = set()
|
||||
|
||||
def __getitem__(self, name):
|
||||
if not (self._loaded or name in self._contents or name in self._deleted):
|
||||
self.load()
|
||||
|
||||
return self._contents[name]
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self._contents[name] = value
|
||||
self._deleted.discard(name)
|
||||
|
||||
def __delitem__(self, name):
|
||||
del self._contents[name]
|
||||
self._deleted.add(name)
|
||||
|
||||
def __contains__(self, name):
|
||||
self.load()
|
||||
return name in self._contents
|
||||
|
||||
def __len__(self):
|
||||
self.load()
|
||||
return len(self._contents)
|
||||
|
||||
def __iter__(self):
|
||||
self.load()
|
||||
return iter(self._contents)
|
||||
|
||||
def __repr__(self):
|
||||
self.load()
|
||||
return repr(self._contents)
|
||||
|
||||
def load(self):
|
||||
if self._loaded:
|
||||
return
|
||||
|
||||
loaded_contents = self._loader()
|
||||
loaded_contents.update(self._contents)
|
||||
self._contents = loaded_contents
|
||||
self._loaded = True
|
||||
|
||||
|
||||
_AttrMapBase = namedtuple('_AttrMap', 'metadata_key to_metadata from_metadata')
|
||||
|
||||
|
||||
class AttrMap(_AttrMapBase):
|
||||
"""
|
||||
A class that specifies a metadata_key, and two functions:
|
||||
@@ -163,6 +100,45 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
"""
|
||||
return etree.parse(file_object).getroot()
|
||||
|
||||
@classmethod
|
||||
def load_definition(cls, xml_object, system, location):
|
||||
'''Load a descriptor definition from the specified xml_object.
|
||||
Subclasses should not need to override this except in special
|
||||
cases (e.g. html module)'''
|
||||
|
||||
filename = xml_object.get('filename')
|
||||
if filename is None:
|
||||
definition_xml = copy.deepcopy(xml_object)
|
||||
else:
|
||||
filepath = cls._format_filepath(xml_object.tag, filename)
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): If the file doesn't exist at the right path,
|
||||
# give the class a chance to fix it up. The file will be written out
|
||||
# again in the correct format. This should go away once the CMS is
|
||||
# online and has imported all current (fall 2012) courses from xml
|
||||
if not system.resources_fs.exists(filepath) and hasattr(
|
||||
cls,
|
||||
'backcompat_paths'):
|
||||
candidates = cls.backcompat_paths(filepath)
|
||||
for candidate in candidates:
|
||||
if system.resources_fs.exists(candidate):
|
||||
filepath = candidate
|
||||
break
|
||||
|
||||
try:
|
||||
with system.resources_fs.open(filepath) as file:
|
||||
definition_xml = cls.file_to_xml(file)
|
||||
except Exception:
|
||||
msg = 'Unable to load file contents at path %s for item %s' % (
|
||||
filepath, location.url())
|
||||
# Add info about where we are, but keep the traceback
|
||||
raise Exception, msg, sys.exc_info()[2]
|
||||
|
||||
cls.clean_metadata_from_xml(definition_xml)
|
||||
return cls.definition_from_xml(definition_xml, system)
|
||||
|
||||
|
||||
@classmethod
|
||||
def from_xml(cls, xml_data, system, org=None, course=None):
|
||||
"""
|
||||
@@ -180,7 +156,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
slug = xml_object.get('url_name', xml_object.get('slug'))
|
||||
location = Location('i4x', org, course, xml_object.tag, slug)
|
||||
|
||||
def metadata_loader():
|
||||
def load_metadata():
|
||||
metadata = {}
|
||||
for attr in cls.metadata_attributes:
|
||||
val = xml_object.get(attr)
|
||||
@@ -192,49 +168,15 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
metadata[attr_map.metadata_key] = attr_map.to_metadata(val)
|
||||
return metadata
|
||||
|
||||
def definition_loader():
|
||||
filename = xml_object.get('filename')
|
||||
if filename is None:
|
||||
definition_xml = copy.deepcopy(xml_object)
|
||||
else:
|
||||
filepath = cls._format_filepath(xml_object.tag, filename)
|
||||
|
||||
# VS[compat]
|
||||
# TODO (cpennington): If the file doesn't exist at the right path,
|
||||
# give the class a chance to fix it up. The file will be written out again
|
||||
# in the correct format.
|
||||
# This should go away once the CMS is online and has imported all current (fall 2012)
|
||||
# courses from xml
|
||||
if not system.resources_fs.exists(filepath) and hasattr(cls, 'backcompat_paths'):
|
||||
candidates = cls.backcompat_paths(filepath)
|
||||
for candidate in candidates:
|
||||
if system.resources_fs.exists(candidate):
|
||||
filepath = candidate
|
||||
break
|
||||
|
||||
try:
|
||||
with system.resources_fs.open(filepath) as file:
|
||||
definition_xml = cls.file_to_xml(file)
|
||||
except (ResourceNotFoundError, etree.XMLSyntaxError):
|
||||
msg = 'Unable to load file contents at path %s for item %s' % (filepath, location.url())
|
||||
log.exception(msg)
|
||||
system.error_handler(msg)
|
||||
# if error_handler didn't reraise, work around problem.
|
||||
error_elem = etree.Element('error')
|
||||
message_elem = etree.SubElement(error_elem, 'error_message')
|
||||
message_elem.text = msg
|
||||
stack_elem = etree.SubElement(error_elem, 'stack_trace')
|
||||
stack_elem.text = traceback.format_exc()
|
||||
return {'data': etree.tostring(error_elem)}
|
||||
|
||||
cls.clean_metadata_from_xml(definition_xml)
|
||||
return cls.definition_from_xml(definition_xml, system)
|
||||
|
||||
definition = cls.load_definition(xml_object, system, location)
|
||||
metadata = load_metadata()
|
||||
# VS[compat] -- just have the url_name lookup once translation is done
|
||||
slug = xml_object.get('url_name', xml_object.get('slug'))
|
||||
return cls(
|
||||
system,
|
||||
LazyLoadingDict(definition_loader),
|
||||
definition,
|
||||
location=location,
|
||||
metadata=LazyLoadingDict(metadata_loader),
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
@classmethod
|
||||
@@ -282,7 +224,7 @@ class XmlDescriptor(XModuleDescriptor):
|
||||
|
||||
# Write it to a file if necessary
|
||||
if self.split_to_file(xml_object):
|
||||
# Put this object in it's own file
|
||||
# Put this object in its own file
|
||||
filepath = self.__class__._format_filepath(self.category, self.name)
|
||||
resource_fs.makedir(os.path.dirname(filepath), allow_recreate=True)
|
||||
with resource_fs.open(filepath, 'w') as file:
|
||||
|
||||
@@ -33,6 +33,7 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
|
||||
try:
|
||||
course_loc = CourseDescriptor.id_to_location(course_id)
|
||||
course = modulestore().get_item(course_loc)
|
||||
|
||||
except (KeyError, ItemNotFoundError):
|
||||
raise Http404("Course not found.")
|
||||
|
||||
|
||||
@@ -10,37 +10,17 @@ from lxml import etree
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
from xmodule.modulestore.xml import XMLModuleStore
|
||||
|
||||
from xmodule.errortracker import make_error_tracker
|
||||
|
||||
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.
|
||||
@@ -73,14 +53,14 @@ def import_with_checks(course_dir, verbose=True):
|
||||
data_dir = course_dir.dirname()
|
||||
course_dirs = [course_dir.basename()]
|
||||
|
||||
(error_handler, errors) = make_logging_error_handler()
|
||||
(error_tracker, errors) = make_error_tracker()
|
||||
# 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)
|
||||
error_tracker=error_tracker)
|
||||
|
||||
def str_of_err(tpl):
|
||||
(msg, exc_info) = tpl
|
||||
@@ -143,6 +123,7 @@ def check_roundtrip(course_dir):
|
||||
# dircmp doesn't do recursive diffs.
|
||||
# diff = dircmp(course_dir, export_dir, ignore=[], hide=[])
|
||||
print "======== Roundtrip diff: ========="
|
||||
sys.stdout.flush() # needed to make diff appear in the right place
|
||||
os.system("diff -r {0} {1}".format(course_dir, export_dir))
|
||||
print "======== ideally there is no diff above this ======="
|
||||
|
||||
|
||||
@@ -172,6 +172,7 @@ def get_module(user, request, location, student_module_cache, position=None):
|
||||
# a module is coming through get_html and is therefore covered
|
||||
# by the replace_static_urls code below
|
||||
replace_urls=replace_urls,
|
||||
is_staff=user.is_staff,
|
||||
)
|
||||
# pass position specified in URL to module through ModuleSystem
|
||||
system.set('position', position)
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
import copy
|
||||
import json
|
||||
from path import path
|
||||
import os
|
||||
|
||||
from pprint import pprint
|
||||
from nose import SkipTest
|
||||
|
||||
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 mock import patch, Mock
|
||||
from override_settings import override_settings
|
||||
|
||||
from student.models import Registration
|
||||
from django.contrib.auth.models import User
|
||||
from student.models import Registration
|
||||
|
||||
from xmodule.modulestore.django import modulestore
|
||||
import xmodule.modulestore.django
|
||||
@@ -189,11 +190,12 @@ class RealCoursesLoadTestCase(PageLoader):
|
||||
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):
|
||||
def test_real_courses_loads(self):
|
||||
'''See if any real courses are available at the REAL_DATA_DIR.
|
||||
If they are, check them.'''
|
||||
|
||||
# TODO: Disabled test for now.. Fix once things are cleaned up.
|
||||
raise SkipTest
|
||||
# TODO: adjust staticfiles_dirs
|
||||
if not os.path.isdir(REAL_DATA_DIR):
|
||||
# No data present. Just pass.
|
||||
|
||||
@@ -29,6 +29,7 @@ from multicourse import multicourse_settings
|
||||
from django_comment_client.utils import get_discussion_title
|
||||
|
||||
from xmodule.modulestore import Location
|
||||
from xmodule.modulestore.search import path_to_location
|
||||
from xmodule.modulestore.exceptions import InvalidLocationError, ItemNotFoundError, NoPathToItem
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from xmodule.course_module import CourseDescriptor
|
||||
@@ -68,14 +69,20 @@ def user_groups(user):
|
||||
|
||||
|
||||
def format_url_params(params):
|
||||
return [urllib.quote(string.replace(' ', '_')) for string in params]
|
||||
return [urllib.quote(string.replace(' ', '_'))
|
||||
if string is not None else None
|
||||
for string in params]
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_if_anonymous
|
||||
def courses(request):
|
||||
# TODO: Clean up how 'error' is done.
|
||||
courses = sorted(modulestore().get_courses(), key=lambda course: course.number)
|
||||
|
||||
# filter out any courses that errored.
|
||||
courses = [c for c in modulestore().get_courses()
|
||||
if isinstance(c, CourseDescriptor)]
|
||||
courses = sorted(courses, key=lambda course: course.number)
|
||||
universities = defaultdict(list)
|
||||
for course in courses:
|
||||
universities[course.org].append(course)
|
||||
@@ -197,34 +204,57 @@ def index(request, course_id, chapter=None, section=None,
|
||||
chapter = clean(chapter)
|
||||
section = clean(section)
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'accordion': render_accordion(request, course, chapter, section),
|
||||
'COURSE_TITLE': course.title,
|
||||
'course': course,
|
||||
'init': '',
|
||||
'content': ''
|
||||
}
|
||||
try:
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'accordion': render_accordion(request, course, chapter, section),
|
||||
'COURSE_TITLE': course.title,
|
||||
'course': course,
|
||||
'init': '',
|
||||
'content': ''
|
||||
}
|
||||
|
||||
look_for_module = chapter is not None and section is not None
|
||||
if look_for_module:
|
||||
# TODO (cpennington): Pass the right course in here
|
||||
look_for_module = chapter is not None and section is not None
|
||||
if look_for_module:
|
||||
# TODO (cpennington): Pass the right course in here
|
||||
|
||||
section_descriptor = get_section(course, chapter, section)
|
||||
if section_descriptor is not None:
|
||||
student_module_cache = StudentModuleCache(request.user,
|
||||
section_descriptor)
|
||||
module, _, _, _ = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
student_module_cache)
|
||||
context['content'] = module.get_html()
|
||||
section_descriptor = get_section(course, chapter, section)
|
||||
if section_descriptor is not None:
|
||||
student_module_cache = StudentModuleCache(request.user,
|
||||
section_descriptor)
|
||||
module, _, _, _ = get_module(request.user, request,
|
||||
section_descriptor.location,
|
||||
student_module_cache)
|
||||
context['content'] = module.get_html()
|
||||
else:
|
||||
log.warning("Couldn't find a section descriptor for course_id '{0}',"
|
||||
"chapter '{1}', section '{2}'".format(
|
||||
course_id, chapter, section))
|
||||
else:
|
||||
log.warning("Couldn't find a section descriptor for course_id '{0}',"
|
||||
"chapter '{1}', section '{2}'".format(
|
||||
course_id, chapter, section))
|
||||
if request.user.is_staff:
|
||||
# Add a list of all the errors...
|
||||
context['course_errors'] = modulestore().get_item_errors(course.location)
|
||||
|
||||
result = render_to_response('courseware.html', context)
|
||||
except:
|
||||
# In production, don't want to let a 500 out for any reason
|
||||
if settings.DEBUG:
|
||||
raise
|
||||
else:
|
||||
log.exception("Error in index view: user={user}, course={course},"
|
||||
" chapter={chapter} section={section}"
|
||||
"position={position}".format(
|
||||
user=request.user,
|
||||
course=course,
|
||||
chapter=chapter,
|
||||
section=section,
|
||||
position=position
|
||||
))
|
||||
try:
|
||||
result = render_to_response('courseware-error.html', {})
|
||||
except:
|
||||
result = HttpResponse("There was an unrecoverable error")
|
||||
|
||||
result = render_to_response('courseware.html', context)
|
||||
return result
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@@ -247,7 +277,7 @@ def jump_to(request, location):
|
||||
|
||||
# Complain if there's not data for this location
|
||||
try:
|
||||
(course_id, chapter, section, position) = modulestore().path_to_location(location)
|
||||
(course_id, chapter, section, position) = path_to_location(modulestore(), location)
|
||||
except ItemNotFoundError:
|
||||
raise Http404("No data at this location: {0}".format(location))
|
||||
except NoPathToItem:
|
||||
|
||||
@@ -3,18 +3,28 @@ from django.views.decorators.http import require_POST
|
||||
from django.http import HttpResponse
|
||||
from django.utils import simplejson
|
||||
from django.core.context_processors import csrf
|
||||
from django.core.urlresolvers import reverse
|
||||
|
||||
from mitxmako.shortcuts import render_to_response, render_to_string
|
||||
from courseware.courses import check_course
|
||||
|
||||
import comment_client
|
||||
import dateutil
|
||||
from dateutil.tz import tzlocal
|
||||
from datehelper import time_ago_in_words
|
||||
|
||||
from django_comment_client.utils import get_categorized_discussion_info
|
||||
from urllib import urlencode
|
||||
|
||||
import json
|
||||
import comment_client
|
||||
import dateutil
|
||||
|
||||
|
||||
THREADS_PER_PAGE = 20
|
||||
PAGES_NEARBY_DELTA = 2
|
||||
|
||||
class HtmlResponse(HttpResponse):
|
||||
def __init__(self, html=''):
|
||||
super(HtmlResponse, self).__init__(html, content_type='text/plain')
|
||||
|
||||
def render_accordion(request, course, discussion_id):
|
||||
|
||||
@@ -29,29 +39,58 @@ def render_accordion(request, course, discussion_id):
|
||||
|
||||
return render_to_string('discussion/_accordion.html', context)
|
||||
|
||||
def render_discussion(request, course_id, threads, discussion_id=None, with_search_bar=True, search_text='', template='discussion/_inline.html'):
|
||||
def render_discussion(request, course_id, threads, discussion_id=None, with_search_bar=True,
|
||||
search_text='', discussion_type='inline', page=1, num_pages=None,
|
||||
per_page=THREADS_PER_PAGE):
|
||||
template = {
|
||||
'inline': 'discussion/_inline.html',
|
||||
'forum': 'discussion/_forum.html',
|
||||
}[discussion_type]
|
||||
|
||||
def _url_for_inline_page(page, per_page):
|
||||
raw_url = reverse('django_comment_client.forum.views.inline_discussion', args=[course_id, discussion_id])
|
||||
return raw_url + '?' + urlencode({'page': page, 'per_page': per_page})
|
||||
|
||||
def _url_for_forum_page(page, per_page):
|
||||
raw_url = reverse('django_comment_client.forum.views.forum_form_discussion', args=[course_id, discussion_id])
|
||||
return raw_url + '?' + urlencode({'page': page, 'per_page': per_page})
|
||||
|
||||
url_for_page = {
|
||||
'inline': _url_for_inline_page,
|
||||
'forum': _url_for_forum_page,
|
||||
}[discussion_type]
|
||||
|
||||
|
||||
context = {
|
||||
'threads': threads,
|
||||
'discussion_id': discussion_id,
|
||||
'search_bar': '' if not with_search_bar \
|
||||
else render_search_bar(request, course_id, discussion_id, text=search_text),
|
||||
'user_info': comment_client.get_user_info(request.user.id, raw=True),
|
||||
'tags': comment_client.get_threads_tags(raw=True),
|
||||
'course_id': course_id,
|
||||
'request': request,
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'num_pages': num_pages,
|
||||
'pages_nearby_delta': PAGES_NEARBY_DELTA,
|
||||
'discussion_type': discussion_type,
|
||||
'url_for_page': url_for_page,
|
||||
}
|
||||
return render_to_string(template, context)
|
||||
|
||||
def render_inline_discussion(*args, **kwargs):
|
||||
return render_discussion(template='discussion/_inline.html', *args, **kwargs)
|
||||
|
||||
return render_discussion(discussion_type='inline', *args, **kwargs)
|
||||
|
||||
def render_forum_discussion(*args, **kwargs):
|
||||
return render_discussion(template='discussion/_forum.html', *args, **kwargs)
|
||||
return render_discussion(discussion_type='forum', *args, **kwargs)
|
||||
|
||||
# discussion per page is fixed for now
|
||||
def inline_discussion(request, course_id, discussion_id):
|
||||
threads = comment_client.get_threads(discussion_id, recursive=False)
|
||||
html = render_inline_discussion(request, course_id, threads, discussion_id=discussion_id)
|
||||
return HttpResponse(html, content_type="text/plain")
|
||||
page = request.GET.get('page', 1)
|
||||
threads, page, per_page, num_pages = comment_client.get_threads(discussion_id, recursive=False, page=page, per_page=THREADS_PER_PAGE)
|
||||
html = render_inline_discussion(request, course_id, threads, discussion_id=discussion_id, num_pages=num_pages, page=page, per_page=per_page)
|
||||
return HtmlResponse(html)
|
||||
|
||||
def render_search_bar(request, course_id, discussion_id=None, text=''):
|
||||
if not discussion_id:
|
||||
@@ -66,18 +105,29 @@ def render_search_bar(request, course_id, discussion_id=None, text=''):
|
||||
def forum_form_discussion(request, course_id, discussion_id):
|
||||
|
||||
course = check_course(course_id)
|
||||
|
||||
search_text = request.GET.get('text', '')
|
||||
page = request.GET.get('page', 1)
|
||||
|
||||
if len(search_text) > 0:
|
||||
threads = comment_client.search_threads({'text': search_text, 'commentable_id': discussion_id})
|
||||
threads, page, per_page, num_pages = comment_client.search_threads({
|
||||
'text': search_text,
|
||||
'commentable_id': discussion_id,
|
||||
'course_id': course_id,
|
||||
'page': page,
|
||||
'per_page': THREADS_PER_PAGE,
|
||||
})
|
||||
else:
|
||||
threads = comment_client.get_threads(discussion_id, recursive=False)
|
||||
threads, page, per_page, num_pages = comment_client.get_threads(discussion_id, recursive=False, page=page, per_page=THREADS_PER_PAGE)
|
||||
|
||||
context = {
|
||||
'csrf': csrf(request)['csrf_token'],
|
||||
'course': course,
|
||||
'content': render_forum_discussion(request, course_id, threads, discussion_id=discussion_id, search_text=search_text),
|
||||
'content': render_forum_discussion(request, course_id, threads,
|
||||
discussion_id=discussion_id,
|
||||
search_text=search_text,
|
||||
num_pages=num_pages,
|
||||
per_page=per_page,
|
||||
page=page),
|
||||
'accordion': render_accordion(request, course, discussion_id),
|
||||
}
|
||||
|
||||
@@ -102,7 +152,6 @@ def render_single_thread(request, course_id, thread_id):
|
||||
'thread': thread,
|
||||
'user_info': comment_client.get_user_info(request.user.id, raw=True),
|
||||
'annotated_content_info': json.dumps(get_annotated_content_info(thread=thread, user_id=request.user.id)),
|
||||
'tags': comment_client.get_threads_tags(raw=True),
|
||||
'course_id': course_id,
|
||||
'request': request,
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ MITX_FEATURES['DISABLE_START_DATES'] = True
|
||||
|
||||
WIKI_ENABLED = True
|
||||
|
||||
LOGGING = get_logger_config(ENV_ROOT / "log",
|
||||
LOGGING = get_logger_config(ENV_ROOT / "log",
|
||||
logging_env="dev",
|
||||
tracking_filename="tracking.log",
|
||||
debug=True)
|
||||
@@ -30,7 +30,7 @@ DATABASES = {
|
||||
}
|
||||
|
||||
CACHES = {
|
||||
# This is the cache used for most things. Askbot will not work without a
|
||||
# This is the cache used for most things. Askbot will not work without a
|
||||
# functioning cache -- it relies on caching to load its settings in places.
|
||||
# In staging/prod envs, the sessions also live here.
|
||||
'default': {
|
||||
@@ -52,11 +52,14 @@ CACHES = {
|
||||
}
|
||||
}
|
||||
|
||||
# Make the keyedcache startup warnings go away
|
||||
CACHE_TIMEOUT = 0
|
||||
|
||||
# Dummy secret key for dev
|
||||
SECRET_KEY = '85920908f28904ed733fe576320db18cabd7b6cd'
|
||||
|
||||
################################ DEBUG TOOLBAR #################################
|
||||
INSTALLED_APPS += ('debug_toolbar',)
|
||||
INSTALLED_APPS += ('debug_toolbar',)
|
||||
MIDDLEWARE_CLASSES += ('debug_toolbar.middleware.DebugToolbarMiddleware',)
|
||||
INTERNAL_IPS = ('127.0.0.1',)
|
||||
|
||||
@@ -71,8 +74,8 @@ DEBUG_TOOLBAR_PANELS = (
|
||||
'debug_toolbar.panels.logger.LoggingPanel',
|
||||
|
||||
# Enabling the profiler has a weird bug as of django-debug-toolbar==0.9.4 and
|
||||
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
|
||||
# hit twice). So you can uncomment when you need to diagnose performance
|
||||
# Django=1.3.1/1.4 where requests to views get duplicated (your method gets
|
||||
# hit twice). So you can uncomment when you need to diagnose performance
|
||||
# problems, but you shouldn't leave it on.
|
||||
# 'debug_toolbar.panels.profiling.ProfilingDebugPanel',
|
||||
)
|
||||
|
||||
@@ -15,8 +15,9 @@ class CommentClientUnknownError(CommentClientError):
|
||||
def delete_threads(commentable_id, *args, **kwargs):
|
||||
return _perform_request('delete', _url_for_commentable_threads(commentable_id), *args, **kwargs)
|
||||
|
||||
def get_threads(commentable_id, recursive=False, *args, **kwargs):
|
||||
return _perform_request('get', _url_for_threads(commentable_id), {'recursive': recursive}, *args, **kwargs)
|
||||
def get_threads(commentable_id, recursive=False, page=1, per_page=20, *args, **kwargs):
|
||||
response = _perform_request('get', _url_for_threads(commentable_id), {'recursive': recursive, 'page': page, 'per_page': per_page}, *args, **kwargs)
|
||||
return response['collection'], response['page'], response['per_page'], response['num_pages']
|
||||
|
||||
def get_threads_tags(*args, **kwargs):
|
||||
return _perform_request('get', _url_for_threads_tags(), {}, *args, **kwargs)
|
||||
@@ -98,6 +99,8 @@ def unsubscribe_commentable(user_id, commentable_id, *args, **kwargs):
|
||||
return unsubscribe(user_id, {'source_type': 'other', 'source_id': commentable_id})
|
||||
|
||||
def search_threads(attributes, *args, **kwargs):
|
||||
default_attributes = {'page': 1, 'per_page': 20}
|
||||
attributes = dict(default_attributes.items() + attributes.items())
|
||||
return _perform_request('get', _url_for_search_threads(), attributes, *args, **kwargs)
|
||||
|
||||
def _perform_request(method, url, data_or_params=None, *args, **kwargs):
|
||||
|
||||
@@ -303,7 +303,12 @@ $discussion_input_width: 90%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.discussion-paginator {
|
||||
margin-top: 40px;
|
||||
div {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -52,6 +52,19 @@
|
||||
|
||||
<section class="course-content">
|
||||
${content}
|
||||
|
||||
% if course_errors is not UNDEFINED:
|
||||
<h2>Course errors</h2>
|
||||
<div id="course-errors">
|
||||
<ul>
|
||||
% for (msg, err) in course_errors:
|
||||
<li>${msg}
|
||||
<ul><li><pre>${err}</pre></li></ul>
|
||||
</li>
|
||||
% endfor
|
||||
</ul>
|
||||
</div>
|
||||
% endif
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -13,9 +13,11 @@
|
||||
|
||||
<div class="discussion-new-post control-button" href="javascript:void(0)">New Post</div>
|
||||
</div>
|
||||
<%include file="_paginator.html" />
|
||||
% for thread in threads:
|
||||
${renderer.render_thread(course_id, thread, edit_thread=False, show_comments=False)}
|
||||
% endfor
|
||||
<%include file="_paginator.html" />
|
||||
</section>
|
||||
|
||||
<%!
|
||||
@@ -26,5 +28,4 @@
|
||||
<script type="text/javascript">
|
||||
var $$user_info = JSON.parse('${user_info | escape_quotes}');
|
||||
var $$course_id = "${course_id}";
|
||||
var $$tags = JSON.parse("${tags | escape_quotes}");
|
||||
</script>
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
</div>
|
||||
<div class="discussion-new-post control-button" href="javascript:void(0)">New Post</div>
|
||||
</div>
|
||||
<%include file="_paginator.html" />
|
||||
% for thread in threads:
|
||||
${renderer.render_thread(course_id, thread, edit_thread=False, show_comments=False)}
|
||||
% endfor
|
||||
<%include file="_paginator.html" />
|
||||
</section>
|
||||
|
||||
<%!
|
||||
@@ -20,5 +22,4 @@
|
||||
<script type="text/javascript">
|
||||
var $$user_info = JSON.parse('${user_info | escape_quotes}');
|
||||
var $$course_id = "${course_id}";
|
||||
var $$tags = JSON.parse("${tags | escape_quotes}");
|
||||
</script>
|
||||
|
||||
52
lms/templates/discussion/_paginator.html
Normal file
52
lms/templates/discussion/_paginator.html
Normal file
@@ -0,0 +1,52 @@
|
||||
<%def name="link_to_page(_page)">
|
||||
% if _page != page:
|
||||
<div class="page-link">
|
||||
<a href="${url_for_page(_page, per_page)}">${_page}</a>
|
||||
</div>
|
||||
% else:
|
||||
<div class="page-link">${_page}</div>
|
||||
% endif
|
||||
</%def>
|
||||
|
||||
<%def name="list_pages(*args)">
|
||||
% for arg in args:
|
||||
% if arg == 'dots':
|
||||
<div class="page-dots">...</div>
|
||||
% elif isinstance(arg, list):
|
||||
% for _page in arg:
|
||||
${link_to_page(_page)}
|
||||
% endfor
|
||||
% else:
|
||||
${link_to_page(arg)}
|
||||
% endif
|
||||
% endfor
|
||||
</%def>
|
||||
|
||||
<div class="discussion-${discussion_type}-paginator discussion-paginator">
|
||||
<div class="prev-page">
|
||||
% if page > 1:
|
||||
<a href="${url_for_page(page - 1, per_page)}">< Previous page</a>
|
||||
% else:
|
||||
< Previous page
|
||||
% endif
|
||||
</div>
|
||||
|
||||
% if num_pages <= 2 * pages_nearby_delta + 2:
|
||||
${list_pages(range(1, num_pages + 1))}
|
||||
% else:
|
||||
% if page <= 2 * pages_nearby_delta:
|
||||
${list_pages(range(1, 2 * pages_nearby_delta + 2), 'dots', num_pages)}
|
||||
% elif num_pages - page + 1 <= 2 * pages_nearby_delta:
|
||||
${list_pages(1, 'dots', range(num_pages - 2 * pages_nearby_delta, num_pages + 1))}
|
||||
% else:
|
||||
${list_pages(1, 'dots', range(page - pages_nearby_delta, page + pages_nearby_delta + 1), 'dots', num_pages)}
|
||||
% endif
|
||||
% endif
|
||||
<div class="next-page">
|
||||
% if page < num_pages:
|
||||
<a href="${url_for_page(page + 1, per_page)}">Next page ></a>
|
||||
% else:
|
||||
Next page >
|
||||
% endif
|
||||
</div>
|
||||
</div>
|
||||
11
lms/templates/module-error-staff.html
Normal file
11
lms/templates/module-error-staff.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<section class="outside-app">
|
||||
<h1>There has been an error on the <em>MITx</em> servers</h1>
|
||||
<p>We're sorry, this module is temporarily unavailable. Our staff is working to fix it as soon as possible. Please email us at <a href="mailto:technical@mitx.mit.edu">technical@mitx.mit.edu</a> to report any problems or downtime.</p>
|
||||
|
||||
<h1>Staff-only details below:</h1>
|
||||
|
||||
<p>Error: ${error}</p>
|
||||
|
||||
<p>Raw data: ${data}</p>
|
||||
|
||||
</section>
|
||||
Reference in New Issue
Block a user