It's safe to remove this because non-staff [1] users cannot access [2] an `ErrorBlock`. We were able to reproduce this with and without this commit with the following results: 1. Staff users were seeing the `ErrorBlock`. 2. Non-staff users were getting an empty `<div class="vert-mod"></div>`. In theory, error blocks should be hidden in the Learning MFE because of this option [3]. However, when we manually set `hide_access_error_blocks` to `False`, we kept getting identical results (with and without this commit), so it looks that the removal `NonStaffErrorBlock` was just omitted at some point. [1]a4ec4c1b8e/lms/djangoapps/courseware/access.py (L419-L436)[2]a4ec4c1b8e/lms/djangoapps/courseware/access.py (L150-L151)[3]92ca176fde/lms/djangoapps/courseware/views/views.py (L1547-L1551)
230 lines
8.2 KiB
Python
230 lines
8.2 KiB
Python
"""
|
|
Modules that get shown to the users when an error has occurred while
|
|
loading or rendering other modules
|
|
"""
|
|
|
|
|
|
import hashlib
|
|
import json
|
|
import logging
|
|
import sys
|
|
|
|
from lxml import etree
|
|
from web_fragments.fragment import Fragment
|
|
from xblock.core import XBlock
|
|
from xblock.field_data import DictFieldData
|
|
from xblock.fields import Scope, ScopeIds, String
|
|
|
|
from xmodule.errortracker import exc_info_to_str
|
|
from xmodule.modulestore import EdxJSONEncoder
|
|
from xmodule.x_module import (
|
|
HTMLSnippet,
|
|
ResourceTemplates,
|
|
XModuleMixin,
|
|
XModuleToXBlockMixin,
|
|
)
|
|
|
|
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 ErrorBlock 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:
|
|
"""
|
|
XBlock fields used by the ErrorBlocks
|
|
"""
|
|
contents = String(scope=Scope.content)
|
|
error_msg = String(scope=Scope.content)
|
|
display_name = String(scope=Scope.settings)
|
|
|
|
|
|
@XBlock.needs('mako')
|
|
class ErrorBlock(
|
|
ErrorFields,
|
|
XModuleToXBlockMixin,
|
|
HTMLSnippet,
|
|
ResourceTemplates,
|
|
XModuleMixin,
|
|
): # pylint: disable=abstract-method
|
|
"""
|
|
Module that gets shown to staff when there has been an error while
|
|
loading or rendering other modules
|
|
"""
|
|
|
|
resources_dir = None
|
|
|
|
def student_view(self, _context):
|
|
"""
|
|
Return a fragment that contains the html for the student view.
|
|
"""
|
|
fragment = Fragment(self.runtime.service(self, 'mako').render_template('module-error.html', {
|
|
'staff_access': True,
|
|
'data': self.contents,
|
|
'error': self.error_msg,
|
|
}))
|
|
return fragment
|
|
|
|
def studio_view(self, _context):
|
|
"""
|
|
Show empty edit view since this is not editable.
|
|
"""
|
|
return Fragment('')
|
|
|
|
@classmethod
|
|
def _construct(cls, system, contents, error_msg, location, for_parent=None):
|
|
"""
|
|
Build a new ErrorBlock 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.block_type == '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 ErrorBlock 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': str(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'): # lint-amnesty, pylint: disable=missing-function-docstring
|
|
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'))
|
|
|
|
@classmethod
|
|
def parse_xml(cls, node, runtime, keys, id_generator): # lint-amnesty, pylint: disable=unused-argument
|
|
"""
|
|
Interpret the parsed XML in `node`, creating an XModuleDescriptor.
|
|
"""
|
|
# It'd be great to not reserialize and deserialize the xml
|
|
xml = etree.tostring(node).decode('utf-8')
|
|
block = cls.from_xml(xml, runtime, id_generator)
|
|
return block
|
|
|
|
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')
|
|
|
|
def add_xml_to_node(self, node):
|
|
"""
|
|
Export this :class:`XModuleDescriptor` as XML, by setting attributes on the provided
|
|
`node`.
|
|
"""
|
|
xml_string = self.export_to_xml(self.runtime.export_fs)
|
|
exported_node = etree.fromstring(xml_string)
|
|
node.tag = exported_node.tag
|
|
node.text = exported_node.text
|
|
node.tail = exported_node.tail
|
|
|
|
for key, value in exported_node.items():
|
|
if key == 'url_name' and value == 'course' and key in node.attrib:
|
|
# if url_name is set in ExportManager then do not override it here.
|
|
continue
|
|
node.set(key, value)
|
|
|
|
node.extend(list(exported_node))
|