diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 9b8bbd7288..fb0b63b83c 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -510,7 +510,9 @@ class LoncapaProblem(object): # let each Response render itself if problemtree in self.responders: - return self.responders[problemtree].render_html(self._extract_html) + overall_msg = self.correct_map.get_overall_message() + return self.responders[problemtree].render_html(self._extract_html, + response_msg=overall_msg) # let each custom renderer render itself: if problemtree.tag in customrender.registry.registered_tags(): diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 89372ca2bd..897f922e93 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -174,13 +174,14 @@ class LoncapaResponse(object): ''' return sum(self.maxpoints.values()) - def render_html(self, renderer): + def render_html(self, renderer, response_msg=None): ''' Return XHTML Element tree representation of this Response. Arguments: - renderer : procedure which produces HTML given an ElementTree + - response_msg: a message displayed at the end of the Response ''' # render ourself as a + our content tree = etree.Element('span') @@ -195,6 +196,13 @@ class LoncapaResponse(object): if item_xhtml is not None: tree.append(item_xhtml) tree.tail = self.xml.tail + + # Add a
for the message at the end of the response + if response_msg: + response_msg_div = etree.SubElement(tree, 'div') + response_msg_div.set("class", "response_message") + response_msg_div.text = response_msg + return tree def evaluate_answers(self, student_answers, old_cmap): @@ -1060,7 +1068,7 @@ def sympy_check2(): # and the first input stores the message if 'ok' in ret: correct = ['correct'] * len(idset) if ret['ok'] else ['incorrect'] * len(idset) - msg = ret['msg'] + msg = ret.get('msg', None) if 1: # try to clean up message html diff --git a/common/lib/capa/capa/tests/test_correctmap.py b/common/lib/capa/capa/tests/test_correctmap.py new file mode 100644 index 0000000000..ed0e9a1948 --- /dev/null +++ b/common/lib/capa/capa/tests/test_correctmap.py @@ -0,0 +1,146 @@ +import unittest +from capa.correctmap import CorrectMap +import datetime + +class CorrectMapTest(unittest.TestCase): + + def setUp(self): + self.cmap = CorrectMap() + + def test_set_input_properties(self): + + # Set the correctmap properties for two inputs + self.cmap.set(answer_id='1_2_1', + correctness='correct', + npoints=5, + msg='Test message', + hint='Test hint', + hintmode='always', + queuestate={'key':'secretstring', + 'time':'20130228100026'}) + + self.cmap.set(answer_id='2_2_1', + correctness='incorrect', + npoints=None, + msg=None, + hint=None, + hintmode=None, + queuestate=None) + + # Assert that each input has the expected properties + self.assertTrue(self.cmap.is_correct('1_2_1')) + self.assertFalse(self.cmap.is_correct('2_2_1')) + + self.assertEqual(self.cmap.get_correctness('1_2_1'), 'correct') + self.assertEqual(self.cmap.get_correctness('2_2_1'), 'incorrect') + + self.assertEqual(self.cmap.get_npoints('1_2_1'), 5) + self.assertEqual(self.cmap.get_npoints('2_2_1'), 0) + + self.assertEqual(self.cmap.get_msg('1_2_1'), 'Test message') + self.assertEqual(self.cmap.get_msg('2_2_1'), None) + + self.assertEqual(self.cmap.get_hint('1_2_1'), 'Test hint') + self.assertEqual(self.cmap.get_hint('2_2_1'), None) + + self.assertEqual(self.cmap.get_hintmode('1_2_1'), 'always') + self.assertEqual(self.cmap.get_hintmode('2_2_1'), None) + + self.assertTrue(self.cmap.is_queued('1_2_1')) + self.assertFalse(self.cmap.is_queued('2_2_1')) + + self.assertEqual(self.cmap.get_queuetime_str('1_2_1'), '20130228100026') + self.assertEqual(self.cmap.get_queuetime_str('2_2_1'), None) + + self.assertTrue(self.cmap.is_right_queuekey('1_2_1', 'secretstring')) + self.assertFalse(self.cmap.is_right_queuekey('1_2_1', 'invalidstr')) + self.assertFalse(self.cmap.is_right_queuekey('1_2_1', '')) + self.assertFalse(self.cmap.is_right_queuekey('1_2_1', None)) + + self.assertFalse(self.cmap.is_right_queuekey('2_2_1', 'secretstring')) + self.assertFalse(self.cmap.is_right_queuekey('2_2_1', 'invalidstr')) + self.assertFalse(self.cmap.is_right_queuekey('2_2_1', '')) + self.assertFalse(self.cmap.is_right_queuekey('2_2_1', None)) + + + def test_get_npoints(self): + # Set the correctmap properties for 4 inputs + # 1) correct, 5 points + # 2) correct, None points + # 3) incorrect, 5 points + # 4) incorrect, None points + self.cmap.set(answer_id='1_2_1', + correctness='correct', + npoints=5) + + self.cmap.set(answer_id='2_2_1', + correctness='correct', + npoints=None) + + self.cmap.set(answer_id='3_2_1', + correctness='incorrect', + npoints=5) + + self.cmap.set(answer_id='4_2_1', + correctness='incorrect', + npoints=None) + + # Assert that we get the expected points + # If points assigned and correct --> npoints + # If no points assigned and correct --> 1 point + # Otherwise --> 0 points + self.assertEqual(self.cmap.get_npoints('1_2_1'), 5) + self.assertEqual(self.cmap.get_npoints('2_2_1'), 1) + self.assertEqual(self.cmap.get_npoints('3_2_1'), 0) + self.assertEqual(self.cmap.get_npoints('4_2_1'), 0) + + + def test_set_overall_message(self): + + # Default is an empty string string + self.assertEqual(self.cmap.get_overall_message(), "") + + # Set a message that applies to the whole question + self.cmap.set_overall_message("Test message") + + # Retrieve the message + self.assertEqual(self.cmap.get_overall_message(), "Test message") + + # Setting the message to None --> empty string + self.cmap.set_overall_message(None) + self.assertEqual(self.cmap.get_overall_message(), "") + + def test_update_from_correctmap(self): + # Initialize a CorrectMap with some properties + self.cmap.set(answer_id='1_2_1', + correctness='correct', + npoints=5, + msg='Test message', + hint='Test hint', + hintmode='always', + queuestate={'key':'secretstring', + 'time':'20130228100026'}) + + self.cmap.set_overall_message("Test message") + + # Create a second cmap, then update it to have the same properties + # as the first cmap + other_cmap = CorrectMap() + other_cmap.update(self.cmap) + + # Assert that it has all the same properties + self.assertEqual(other_cmap.get_overall_message(), + self.cmap.get_overall_message()) + + self.assertEqual(other_cmap.get_dict(), + self.cmap.get_dict()) + + + def test_update_from_invalid(self): + # Should get an exception if we try to update() a CorrectMap + # with a non-CorrectMap value + invalid_list = [None, "string", 5, datetime.datetime.today()] + + for invalid in invalid_list: + with self.assertRaises(Exception): + self.cmap.update(invalid) diff --git a/common/lib/capa/capa/tests/test_html_render.py b/common/lib/capa/capa/tests/test_html_render.py new file mode 100644 index 0000000000..aa5312aa14 --- /dev/null +++ b/common/lib/capa/capa/tests/test_html_render.py @@ -0,0 +1,186 @@ +import unittest +from lxml import etree +import os +import textwrap +import json +import mock + +from capa.capa_problem import LoncapaProblem +from response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory +from . import test_system + +class CapaHtmlRenderTest(unittest.TestCase): + + def test_include_html(self): + # Create a test file to include + self._create_test_file('test_include.xml', + 'Test include') + + # Generate some XML with an + xml_str = textwrap.dedent(""" + + + + """) + + # Create the problem + problem = LoncapaProblem(xml_str, '1', system=test_system) + + # Render the HTML + rendered_html = etree.XML(problem.get_html()) + + # Expect that the include file was embedded in the problem + test_element = rendered_html.find("test") + self.assertEqual(test_element.tag, "test") + self.assertEqual(test_element.text, "Test include") + + + def test_process_outtext(self): + # Generate some XML with and + xml_str = textwrap.dedent(""" + + Test text + + """) + + # Create the problem + problem = LoncapaProblem(xml_str, '1', system=test_system) + + # Render the HTML + rendered_html = etree.XML(problem.get_html()) + + # Expect that the and + # were converted to tags + span_element = rendered_html.find('span') + self.assertEqual(span_element.text, 'Test text') + + def test_render_script(self): + # Generate some XML with a + + """) + + # Create the problem + problem = LoncapaProblem(xml_str, '1', system=test_system) + + # Render the HTML + rendered_html = etree.XML(problem.get_html()) + + # Expect that the script element has been removed from the rendered HTML + script_element = rendered_html.find('script') + self.assertEqual(None, script_element) + + def test_render_response_xml(self): + # Generate some XML for a string response + kwargs = {'question_text': "Test question", + 'explanation_text': "Test explanation", + 'answer': 'Test answer', + 'hints': [('test prompt', 'test_hint', 'test hint text')]} + xml_str = StringResponseXMLFactory().build_xml(**kwargs) + + # Mock out the template renderer + test_system.render_template = mock.Mock() + test_system.render_template.return_value = "
Input Template Render
" + + # Create the problem and render the HTML + problem = LoncapaProblem(xml_str, '1', system=test_system) + rendered_html = etree.XML(problem.get_html()) + + # Expect problem has been turned into a
+ self.assertEqual(rendered_html.tag, "div") + + # Expect question text is in a

child + question_element = rendered_html.find("p") + self.assertEqual(question_element.text, "Test question") + + # Expect that the response has been turned into a + response_element = rendered_html.find("span") + self.assertEqual(response_element.tag, "span") + + # Expect that the response + # that contains a

for the textline + textline_element = response_element.find("div") + self.assertEqual(textline_element.text, 'Input Template Render') + + # Expect a child
for the solution + # with the rendered template + solution_element = rendered_html.find("div") + self.assertEqual(solution_element.text, 'Input Template Render') + + # Expect that the template renderer was called with the correct + # arguments, once for the textline input and once for + # the solution + expected_textline_context = {'status': 'unsubmitted', + 'value': '', + 'preprocessor': None, + 'msg': '', + 'inline': False, + 'hidden': False, + 'do_math': False, + 'id': '1_2_1', + 'size': None} + + expected_solution_context = {'id': '1_solution_1'} + + expected_calls = [mock.call('textline.html', expected_textline_context), + mock.call('solutionspan.html', expected_solution_context)] + + self.assertEqual(test_system.render_template.call_args_list, + expected_calls) + + + def test_render_response_with_overall_msg(self): + # CustomResponse script that sets an overall_message + script=textwrap.dedent(""" + def check_func(*args): + return {'overall_message': 'Test message', + 'input_list': [ {'ok': True, 'msg': '' } ] } + """) + + # Generate some XML for a CustomResponse + kwargs = {'script':script, 'cfn': 'check_func'} + xml_str = CustomResponseXMLFactory().build_xml(**kwargs) + + # Create the problem and render the html + problem = LoncapaProblem(xml_str, '1', system=test_system) + + # Grade the problem + correctmap = problem.grade_answers({'1_2_1': 'test'}) + + # Render the html + rendered_html = etree.XML(problem.get_html()) + + + # Expect that there is a
within the response
+ # with css class response_message + msg_div_element = rendered_html.find(".//div[@class='response_message']") + self.assertEqual(msg_div_element.tag, "div") + self.assertEqual(msg_div_element.get('class'), "response_message") + + + def test_substitute_python_vars(self): + # Generate some XML with Python variables defined in a script + # and used later as attributes + xml_str = textwrap.dedent(""" + + + + + """) + + # Create the problem and render the HTML + problem = LoncapaProblem(xml_str, '1', system=test_system) + rendered_html = etree.XML(problem.get_html()) + + # Expect that the variable $test has been replaced with its value + span_element = rendered_html.find('span') + self.assertEqual(span_element.get('attr'), "TEST") + + def _create_test_file(self, path, content_str): + test_fp = test_system.filestore.open(path, "w") + test_fp.write(content_str) + test_fp.close() + + self.addCleanup(lambda: os.remove(test_fp.name))