Commit 32253510 by Victor Shnayder

Import error cleanup

* call error tracker when needed
* remove duplicate logging--just add info and re-raise
* xml modulestore uses error tracker to capture load errors
* add unstyled list of import errors to courseware homepage!
parent 463b7584
...@@ -35,12 +35,13 @@ def process_includes(fn): ...@@ -35,12 +35,13 @@ def process_includes(fn):
# insert new XML into tree in place of include # insert new XML into tree in place of include
parent.insert(parent.index(next_include), incxml) parent.insert(parent.index(next_include), incxml)
except Exception: except Exception:
# Log error and work around it # Log error
msg = "Error in problem xml include: %s" % ( msg = "Error in problem xml include: %s" % (
etree.tostring(next_include, pretty_print=True)) etree.tostring(next_include, pretty_print=True))
# tell the tracker
system.error_tracker(msg) system.error_tracker(msg)
# work around
parent = next_include.getparent() parent = next_include.getparent()
errorxml = etree.Element('error') errorxml = etree.Element('error')
messagexml = etree.SubElement(errorxml, 'message') messagexml = etree.SubElement(errorxml, 'message')
......
...@@ -5,6 +5,7 @@ import json ...@@ -5,6 +5,7 @@ import json
import logging import logging
import traceback import traceback
import re import re
import sys
from datetime import timedelta from datetime import timedelta
from lxml import etree from lxml import etree
...@@ -91,7 +92,8 @@ class CapaModule(XModule): ...@@ -91,7 +92,8 @@ class CapaModule(XModule):
display_due_date_string = self.metadata.get('due', None) display_due_date_string = self.metadata.get('due', None)
if display_due_date_string is not None: if display_due_date_string is not None:
self.display_due_date = dateutil.parser.parse(display_due_date_string) 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: else:
self.display_due_date = None self.display_due_date = None
...@@ -99,7 +101,8 @@ class CapaModule(XModule): ...@@ -99,7 +101,8 @@ class CapaModule(XModule):
if grace_period_string is not None and self.display_due_date: if grace_period_string is not None and self.display_due_date:
self.grace_period = parse_timedelta(grace_period_string) self.grace_period = parse_timedelta(grace_period_string)
self.close_date = self.display_due_date + self.grace_period 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: else:
self.grace_period = None self.grace_period = None
self.close_date = self.display_due_date self.close_date = self.display_due_date
...@@ -138,10 +141,16 @@ class CapaModule(XModule): ...@@ -138,10 +141,16 @@ class CapaModule(XModule):
try: try:
self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(), self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
instance_state, seed=seed, system=self.system) instance_state, seed=seed, system=self.system)
except Exception: except Exception as err:
msg = 'cannot create LoncapaProblem %s' % self.location.url() msg = 'cannot create LoncapaProblem {loc}: {err}'.format(
log.exception(msg) 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: 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('<', '&lt;') msg = '<p>%s</p>' % msg.replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;') msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
# create a dummy problem with error message instead of failing # create a dummy problem with error message instead of failing
...@@ -152,7 +161,8 @@ class CapaModule(XModule): ...@@ -152,7 +161,8 @@ class CapaModule(XModule):
problem_text, self.location.html_id(), problem_text, self.location.html_id(),
instance_state, seed=seed, system=self.system) instance_state, seed=seed, system=self.system)
else: else:
raise # add extra info and raise
raise Exception(msg), None, sys.exc_info()[2]
@property @property
def rerandomize(self): def rerandomize(self):
...@@ -191,6 +201,7 @@ class CapaModule(XModule): ...@@ -191,6 +201,7 @@ class CapaModule(XModule):
try: try:
return Progress(score, total) return Progress(score, total)
except Exception as err: except Exception as err:
# TODO (vshnayder): why is this still here? still needed?
if self.system.DEBUG: if self.system.DEBUG:
return None return None
raise raise
...@@ -210,6 +221,7 @@ class CapaModule(XModule): ...@@ -210,6 +221,7 @@ class CapaModule(XModule):
try: try:
html = self.lcp.get_html() html = self.lcp.get_html()
except Exception, err: except Exception, err:
# TODO (vshnayder): another switch on DEBUG.
if self.system.DEBUG: if self.system.DEBUG:
log.exception(err) log.exception(err)
msg = ( msg = (
...@@ -561,6 +573,7 @@ class CapaDescriptor(RawDescriptor): ...@@ -561,6 +573,7 @@ class CapaDescriptor(RawDescriptor):
'problems/' + path[8:], 'problems/' + path[8:],
path[8:], path[8:],
] ]
@classmethod @classmethod
def split_to_file(cls, xml_object): def split_to_file(cls, xml_object):
'''Problems always written in their own files''' '''Problems always written in their own files'''
......
...@@ -20,15 +20,22 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -20,15 +20,22 @@ class CourseDescriptor(SequenceDescriptor):
self._grader = None self._grader = None
self._grade_cutoffs = None self._grade_cutoffs = None
msg = None
try: try:
self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M") self.start = time.strptime(self.metadata["start"], "%Y-%m-%dT%H:%M")
except KeyError: except KeyError:
self.start = time.gmtime(0) #The epoch self.start = time.gmtime(0) #The epoch
log.critical("Course loaded without a start date. %s", self.id) msg = "Course loaded without a start date. id = %s" % self.id
log.critical(msg)
except ValueError as e: except ValueError as e:
self.start = time.gmtime(0) #The epoch self.start = time.gmtime(0) #The epoch
log.critical("Course loaded with a bad start date. %s '%s'", msg = "Course loaded with a bad start date. %s '%s'" % (self.id, e)
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): def has_started(self):
return time.gmtime() > self.start return time.gmtime() > self.start
...@@ -104,3 +111,4 @@ class CourseDescriptor(SequenceDescriptor): ...@@ -104,3 +111,4 @@ class CourseDescriptor(SequenceDescriptor):
@property @property
def org(self): def org(self):
return self.location.org return self.location.org
import logging import logging
import sys import sys
from collections import namedtuple
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
ErrorLog = namedtuple('ErrorLog', 'tracker errors')
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)
def make_error_tracker(): def make_error_tracker():
'''Return a tuple (logger, error_list), where '''Return an ErrorLog (named tuple), with fields (tracker, errors), where
the logger appends a tuple (message, exc_info=None) the logger appends a tuple (message, exc_info=None)
to the error_list on every call. to the errors on every call.
error_list is a simple list. If the caller messes with it, info error_list is a simple list. If the caller messes with it, info
will be lost. will be lost.
...@@ -26,7 +30,7 @@ def make_error_tracker(): ...@@ -26,7 +30,7 @@ def make_error_tracker():
errors.append((msg, exc_info)) errors.append((msg, exc_info))
return (error_tracker, errors) return ErrorLog(error_tracker, errors)
def null_error_tracker(msg): def null_error_tracker(msg):
'''A dummy error tracker that just ignores the messages''' '''A dummy error tracker that just ignores the messages'''
......
...@@ -2,6 +2,7 @@ import copy ...@@ -2,6 +2,7 @@ import copy
from fs.errors import ResourceNotFoundError from fs.errors import ResourceNotFoundError
import logging import logging
import os import os
import sys
from lxml import etree from lxml import etree
from xmodule.x_module import XModule from xmodule.x_module import XModule
...@@ -95,8 +96,8 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor): ...@@ -95,8 +96,8 @@ class HtmlDescriptor(XmlDescriptor, EditingDescriptor):
except (ResourceNotFoundError) as err: except (ResourceNotFoundError) as err:
msg = 'Unable to load file contents at path {0}: {1} '.format( msg = 'Unable to load file contents at path {0}: {1} '.format(
filepath, err) filepath, err)
log.error(msg) # add more info and re-raise
raise raise Exception(msg), None, sys.exc_info()[2]
@classmethod @classmethod
def split_to_file(cls, xml_object): def split_to_file(cls, xml_object):
......
...@@ -213,6 +213,18 @@ class ModuleStore(object): ...@@ -213,6 +213,18 @@ class ModuleStore(object):
""" """
raise NotImplementedError 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): def get_items(self, location, depth=0):
""" """
Returns a list of XModuleDescriptor instances for the items Returns a list of XModuleDescriptor instances for the items
...@@ -269,6 +281,7 @@ class ModuleStore(object): ...@@ -269,6 +281,7 @@ class ModuleStore(object):
''' '''
raise NotImplementedError raise NotImplementedError
def get_parent_locations(self, location): def get_parent_locations(self, location):
'''Find all locations that are the parents of this location. Needed '''Find all locations that are the parents of this location. Needed
for path_to_location(). for path_to_location().
......
import logging import logging
import os
import re
from fs.osfs import OSFS from fs.osfs import OSFS
from importlib import import_module from importlib import import_module
from lxml import etree from lxml import etree
from path import path from path import path
from xmodule.errortracker import null_error_tracker from xmodule.errortracker import ErrorLog, make_error_tracker
from xmodule.x_module import XModuleDescriptor, XMLParsingSystem from xmodule.x_module import XModuleDescriptor, XMLParsingSystem
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
import os
import re
from . import ModuleStore, Location from . import ModuleStore, Location
from .exceptions import ItemNotFoundError from .exceptions import ItemNotFoundError
...@@ -19,7 +21,6 @@ etree.set_default_parser( ...@@ -19,7 +21,6 @@ etree.set_default_parser(
log = logging.getLogger('mitx.' + __name__) log = logging.getLogger('mitx.' + __name__)
# VS[compat] # VS[compat]
# TODO (cpennington): Remove this once all fall 2012 courses have been imported # TODO (cpennington): Remove this once all fall 2012 courses have been imported
# into the cms from xml # into the cms from xml
...@@ -94,14 +95,12 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem): ...@@ -94,14 +95,12 @@ class ImportSystem(XMLParsingSystem, MakoDescriptorSystem):
error_tracker, process_xml, **kwargs) error_tracker, process_xml, **kwargs)
class XMLModuleStore(ModuleStore): class XMLModuleStore(ModuleStore):
""" """
An XML backed ModuleStore An XML backed ModuleStore
""" """
def __init__(self, data_dir, default_class=None, eager=False, def __init__(self, data_dir, default_class=None, eager=False,
course_dirs=None, course_dirs=None):
error_tracker=null_error_tracker):
""" """
Initialize an XMLModuleStore from data_dir Initialize an XMLModuleStore from data_dir
...@@ -115,17 +114,14 @@ class XMLModuleStore(ModuleStore): ...@@ -115,17 +114,14 @@ 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
error_tracker: The error tracker used here and in the underlying
DescriptorSystem. By default, ignore all messages.
See the comments in x_module.py:DescriptorSystem
""" """
self.eager = eager self.eager = eager
self.data_dir = path(data_dir) self.data_dir = path(data_dir)
self.modules = {} # location -> XModuleDescriptor self.modules = {} # location -> XModuleDescriptor
self.courses = {} # course_dir -> XModuleDescriptor for the course self.courses = {} # course_dir -> XModuleDescriptor for the course
self.error_tracker = error_tracker self.location_errors = {} # location -> ErrorLog
if default_class is None: if default_class is None:
self.default_class = None self.default_class = None
...@@ -149,15 +145,18 @@ class XMLModuleStore(ModuleStore): ...@@ -149,15 +145,18 @@ class XMLModuleStore(ModuleStore):
for course_dir in course_dirs: for course_dir in course_dirs:
try: 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.courses[course_dir] = course_descriptor
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)
error_tracker(msg)
def load_course(self, course_dir): def load_course(self, course_dir, tracker):
""" """
Load a course into this module store Load a course into this module store
course_path: Course directory name course_path: Course directory name
...@@ -191,13 +190,13 @@ class XMLModuleStore(ModuleStore): ...@@ -191,13 +190,13 @@ class XMLModuleStore(ModuleStore):
)) ))
course = course_dir course = course_dir
system = ImportSystem(self, org, course, course_dir, system = ImportSystem(self, org, course, course_dir, tracker)
self.error_tracker)
course_descriptor = system.process_xml(etree.tostring(course_data)) course_descriptor = system.process_xml(etree.tostring(course_data))
log.debug('========> Done with course import from {0}'.format(course_dir)) log.debug('========> Done with course import from {0}'.format(course_dir))
return course_descriptor return course_descriptor
def get_item(self, location, depth=0): def get_item(self, location, depth=0):
""" """
Returns an XModuleDescriptor instance for the item at location. Returns an XModuleDescriptor instance for the item at location.
...@@ -218,9 +217,28 @@ class XMLModuleStore(ModuleStore): ...@@ -218,9 +217,28 @@ class XMLModuleStore(ModuleStore):
except KeyError: except KeyError:
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 Returns a list of course descriptors. If there were errors on loading,
some of these may be ErrorDescriptors instead.
""" """
return self.courses.values() return self.courses.values()
......
...@@ -460,8 +460,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet): ...@@ -460,8 +460,9 @@ class XModuleDescriptor(Plugin, HTMLSnippet):
# Put import here to avoid circular import errors # Put import here to avoid circular import errors
from xmodule.error_module import ErrorDescriptor from xmodule.error_module import ErrorDescriptor
msg = "Error loading from xml."
system.error_tracker("Error loading from xml.") log.exception(msg)
system.error_tracker(msg)
descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course, err) descriptor = ErrorDescriptor.from_xml(xml_data, system, org, course, err)
return descriptor return descriptor
......
...@@ -33,6 +33,7 @@ def check_course(course_id, course_must_be_open=True, course_required=True): ...@@ -33,6 +33,7 @@ def check_course(course_id, course_must_be_open=True, course_required=True):
try: try:
course_loc = CourseDescriptor.id_to_location(course_id) course_loc = CourseDescriptor.id_to_location(course_id)
course = modulestore().get_item(course_loc) course = modulestore().get_item(course_loc)
except (KeyError, ItemNotFoundError): except (KeyError, ItemNotFoundError):
raise Http404("Course not found.") raise Http404("Course not found.")
......
...@@ -55,14 +55,20 @@ def user_groups(user): ...@@ -55,14 +55,20 @@ def user_groups(user):
def format_url_params(params): 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 @ensure_csrf_cookie
@cache_if_anonymous @cache_if_anonymous
def courses(request): def courses(request):
# TODO: Clean up how 'error' is done. # 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) universities = defaultdict(list)
for course in courses: for course in courses:
universities[course.org].append(course) universities[course.org].append(course)
...@@ -210,7 +216,10 @@ def index(request, course_id, chapter=None, section=None, ...@@ -210,7 +216,10 @@ def index(request, course_id, chapter=None, section=None,
log.warning("Couldn't find a section descriptor for course_id '{0}'," log.warning("Couldn't find a section descriptor for course_id '{0}',"
"chapter '{1}', section '{2}'".format( "chapter '{1}', section '{2}'".format(
course_id, chapter, section)) course_id, chapter, section))
else:
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) result = render_to_response('courseware.html', context)
except: except:
......
...@@ -47,6 +47,13 @@ ...@@ -47,6 +47,13 @@
<section class="course-content"> <section class="course-content">
${content} ${content}
% if course_errors is not UNDEFINED:
<h2>Course errors</h2>
<div id="course-errors">
${course_errors}
</div>
% endif
</section> </section>
</div> </div>
</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