[PERF-303] Integer XBlocks/XModules into the static asset pipeline. This PR, based on hackathon work from Christina/Andy, implements a way to discover all installed XBlocks and XModules and to enumerate their public assets, then pulling them in during the collectstatic phase and hashing them. In turn, the methods for generating URLs to resources will then returned the hashed name for assets, allowing them to be served from nginx/CDNs, and cached heavily.
213 lines
7.4 KiB
Python
213 lines
7.4 KiB
Python
"""
|
|
Modules that get shown to the users when an error has occurred while
|
|
loading or rendering other modules
|
|
"""
|
|
|
|
import hashlib
|
|
import logging
|
|
import json
|
|
import sys
|
|
|
|
from lxml import etree
|
|
from xmodule.x_module import XModule, XModuleDescriptor
|
|
from xmodule.errortracker import exc_info_to_str
|
|
from xblock.fields import String, Scope, ScopeIds
|
|
from xblock.field_data import DictFieldData
|
|
from xmodule.modulestore import EdxJSONEncoder
|
|
|
|
|
|
log = logging.getLogger(__name__)
|
|
|
|
# NOTE: This is not the most beautiful design in the world, but there's no good
|
|
# way to tell if the module is being used in a staff context or not. Errors that get discovered
|
|
# at course load time are turned into ErrorDescriptor objects, and automatically hidden from students.
|
|
# Unfortunately, we can also have errors when loading modules mid-request, and then we need to decide
|
|
# what to show, and the logic for that belongs in the LMS (e.g. in get_module), so the error handler
|
|
# decides whether to create a staff or not-staff module.
|
|
|
|
|
|
class ErrorFields(object):
|
|
"""
|
|
XBlock fields used by the ErrorModules
|
|
"""
|
|
contents = String(scope=Scope.content)
|
|
error_msg = String(scope=Scope.content)
|
|
display_name = String(scope=Scope.settings)
|
|
|
|
|
|
class ErrorModule(ErrorFields, XModule):
|
|
"""
|
|
Module that gets shown to staff when there has been an error while
|
|
loading or rendering other modules
|
|
"""
|
|
|
|
def get_html(self):
|
|
'''Show an error to staff.
|
|
TODO (vshnayder): proper style, divs, etc.
|
|
'''
|
|
# staff get to see all the details
|
|
return self.system.render_template('module-error.html', {
|
|
'staff_access': True,
|
|
'data': self.contents,
|
|
'error': self.error_msg,
|
|
})
|
|
|
|
|
|
class NonStaffErrorModule(ErrorFields, XModule):
|
|
"""
|
|
Module that gets shown to students when there has been an error while
|
|
loading or rendering other modules
|
|
"""
|
|
def get_html(self):
|
|
'''Show an error to a student.
|
|
TODO (vshnayder): proper style, divs, etc.
|
|
'''
|
|
# staff get to see all the details
|
|
return self.system.render_template('module-error.html', {
|
|
'staff_access': False,
|
|
'data': "",
|
|
'error': "",
|
|
})
|
|
|
|
|
|
class ErrorDescriptor(ErrorFields, XModuleDescriptor):
|
|
"""
|
|
Module that provides a raw editing view of broken xml.
|
|
"""
|
|
module_class = ErrorModule
|
|
resources_dir = None
|
|
|
|
def get_html(self):
|
|
return u''
|
|
|
|
@classmethod
|
|
def _construct(cls, system, contents, error_msg, location, for_parent=None):
|
|
"""
|
|
Build a new ErrorDescriptor. using ``system``.
|
|
|
|
Arguments:
|
|
system (:class:`DescriptorSystem`): The :class:`DescriptorSystem` used
|
|
to construct the XBlock that had an error.
|
|
contents (unicode): An encoding of the content of the xblock that had an error.
|
|
error_msg (unicode): A message describing the error.
|
|
location (:class:`UsageKey`): The usage key of the XBlock that had an error.
|
|
for_parent (:class:`XBlock`): Optional. The parent of this error block.
|
|
"""
|
|
|
|
if error_msg is None:
|
|
# this string is not marked for translation because we don't have
|
|
# access to the user context, and this will only be seen by staff
|
|
error_msg = 'Error not available'
|
|
|
|
if location.category == 'error':
|
|
location = location.replace(
|
|
# Pick a unique url_name -- the sha1 hash of the contents.
|
|
# NOTE: We could try to pull out the url_name of the errored descriptor,
|
|
# but url_names aren't guaranteed to be unique between descriptor types,
|
|
# and ErrorDescriptor can wrap any type. When the wrapped module is fixed,
|
|
# it will be written out with the original url_name.
|
|
name=hashlib.sha1(contents.encode('utf8')).hexdigest()
|
|
)
|
|
|
|
# real metadata stays in the content, but add a display name
|
|
field_data = DictFieldData({
|
|
'error_msg': unicode(error_msg),
|
|
'contents': contents,
|
|
'location': location,
|
|
'category': 'error'
|
|
})
|
|
return system.construct_xblock_from_class(
|
|
cls,
|
|
# The error module doesn't use scoped data, and thus doesn't need
|
|
# real scope keys
|
|
ScopeIds(None, 'error', location, location),
|
|
field_data,
|
|
for_parent=for_parent,
|
|
)
|
|
|
|
def get_context(self):
|
|
return {
|
|
'module': self,
|
|
'data': self.contents,
|
|
}
|
|
|
|
@classmethod
|
|
def from_json(cls, json_data, system, location, error_msg='Error not available'):
|
|
try:
|
|
json_string = json.dumps(json_data, skipkeys=False, indent=4, cls=EdxJSONEncoder)
|
|
except: # pylint: disable=bare-except
|
|
json_string = repr(json_data)
|
|
|
|
return cls._construct(
|
|
system,
|
|
json_string,
|
|
error_msg,
|
|
location=location
|
|
)
|
|
|
|
@classmethod
|
|
def from_descriptor(cls, descriptor, error_msg=None):
|
|
return cls._construct(
|
|
descriptor.runtime,
|
|
str(descriptor),
|
|
error_msg,
|
|
location=descriptor.location,
|
|
for_parent=descriptor.get_parent() if descriptor.has_cached_parent else None
|
|
)
|
|
|
|
@classmethod
|
|
def from_xml(cls, xml_data, system, id_generator, # pylint: disable=arguments-differ
|
|
error_msg=None):
|
|
'''Create an instance of this descriptor from the supplied data.
|
|
|
|
Does not require that xml_data be parseable--just stores it and exports
|
|
as-is if not.
|
|
|
|
Takes an extra, optional, parameter--the error that caused an
|
|
issue. (should be a string, or convert usefully into one).
|
|
'''
|
|
|
|
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
|
|
error_node = xml_obj.find('error_msg')
|
|
if error_node is not None:
|
|
error_msg = error_node.text
|
|
else:
|
|
error_msg = None
|
|
|
|
except etree.XMLSyntaxError:
|
|
# Save the error to display later--overrides other problems
|
|
error_msg = exc_info_to_str(sys.exc_info())
|
|
|
|
return cls._construct(system, xml_data, error_msg, location=id_generator.create_definition('error'))
|
|
|
|
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.contents)
|
|
return etree.tostring(xml, encoding='unicode')
|
|
except etree.XMLSyntaxError:
|
|
# still not valid.
|
|
root = etree.Element('error')
|
|
root.text = self.contents
|
|
err_node = etree.SubElement(root, 'error_msg')
|
|
err_node.text = self.error_msg
|
|
return etree.tostring(root, encoding='unicode')
|
|
|
|
|
|
class NonStaffErrorDescriptor(ErrorDescriptor):
|
|
"""
|
|
Module that provides non-staff error messages.
|
|
"""
|
|
module_class = NonStaffErrorModule
|