diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index e253b61948..65280d6d29 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -46,7 +46,7 @@ import sys import pyparsing from .registry import TagRegistry -from capa.chem import chemcalc +from chem import chemcalc import xqueue_interface from datetime import datetime diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 9dd1e74409..ddf184c9be 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -29,7 +29,7 @@ from collections import namedtuple from shapely.geometry import Point, MultiPoint # specific library imports -from .calc import evaluator, UndefinedVariable +from calc import evaluator, UndefinedVariable from .correctmap import CorrectMap from datetime import datetime from .util import * @@ -1043,7 +1043,7 @@ class CustomResponse(LoncapaResponse): messages = self.context['messages'] correct_map = CorrectMap() - overall_message = self.clean_message_html(self.context['overall_message'])) + overall_message = self.clean_message_html(self.context['overall_message']) correct_map.set_overall_message(overall_message) for k in range(len(idset)): @@ -1195,12 +1195,24 @@ class SymbolicResponse(CustomResponse): """ response_tag = 'symbolicresponse' + max_inputfields = 1 + + def setup_response(self): + # Symbolic response always uses symmath_check() + # If the XML did not specify this, then set it now + # Otherwise, we get an error from the superclass + self.xml.set('cfn', 'symmath_check') + + # Let CustomResponse do its setup + super(SymbolicResponse, self).setup_response() def execute_check_function(self, idset, submission): from symmath import symmath_check - fn = self.code try: - answer_given = submission[0] if (len(idset) == 1) else submission + # Since we have limited max_inputfields to 1, + # we can assume that there is only one submission + answer_given = submission[0] + ret = symmath_check( self.expect, answer_given, dynamath=self.context.get('dynamath'), diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py index ee1fdfafc9..ee9a7e6530 100644 --- a/common/lib/capa/capa/tests/response_xml_factory.py +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -734,3 +734,38 @@ class AnnotationResponseXMLFactory(ResponseXMLFactory): option_element.text = description return input_element + + +class SymbolicResponseXMLFactory(ResponseXMLFactory): + """ Factory for producing xml """ + + def create_response_element(self, **kwargs): + """ Build the XML element. + + Uses **kwargs: + + *expect*: The correct answer (a sympy string) + + *options*: list of option strings to pass to symmath_check + (e.g. 'matrix', 'qbit', 'imaginary', 'numerical')""" + + # Retrieve **kwargs + expect = kwargs.get('expect', '') + options = kwargs.get('options', []) + + # Symmath check expects a string of options + options_str = ",".join(options) + + # Construct the element + response_element = etree.Element('symbolicresponse') + + if expect: + response_element.set('expect', str(expect)) + + if options_str: + response_element.set('options', str(options_str)) + + return response_element + + def create_input_element(self, **kwargs): + return ResponseXMLFactory.textline_input_xml(**kwargs) diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index c2ee62ed7a..b76854c744 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -10,6 +10,7 @@ import random import unittest import textwrap import mock +import textwrap from . import test_system @@ -184,107 +185,151 @@ class ImageResponseTest(ResponseTest): self.assert_answer_format(problem) -class SymbolicResponseTest(unittest.TestCase): +class SymbolicResponseTest(ResponseTest): from response_xml_factory import SymbolicResponseXMLFactory xml_factory_class = SymbolicResponseXMLFactory - def test_symbolic_response_grade(self): - symbolicresponse_file = os.path.dirname(__file__) + "/test_files/symbolicresponse.xml" - test_lcp = lcp.LoncapaProblem(open(symbolicresponse_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': 'cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]', - '1_2_1_dynamath': ''' - - - - cos - - ( - θ - ) - - - - - [ - - - - 1 - - - 0 - - - - - 0 - - - 1 - - - - ] - - + - i - - - sin - - ( - θ - ) - - - - - [ - - - - 0 - - - 1 - - - - - 1 - - - 0 - - - - ] - - - - ''', - } - wrong_answers = {'1_2_1': '2', - '1_2_1_dynamath': ''' - - - 2 - - ''', - } + def test_grade_single_input(self): + problem = self.build_problem(math_display=True, + expect="2*x+3*y") + # Correct answers + correct_inputs = [ + ('2x+3y', textwrap.dedent(""" + + + 2*x+3*y + """)), + + ('x+x+3y', textwrap.dedent(""" + + + x+x+3*y + """)), + ] + + for (input_str, input_mathml) in correct_inputs: + self._assert_symbolic_grade(problem, input_str, input_mathml, 'correct') + + # Incorrect answers + incorrect_inputs = [ + ('0', ''), + ('4x+3y', textwrap.dedent(""" + + + 4*x+3*y + """)), + ] + + for (input_str, input_mathml) in incorrect_inputs: + self._assert_symbolic_grade(problem, input_str, input_mathml, 'incorrect') + + + def test_complex_number_grade(self): + problem = self.build_problem(math_display=True, + expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]", + options=["matrix", "imaginary"]) + + # For LaTeX-style inputs, symmath_check() will try to contact + # a server to convert the input to MathML. + # We mock out the server, simulating the response that it would give + # for this input. import requests - d = os.path.dirname(__file__) - correct_snuggletex_response = open(os.path.join(d, "test_files/snuggletex_correct.html")).read().decode('utf8') - wrong_snuggletex_response = open(os.path.join(d, "test_files/snuggletex_wrong.html")).read().decode('utf8') + dirpath = os.path.dirname(__file__) + correct_snuggletex_response = open(os.path.join(dirpath, "test_files/snuggletex_correct.html")).read().decode('utf8') + wrong_snuggletex_response = open(os.path.join(dirpath, "test_files/snuggletex_wrong.html")).read().decode('utf8') + # Correct answer with mock.patch.object(requests, 'post') as mock_post: + + # Simulate what the LaTeX-to-MathML server would + # send for the correct response input mock_post.return_value.text = correct_snuggletex_response - self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') + self._assert_symbolic_grade(problem, + "cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]", + textwrap.dedent(""" + + + + cos + (θ) + + + + [ + + + 10 + + + 01 + + + ] + + + + i + + + sin + + (θ) + + + + + [ + + + 01 + + + 10 + + + ] + + + + """), + 'correct') + + # Incorrect answer with mock.patch.object(requests, 'post') as mock_post: + + # Simulate what the LaTeX-to-MathML server would + # send for the incorrect response input mock_post.return_value.text = wrong_snuggletex_response - self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect') + + self._assert_symbolic_grade(problem, "2", + textwrap.dedent(""" + + 2 + + """), + 'incorrect') + + def test_multiple_inputs_exception(self): + + # Should not allow multiple inputs, since we specify + # only one "expect" value + with self.assertRaises(Exception): + problem = self.build_problem(math_display=True, + expect="2*x+3*y", + num_inputs=3) + + def _assert_symbolic_grade(self, problem, + student_input, + dynamath_input, + expected_correctness): + input_dict = {'1_2_1': str(student_input), + '1_2_1_dynamath': str(dynamath_input) } + + correct_map = problem.grade_answers(input_dict) + + self.assertEqual(correct_map.get_correctness('1_2_1'), + expected_correctness) class OptionResponseTest(ResponseTest): diff --git a/common/lib/capa/capa/util.py b/common/lib/capa/capa/util.py index 9f3e8bd3a0..cd694f1137 100644 --- a/common/lib/capa/capa/util.py +++ b/common/lib/capa/capa/util.py @@ -1,4 +1,4 @@ -from .calc import evaluator, UndefinedVariable +from calc import evaluator #----------------------------------------------------------------------------- #