Commit 7ab13732 by Rocky Duan

Merge branch 'master' of github.com:MITx/mitx into discussion2

parents 51961c1c 3b6f33f5
...@@ -11,33 +11,33 @@ class PostReceiveTestCase(TestCase): ...@@ -11,33 +11,33 @@ class PostReceiveTestCase(TestCase):
def setUp(self): def setUp(self):
self.client = Client() self.client = Client()
@patch('github_sync.views.sync_with_github') @patch('github_sync.views.import_from_github')
def test_non_branch(self, sync_with_github): def test_non_branch(self, import_from_github):
self.client.post('/github_service_hook', {'payload': json.dumps({ self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/tags/foo'}) 'ref': 'refs/tags/foo'})
}) })
self.assertFalse(sync_with_github.called) self.assertFalse(import_from_github.called)
@patch('github_sync.views.sync_with_github') @patch('github_sync.views.import_from_github')
def test_non_watched_repo(self, sync_with_github): def test_non_watched_repo(self, import_from_github):
self.client.post('/github_service_hook', {'payload': json.dumps({ self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/heads/branch', 'ref': 'refs/heads/branch',
'repository': {'name': 'bad_repo'}}) 'repository': {'name': 'bad_repo'}})
}) })
self.assertFalse(sync_with_github.called) self.assertFalse(import_from_github.called)
@patch('github_sync.views.sync_with_github') @patch('github_sync.views.import_from_github')
def test_non_tracked_branch(self, sync_with_github): def test_non_tracked_branch(self, import_from_github):
self.client.post('/github_service_hook', {'payload': json.dumps({ self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/heads/non_branch', 'ref': 'refs/heads/non_branch',
'repository': {'name': 'repo'}}) 'repository': {'name': 'repo'}})
}) })
self.assertFalse(sync_with_github.called) self.assertFalse(import_from_github.called)
@patch('github_sync.views.sync_with_github') @patch('github_sync.views.import_from_github')
def test_tracked_branch(self, sync_with_github): def test_tracked_branch(self, import_from_github):
self.client.post('/github_service_hook', {'payload': json.dumps({ self.client.post('/github_service_hook', {'payload': json.dumps({
'ref': 'refs/heads/branch', 'ref': 'refs/heads/branch',
'repository': {'name': 'repo'}}) 'repository': {'name': 'repo'}})
}) })
sync_with_github.assert_called_with(load_repo_settings('repo')) import_from_github.assert_called_with(load_repo_settings('repo'))
...@@ -5,7 +5,7 @@ from django.http import HttpResponse ...@@ -5,7 +5,7 @@ from django.http import HttpResponse
from django.conf import settings from django.conf import settings
from django_future.csrf import csrf_exempt from django_future.csrf import csrf_exempt
from . import sync_with_github, load_repo_settings from . import import_from_github, load_repo_settings
log = logging.getLogger() log = logging.getLogger()
...@@ -46,6 +46,6 @@ def github_post_receive(request): ...@@ -46,6 +46,6 @@ def github_post_receive(request):
log.info('Ignoring changes to non-tracked branch %s in repo %s' % (branch_name, repo_name)) log.info('Ignoring changes to non-tracked branch %s in repo %s' % (branch_name, repo_name))
return HttpResponse('Ignoring non-tracked branch') return HttpResponse('Ignoring non-tracked branch')
sync_with_github(repo) import_from_github(repo)
return HttpResponse('Push received') return HttpResponse('Push received')
...@@ -89,8 +89,8 @@ def add_histogram(get_html, module): ...@@ -89,8 +89,8 @@ def add_histogram(get_html, module):
else: else:
edit_link = False edit_link = False
staff_context = {'definition': dict(module.definition), staff_context = {'definition': json.dumps(module.definition, indent=4),
'metadata': dict(module.metadata), 'metadata': json.dumps(module.metadata, indent=4),
'element_id': module.location.html_id(), 'element_id': module.location.html_id(),
'edit_link': edit_link, 'edit_link': edit_link,
'histogram': json.dumps(histogram), 'histogram': json.dumps(histogram),
......
"""
A handy util to print a django-debug-screen-like stack trace with
values of local variables.
"""
import sys, traceback
from django.utils.encoding import smart_unicode
def supertrace(max_len=160):
"""
Print the usual traceback information, followed by a listing of all the
local variables in each frame. Should be called from an exception handler.
if max_len is not None, will print up to max_len chars for each local variable.
(cite: modified from somewhere on stackoverflow)
"""
tb = sys.exc_info()[2]
while True:
if not tb.tb_next:
break
tb = tb.tb_next
stack = []
frame = tb.tb_frame
while frame:
stack.append(f)
frame = frame.f_back
stack.reverse()
# First print the regular traceback
traceback.print_exc()
print "Locals by frame, innermost last"
for frame in stack:
print
print "Frame %s in %s at line %s" % (frame.f_code.co_name,
frame.f_code.co_filename,
frame.f_lineno)
for key, value in frame.f_locals.items():
print ("\t%20s = " % smart_unicode(key, errors='ignore')),
# We have to be careful not to cause a new error in our error
# printer! Calling str() on an unknown object could cause an
# error.
try:
s = smart_unicode(value, errors='ignore')
if max_len is not None:
s = s[:max_len]
print s
except:
print "<ERROR WHILE PRINTING VALUE>"
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
'''
Progress class for modules. Represents where a student is in a module.
Useful things to know:
- Use Progress.to_js_status_str() to convert a progress into a simple
status string to pass to js.
- Use Progress.to_js_detail_str() to convert a progress into a more detailed
string to pass to js.
In particular, these functions have a canonical handing of None.
For most subclassing needs, you should only need to reimplement
frac() and __str__().
'''
from collections import namedtuple
import numbers
class Progress(object):
'''Represents a progress of a/b (a out of b done)
a and b must be numeric, but not necessarily integer, with
0 <= a <= b and b > 0.
Progress can only represent Progress for modules where that makes sense. Other
modules (e.g. html) should return None from get_progress().
TODO: add tag for module type? Would allow for smarter merging.
'''
def __init__(self, a, b):
'''Construct a Progress object. a and b must be numbers, and must have
0 <= a <= b and b > 0
'''
# Want to do all checking at construction time, so explicitly check types
if not (isinstance(a, numbers.Number) and
isinstance(b, numbers.Number)):
raise TypeError('a and b must be numbers. Passed {0}/{1}'.format(a, b))
if not (0 <= a <= b and b > 0):
raise ValueError(
'fraction a/b = {0}/{1} must have 0 <= a <= b and b > 0'.format(a, b))
self._a = a
self._b = b
def frac(self):
''' Return tuple (a,b) representing progress of a/b'''
return (self._a, self._b)
def percent(self):
''' Returns a percentage progress as a float between 0 and 100.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
(a, b) = self.frac()
return 100.0 * a / b
def started(self):
''' Returns True if fractional progress is greater than 0.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
return self.frac()[0] > 0
def inprogress(self):
''' Returns True if fractional progress is strictly between 0 and 1.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
(a, b) = self.frac()
return a > 0 and a < b
def done(self):
''' Return True if this represents done.
subclassing note: implemented in terms of frac(), assumes sanity
checking is done at construction time.
'''
(a, b) = self.frac()
return a == b
def ternary_str(self):
''' Return a string version of this progress: either
"none", "in_progress", or "done".
subclassing note: implemented in terms of frac()
'''
(a, b) = self.frac()
if a == 0:
return "none"
if a < b:
return "in_progress"
return "done"
def __eq__(self, other):
''' Two Progress objects are equal if they have identical values.
Implemented in terms of frac()'''
if not isinstance(other, Progress):
return False
(a, b) = self.frac()
(a2, b2) = other.frac()
return a == a2 and b == b2
def __ne__(self, other):
''' The opposite of equal'''
return not self.__eq__(other)
def __str__(self):
''' Return a string representation of this string.
subclassing note: implemented in terms of frac().
'''
(a, b) = self.frac()
return "{0}/{1}".format(a, b)
@staticmethod
def add_counts(a, b):
'''Add two progress indicators, assuming that each represents items done:
(a / b) + (c / d) = (a + c) / (b + d).
If either is None, returns the other.
'''
if a is None:
return b
if b is None:
return a
# get numerators + denominators
(n, d) = a.frac()
(n2, d2) = b.frac()
return Progress(n + n2, d + d2)
@staticmethod
def to_js_status_str(progress):
'''
Return the "status string" version of the passed Progress
object that should be passed to js. Use this function when
sending Progress objects to js to limit dependencies.
'''
if progress is None:
return "NA"
return progress.ternary_str()
@staticmethod
def to_js_detail_str(progress):
'''
Return the "detail string" version of the passed Progress
object that should be passed to js. Use this function when
passing Progress objects to js to limit dependencies.
'''
if progress is None:
return "NA"
return str(progress)
...@@ -20,5 +20,10 @@ class EditingDescriptor(MakoModuleDescriptor): ...@@ -20,5 +20,10 @@ class EditingDescriptor(MakoModuleDescriptor):
def get_context(self): def get_context(self):
return { return {
'module': self, 'module': self,
'data': self.definition['data'], 'data': self.definition.get('data', ''),
# TODO (vshnayder): allow children and metadata to be edited.
#'children' : self.definition.get('children, ''),
# TODO: show both own metadata and inherited?
#'metadata' : self.own_metadata,
} }
import sys
import logging
from pkg_resources import resource_string from pkg_resources import resource_string
from lxml import etree from lxml import etree
from xmodule.x_module import XModule from xmodule.x_module import XModule
from xmodule.mako_module import MakoModuleDescriptor from xmodule.mako_module import MakoModuleDescriptor
from xmodule.xml_module import XmlDescriptor from xmodule.xml_module import XmlDescriptor
from xmodule.editing_module import EditingDescriptor from xmodule.editing_module import EditingDescriptor
from xmodule.errortracker import exc_info_to_str
import logging
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -14,14 +17,11 @@ class ErrorModule(XModule): ...@@ -14,14 +17,11 @@ class ErrorModule(XModule):
'''Show an error. '''Show an error.
TODO (vshnayder): proper style, divs, etc. 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 # staff get to see all the details
return self.system.render_template('module-error-staff.html', { return self.system.render_template('module-error.html', {
'data' : self.definition['data'], 'data' : self.definition['data']['contents'],
# TODO (vshnayder): need to get non-syntax errors in here somehow 'error' : self.definition['data']['error_msg'],
'error' : self.definition.get('error', 'Error not available') 'is_staff' : self.system.is_staff,
}) })
class ErrorDescriptor(EditingDescriptor): class ErrorDescriptor(EditingDescriptor):
...@@ -31,29 +31,36 @@ class ErrorDescriptor(EditingDescriptor): ...@@ -31,29 +31,36 @@ class ErrorDescriptor(EditingDescriptor):
module_class = ErrorModule module_class = ErrorModule
@classmethod @classmethod
def from_xml(cls, xml_data, system, org=None, course=None, err=None): def from_xml(cls, xml_data, system, org=None, course=None,
error_msg='Error not available'):
'''Create an instance of this descriptor from the supplied data. '''Create an instance of this descriptor from the supplied data.
Does not try to parse the data--just stores it. Does not try to parse the data--just stores it.
Takes an extra, optional, parameter--the error that caused an Takes an extra, optional, parameter--the error that caused an
issue. issue. (should be a string, or convert usefully into one).
''' '''
# Use a nested inner dictionary because 'data' is hardcoded
definition = {} inner = {}
if err is not None: definition = {'data': inner}
definition['error'] = err inner['error_msg'] = str(error_msg)
try: try:
# If this is already an error tag, don't want to re-wrap it. # If this is already an error tag, don't want to re-wrap it.
xml_obj = etree.fromstring(xml_data) xml_obj = etree.fromstring(xml_data)
if xml_obj.tag == 'error': if xml_obj.tag == 'error':
xml_data = xml_obj.text xml_data = xml_obj.text
except etree.XMLSyntaxError as err: error_node = xml_obj.find('error_msg')
if error_node is not None:
inner['error_msg'] = error_node.text
else:
inner['error_msg'] = 'Error not available'
except etree.XMLSyntaxError:
# Save the error to display later--overrides other problems # Save the error to display later--overrides other problems
definition['error'] = err inner['error_msg'] = exc_info_to_str(sys.exc_info())
definition['data'] = xml_data inner['contents'] = xml_data
# TODO (vshnayder): Do we need a unique slug here? Just pick a random # TODO (vshnayder): Do we need a unique slug here? Just pick a random
# 64-bit num? # 64-bit num?
location = ['i4x', org, course, 'error', 'slug'] location = ['i4x', org, course, 'error', 'slug']
...@@ -71,10 +78,12 @@ class ErrorDescriptor(EditingDescriptor): ...@@ -71,10 +78,12 @@ class ErrorDescriptor(EditingDescriptor):
files, etc. That would just get re-wrapped on import. files, etc. That would just get re-wrapped on import.
''' '''
try: try:
xml = etree.fromstring(self.definition['data']) xml = etree.fromstring(self.definition['data']['contents'])
return etree.tostring(xml) return etree.tostring(xml)
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
# still not valid. # still not valid.
root = etree.Element('error') root = etree.Element('error')
root.text = self.definition['data'] root.text = self.definition['data']['contents']
err_node = etree.SubElement(root, 'error_msg')
err_node.text = self.definition['data']['error_msg']
return etree.tostring(root) return etree.tostring(root)
...@@ -8,6 +8,12 @@ log = logging.getLogger(__name__) ...@@ -8,6 +8,12 @@ log = logging.getLogger(__name__)
ErrorLog = namedtuple('ErrorLog', 'tracker errors') ErrorLog = namedtuple('ErrorLog', 'tracker errors')
def exc_info_to_str(exc_info):
"""Given some exception info, convert it into a string using
the traceback.format_exception() function.
"""
return ''.join(traceback.format_exception(*exc_info))
def in_exception_handler(): def in_exception_handler():
'''Is there an active exception?''' '''Is there an active exception?'''
return sys.exc_info() != (None, None, None) return sys.exc_info() != (None, None, None)
...@@ -27,7 +33,7 @@ def make_error_tracker(): ...@@ -27,7 +33,7 @@ def make_error_tracker():
'''Log errors''' '''Log errors'''
exc_str = '' exc_str = ''
if in_exception_handler(): if in_exception_handler():
exc_str = ''.join(traceback.format_exception(*sys.exc_info())) exc_str = exc_info_to_str(sys.exc_info())
errors.append((msg, exc_str)) errors.append((msg, exc_str))
......
...@@ -5,11 +5,11 @@ import os ...@@ -5,11 +5,11 @@ import os
import sys import sys
from lxml import etree from lxml import etree
from xmodule.x_module import XModule from .x_module import XModule
from xmodule.xml_module import XmlDescriptor from .xml_module import XmlDescriptor
from xmodule.editing_module import EditingDescriptor from .editing_module import EditingDescriptor
from stringify import stringify_children from .stringify import stringify_children
from html_checker import check_html from .html_checker import check_html
log = logging.getLogger("mitx.courseware") log = logging.getLogger("mitx.courseware")
......
...@@ -3,10 +3,13 @@ This module provides an abstraction for working with XModuleDescriptors ...@@ -3,10 +3,13 @@ This module provides an abstraction for working with XModuleDescriptors
that are stored in a database an accessible using their Location as an identifier that are stored in a database an accessible using their Location as an identifier
""" """
import logging
import re import re
from collections import namedtuple from collections import namedtuple
from .exceptions import InvalidLocationError, InsufficientSpecificationError from .exceptions import InvalidLocationError, InsufficientSpecificationError
import logging from xmodule.errortracker import ErrorLog, make_error_tracker
log = logging.getLogger('mitx.' + 'modulestore') log = logging.getLogger('mitx.' + 'modulestore')
...@@ -290,3 +293,38 @@ class ModuleStore(object): ...@@ -290,3 +293,38 @@ class ModuleStore(object):
''' '''
raise NotImplementedError raise NotImplementedError
class ModuleStoreBase(ModuleStore):
'''
Implement interface functionality that can be shared.
'''
def __init__(self):
'''
Set up the error-tracking logic.
'''
self._location_errors = {} # location -> ErrorLog
def _get_errorlog(self, location):
"""
If we already have an errorlog for this location, return it. Otherwise,
create one.
"""
location = Location(location)
if location not in self._location_errors:
self._location_errors[location] = make_error_tracker()
return self._location_errors[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: For now, the only items that track errors are CourseDescriptors in
the xml datastore. This will return an empty list for all other items
and datastores.
"""
# check that item is present and raise the promised exceptions if needed
self.get_item(location)
errorlog = self._get_errorlog(location)
return errorlog.errors
...@@ -11,7 +11,7 @@ from xmodule.x_module import XModuleDescriptor ...@@ -11,7 +11,7 @@ from xmodule.x_module import XModuleDescriptor
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from mitxmako.shortcuts import render_to_string from mitxmako.shortcuts import render_to_string
from . import ModuleStore, Location from . import ModuleStoreBase, Location
from .exceptions import (ItemNotFoundError, from .exceptions import (ItemNotFoundError,
NoPathToItem, DuplicateItemError) NoPathToItem, DuplicateItemError)
...@@ -38,7 +38,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem): ...@@ -38,7 +38,7 @@ class CachingDescriptorSystem(MakoDescriptorSystem):
resources_fs: a filesystem, as per MakoDescriptorSystem resources_fs: a filesystem, as per MakoDescriptorSystem
error_tracker: error_tracker: a function that logs errors for later display to users
render_template: a function for rendering templates, as per render_template: a function for rendering templates, as per
MakoDescriptorSystem MakoDescriptorSystem
...@@ -73,7 +73,7 @@ def location_to_query(location): ...@@ -73,7 +73,7 @@ def location_to_query(location):
return query return query
class MongoModuleStore(ModuleStore): class MongoModuleStore(ModuleStoreBase):
""" """
A Mongodb backed ModuleStore A Mongodb backed ModuleStore
""" """
...@@ -81,6 +81,9 @@ class MongoModuleStore(ModuleStore): ...@@ -81,6 +81,9 @@ class MongoModuleStore(ModuleStore):
# TODO (cpennington): Enable non-filesystem filestores # 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): error_tracker=null_error_tracker):
ModuleStoreBase.__init__(self)
self.collection = pymongo.connection.Connection( self.collection = pymongo.connection.Connection(
host=host, host=host,
port=port port=port
......
...@@ -12,7 +12,7 @@ from xmodule.course_module import CourseDescriptor ...@@ -12,7 +12,7 @@ from xmodule.course_module import CourseDescriptor
from xmodule.mako_module import MakoDescriptorSystem from xmodule.mako_module import MakoDescriptorSystem
from cStringIO import StringIO from cStringIO import StringIO
from . import ModuleStore, Location from . import ModuleStoreBase, Location
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
etree.set_default_parser( etree.set_default_parser(
...@@ -98,7 +98,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -98,7 +98,7 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
error_tracker, process_xml, **kwargs) error_tracker, process_xml, **kwargs)
class XMLModuleStore(ModuleStore): class XMLModuleStore(ModuleStoreBase):
""" """
An XML backed ModuleStore An XML backed ModuleStore
""" """
...@@ -118,13 +118,12 @@ class XMLModuleStore(ModuleStore): ...@@ -118,13 +118,12 @@ class XMLModuleStore(ModuleStore):
course_dirs: If specified, the list of course_dirs to load. Otherwise, course_dirs: If specified, the list of course_dirs to load. Otherwise,
load all course dirs load all course dirs
""" """
ModuleStoreBase.__init__(self)
self.eager = eager self.eager = eager
self.data_dir = path(data_dir) self.data_dir = path(data_dir)
self.modules = {} # location -> XModuleDescriptor self.modules = {} # location -> XModuleDescriptor
self.courses = {} # course_dir -> XModuleDescriptor for the course self.courses = {} # course_dir -> XModuleDescriptor for the course
self.location_errors = {} # location -> ErrorLog
if default_class is None: if default_class is None:
self.default_class = None self.default_class = None
...@@ -148,12 +147,14 @@ class XMLModuleStore(ModuleStore): ...@@ -148,12 +147,14 @@ class XMLModuleStore(ModuleStore):
for course_dir in course_dirs: for course_dir in course_dirs:
try: try:
# make a tracker, then stick in the right place once the course loads # Special-case code here, since we don't have a location for the
# and we know its location # course before it loads.
# So, make a tracker to track load-time errors, then put in the right
# place after the course loads and we have its location
errorlog = make_error_tracker() errorlog = make_error_tracker()
course_descriptor = self.load_course(course_dir, errorlog.tracker) course_descriptor = self.load_course(course_dir, errorlog.tracker)
self.courses[course_dir] = course_descriptor self.courses[course_dir] = course_descriptor
self.location_errors[course_descriptor.location] = errorlog self._location_errors[course_descriptor.location] = errorlog
except: except:
msg = "Failed to load course '%s'" % course_dir msg = "Failed to load course '%s'" % course_dir
log.exception(msg) log.exception(msg)
...@@ -221,23 +222,6 @@ class XMLModuleStore(ModuleStore): ...@@ -221,23 +222,6 @@ class XMLModuleStore(ModuleStore):
raise ItemNotFoundError(location) 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): def get_courses(self, depth=0):
""" """
Returns a list of course descriptors. If there were errors on loading, Returns a list of course descriptors. If there were errors on loading,
...@@ -245,9 +229,11 @@ class XMLModuleStore(ModuleStore): ...@@ -245,9 +229,11 @@ class XMLModuleStore(ModuleStore):
""" """
return self.courses.values() return self.courses.values()
def create_item(self, location): def create_item(self, location):
raise NotImplementedError("XMLModuleStores are read-only") raise NotImplementedError("XMLModuleStores are read-only")
def update_item(self, location, data): def update_item(self, location, data):
""" """
Set the data in the item specified by the location to Set the data in the item specified by the location to
...@@ -258,6 +244,7 @@ class XMLModuleStore(ModuleStore): ...@@ -258,6 +244,7 @@ class XMLModuleStore(ModuleStore):
""" """
raise NotImplementedError("XMLModuleStores are read-only") raise NotImplementedError("XMLModuleStores are read-only")
def update_children(self, location, children): def update_children(self, location, children):
""" """
Set the children for the item specified by the location to Set the children for the item specified by the location to
...@@ -268,6 +255,7 @@ class XMLModuleStore(ModuleStore): ...@@ -268,6 +255,7 @@ class XMLModuleStore(ModuleStore):
""" """
raise NotImplementedError("XMLModuleStores are read-only") raise NotImplementedError("XMLModuleStores are read-only")
def update_metadata(self, location, metadata): def update_metadata(self, location, metadata):
""" """
Set the metadata for the item specified by the location to Set the metadata for the item specified by the location to
......
...@@ -73,6 +73,7 @@ class ImportTestCase(unittest.TestCase): ...@@ -73,6 +73,7 @@ class ImportTestCase(unittest.TestCase):
def test_reimport(self): def test_reimport(self):
'''Make sure an already-exported error xml tag loads properly''' '''Make sure an already-exported error xml tag loads properly'''
self.maxDiff = None
bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>''' bad_xml = '''<sequential display_name="oops"><video url="hi"></sequential>'''
system = self.get_system() system = self.get_system()
descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course', descriptor = XModuleDescriptor.load_from_xml(bad_xml, system, 'org', 'course',
......
from nose.tools import assert_equals from nose.tools import assert_equals
from lxml import etree from lxml import etree
from stringify import stringify_children from xmodule.stringify import stringify_children
def test_stringify(): def test_stringify():
html = '''<html a="b" foo="bar">Hi <div x="foo">there <span>Bruce</span><b>!</b></div></html>''' text = 'Hi <div x="foo">there <span>Bruce</span><b>!</b></div>'
html = '''<html a="b" foo="bar">{0}</html>'''.format(text)
xml = etree.fromstring(html) xml = etree.fromstring(html)
out = stringify_children(xml) out = stringify_children(xml)
assert_equals(out, '''Hi <div x="foo">there <span>Bruce</span><b>!</b></div>''') assert_equals(out, text)
from lxml import etree
from lxml.etree import XMLSyntaxError
import pkg_resources
import logging import logging
import pkg_resources
import sys
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
from functools import partial from functools import partial
from lxml import etree
from lxml.etree import XMLSyntaxError
from xmodule.modulestore import Location from xmodule.modulestore import Location
from xmodule.errortracker import exc_info_to_str
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
...@@ -471,7 +473,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -471,7 +473,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
msg = "Error loading from xml." msg = "Error loading from xml."
log.exception(msg) log.exception(msg)
system.error_tracker(msg) system.error_tracker(msg)
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course, err) err_msg = msg + "\n" + exc_info_to_str(sys.exc_info())
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course,
err_msg)
return descriptor return descriptor
......
...@@ -131,7 +131,7 @@ def profile(request, course_id, student_id=None): ...@@ -131,7 +131,7 @@ def profile(request, course_id, student_id=None):
student_module_cache = StudentModuleCache(request.user, course) student_module_cache = StudentModuleCache(request.user, course)
course_module, _, _, _ = get_module(request.user, request, course.location, student_module_cache) course_module, _, _, _ = get_module(request.user, request, course.location, student_module_cache)
context = {'name': user_info.name, context = {'name': user_info.name,
'username': student.username, 'username': student.username,
'location': user_info.location, 'location': user_info.location,
...@@ -257,6 +257,7 @@ def index(request, course_id, chapter=None, section=None, ...@@ -257,6 +257,7 @@ def index(request, course_id, chapter=None, section=None,
return result return result
@ensure_csrf_cookie @ensure_csrf_cookie
def jump_to(request, location): def jump_to(request, location):
''' '''
...@@ -283,7 +284,7 @@ def jump_to(request, location): ...@@ -283,7 +284,7 @@ def jump_to(request, location):
except NoPathToItem: except NoPathToItem:
raise Http404("This location is not in any class: {0}".format(location)) raise Http404("This location is not in any class: {0}".format(location))
# Rely on index to do all error handling
return index(request, course_id, chapter, section, position) return index(request, course_id, chapter, section, position)
@ensure_csrf_cookie @ensure_csrf_cookie
......
<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>
<section class="outside-app"> <section class="outside-app">
<h1>There has been an error on the <em>MITx</em> servers</h1> <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> <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>
% if is_staff:
<h1>Staff-only details below:</h1>
<p>Error: ${error | h}</p>
<p>Raw data: ${data | h}</p>
% endif
</section> </section>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment