Commit c7785d8f by Alexander Kryklia

Allow HTML in grader messages.

parent 8e4b924b
......@@ -46,6 +46,7 @@ import re
import shlex # for splitting quoted strings
import sys
import pyparsing
import html5lib
from .registry import TagRegistry
from chem import chemcalc
......@@ -286,7 +287,18 @@ class InputTypeBase(object):
context = self._get_render_context()
html = self.capa_system.render_template(self.template, context)
return etree.XML(html)
try:
output = etree.XML(html)
except etree.XMLSyntaxError as ex:
# If `html` contains attrs with no values, like `controls` in <audio controls src='smth'/>,
# XML parser will raise exception, so wee fallback to html5parser, which will set empty "" values for such attrs.
try:
output = html5lib.parseFragment(html, treebuilder='lxml', namespaceHTMLElements=False)[0]
except IndexError:
raise ex
return output
def get_user_visible_answer(self, internal_answer):
"""
......
......@@ -14,6 +14,7 @@ import cgi
import inspect
import json
import logging
import html5lib
import numbers
import numpy
import os
......@@ -1761,17 +1762,22 @@ class CodeResponse(LoncapaResponse):
" tags: 'correct', 'score', 'msg'")
return fail
# Next, we need to check that the contents of the external grader message
# is safe for the LMS.
# Next, we need to check that the contents of the external grader message is safe for the LMS.
# 1) Make sure that the message is valid XML (proper opening/closing tags)
# 2) TODO: Is the message actually HTML?
# 2) If it is not valid XML, make sure it is valid HTML. Note: html5lib parser will try to repair any broken HTML
# For example: <aaa></bbb> will become <aaa/>.
msg = score_result['msg']
try:
etree.fromstring(msg)
except etree.XMLSyntaxError as _err:
log.error("Unable to parse external grader message as valid"
# If `html` contains attrs with no values, like `controls` in <audio controls src='smth'/>,
# XML parser will raise exception, so wee fallback to html5parser, which will set empty "" values for such attrs.
parsed = html5lib.parseFragment(msg, treebuilder='lxml', namespaceHTMLElements=False)
if not parsed:
log.error("Unable to parse external grader message as valid"
" XML: score_msg['msg']=%s", msg)
return fail
return fail
return (True, score_result['correct'], score_result['score'], msg)
......
......@@ -20,6 +20,7 @@ TODO:
import json
from lxml import etree
import unittest
import textwrap
import xml.sax.saxutils as saxutils
from . import test_capa_system
......@@ -583,6 +584,44 @@ class MatlabTest(unittest.TestCase):
self.assertEqual(input_state['queuestate'], 'queued')
self.assertFalse('queue_msg' in input_state)
def test_get_html(self):
# usual output
output = self.the_input.get_html()
self.assertEqual(
etree.tostring(output),
"""<div>{\'status\': \'queued\', \'button_enabled\': True, \'rows\': \'10\', \'queue_len\': \'3\'\
, \'mode\': \'\', \'cols\': \'80\', \'STATIC_URL\': \'/dummy-static/\', \'linenumbers\': \'true\'\
, \'queue_msg\': \'\', \'value\': \'print "good evening"\', \'msg\': u\'Submitted\
. As soon as a response is returned, this message will be replaced by that feedback.\', \'hidden\': \'\'\
, \'id\': \'prob_1_2\', \'tabsize\': 4}</div>"""
)
# test html, that is correct HTML5 html, but is not parsable by XML parser.
old_render_template = self.the_input.capa_system.render_template
self.the_input.capa_system.render_template = lambda *args: textwrap.dedent("""
<div class='matlabResponse'><div id='mwAudioPlaceHolder'>
<audio controls autobuffer autoplay src='data:audio/wav;base64='>Audio is not supported on this browser.</audio>
<div>Right click <a href=https://endpoint.mss-mathworks.com/media/filename.wav>here</a> and click \"Save As\" to download the file</div></div>
<div style='white-space:pre' class='commandWindowOutput'></div><ul></ul></div>
""").replace('\n', '')
output = self.the_input.get_html()
self.assertEqual(
etree.tostring(output),
textwrap.dedent("""
<div class='matlabResponse'><div id='mwAudioPlaceHolder'>
<audio src='data:audio/wav;base64=' autobuffer="" controls="" autoplay="">Audio is not supported on this browser.</audio>
<div>Right click <a href="https://endpoint.mss-mathworks.com/media/filename.wav">here</a> and click \"Save As\" to download the file</div></div>
<div style='white-space:pre' class='commandWindowOutput'/><ul/></div>
""").replace('\n', '').replace('\'', '\"')
)
# check that exception is raised during parsing for html.
self.the_input.capa_system.render_template = lambda *args: "<aaa"
with self.assertRaises(etree.XMLSyntaxError):
self.the_input.get_html()
self.the_input.capa_system.render_template = old_render_template
class SchematicTest(unittest.TestCase):
'''
......
......@@ -998,6 +998,59 @@ class CodeResponseTest(ResponseTest):
self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3'])
self.assertEquals(answers_converted['1_4_1'], [fp.name, fp.name])
def test_parse_score_msg_of_responder(self):
"""
Test whether LoncapaProblem._parse_score_msg correcly parses valid HTML5 html.
"""
valid_grader_msgs = [
u'<span>MESSAGE</span>', # Valid XML
textwrap.dedent("""
<div class='matlabResponse'><div id='mwAudioPlaceHolder'>
<audio controls autobuffer autoplay src='data:audio/wav;base64='>Audio is not supported on this browser.</audio>
<div>Right click <a href=https://endpoint.mss-mathworks.com/media/filename.wav>here</a> and click \"Save As\" to download the file</div></div>
<div style='white-space:pre' class='commandWindowOutput'></div><ul></ul></div>
""").replace('\n', ''), # Valid HTML5 real case Matlab response, invalid XML
'<aaa></bbb>' # Invalid XML, but will be parsed by html5lib to <aaa/>
]
invalid_grader_msgs = [
'<audio', # invalid XML and HTML5
]
answer_ids = sorted(self.problem.get_question_answers())
# CodeResponse requires internal CorrectMap state. Build it now in the queued state
old_cmap = CorrectMap()
for i, answer_id in enumerate(answer_ids):
queuekey = 1000 + i
queuestate = CodeResponseTest.make_queuestate(queuekey, datetime.now(UTC))
old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate))
for grader_msg in valid_grader_msgs:
correct_score_msg = json.dumps({'correct': True, 'score': 1, 'msg': grader_msg})
incorrect_score_msg = json.dumps({'correct': False, 'score': 0, 'msg': grader_msg})
xserver_msgs = {'correct': correct_score_msg, 'incorrect': incorrect_score_msg, }
for i, answer_id in enumerate(answer_ids):
self.problem.correct_map = CorrectMap()
self.problem.correct_map.update(old_cmap)
output = self.problem.update_score(xserver_msgs['correct'], queuekey=1000 + i)
self.assertEquals(output[answer_id]['msg'], grader_msg)
for grader_msg in invalid_grader_msgs:
correct_score_msg = json.dumps({'correct': True, 'score': 1, 'msg': grader_msg})
incorrect_score_msg = json.dumps({'correct': False, 'score': 0, 'msg': grader_msg})
xserver_msgs = {'correct': correct_score_msg, 'incorrect': incorrect_score_msg, }
for i, answer_id in enumerate(answer_ids):
self.problem.correct_map = CorrectMap()
self.problem.correct_map.update(old_cmap)
output = self.problem.update_score(xserver_msgs['correct'], queuekey=1000 + i)
self.assertEquals(output[answer_id]['msg'], u'Invalid grader reply. Please contact the course staff.')
class ChoiceResponseTest(ResponseTest):
from capa.tests.response_xml_factory import ChoiceResponseXMLFactory
......
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