diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py index cdf5a5e47f..3f6c16e1c3 100644 --- a/common/lib/capa/capa/tests/response_xml_factory.py +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -280,6 +280,8 @@ class CodeResponseXMLFactory(ResponseXMLFactory): class ChoiceResponseXMLFactory(ResponseXMLFactory): + """ Factory for creating XML trees """ + def create_response_element(self, **kwargs): """ Create a element """ return etree.Element("choiceresponse") @@ -290,11 +292,104 @@ class ChoiceResponseXMLFactory(ResponseXMLFactory): class FormulaResponseXMLFactory(ResponseXMLFactory): + """ Factory for creating XML trees """ + def create_response_element(self, **kwargs): - raise NotImplemented + """ Create a element. + + *sample_dict*: A dictionary of the form: + { VARIABLE_NAME: (MIN, MAX), ....} + + This specifies the range within which + to numerically sample each variable to check + student answers. + [REQUIRED] + + *num_samples*: The number of times to sample the student's answer + to numerically compare it to the correct answer. + + *tolerance*: The tolerance within which answers will be accepted + [DEFAULT: 0.01] + + *answer*: The answer to the problem. Can be a formula string + or a Python variable defined in a script + (e.g. "$calculated_answer" for a Python variable + called calculated_answer) + [REQUIRED] + + *hints*: List of (hint_prompt, hint_name, hint_text) tuples + Where *hint_prompt* is the formula for which we show the hint, + *hint_name* is an internal identifier for the hint, + and *hint_text* is the text we show for the hint. + """ + # Retrieve kwargs + sample_dict = kwargs.get("sample_dict", None) + num_samples = kwargs.get("num_samples", None) + tolerance = kwargs.get("tolerance", 0.01) + answer = kwargs.get("answer", None) + hint_list = kwargs.get("hints", None) + + assert(answer) + assert(sample_dict and num_samples) + + # Create the element + response_element = etree.Element("formularesponse") + + # Set the sample information + sample_str = self._sample_str(sample_dict, num_samples, tolerance) + response_element.set("samples", sample_str) + + + # Set the tolerance + responseparam_element = etree.SubElement(response_element, "responseparam") + responseparam_element.set("type", "tolerance") + responseparam_element.set("default", str(tolerance)) + + # Set the answer + response_element.set("answer", str(answer)) + + # Include hints, if specified + if hint_list: + hintgroup_element = etree.SubElement(response_element, "hintgroup") + + for (hint_prompt, hint_name, hint_text) in hint_list: + + # For each hint, create a element + formulahint_element = etree.SubElement(hintgroup_element, "formulahint") + + # We could sample a different range, but for simplicity, + # we use the same sample string for the hints + # that we used previously. + formulahint_element.set("samples", sample_str) + + formulahint_element.set("answer", hint_prompt) + formulahint_element.set("name", hint_name) + + # For each hint, create a element + # corresponding to the + hintpart_element = etree.SubElement(hintgroup_element, "hintpart") + hintpart_element.set("on", hint_name) + text_element = etree.SubElement(hintpart_element, "text") + text_element.text = hint_text + + return response_element def create_input_element(self, **kwargs): - raise NotImplemented + return ResponseXMLFactory.textline_input_xml(**kwargs) + + def _sample_str(self, sample_dict, num_samples, tolerance): + # Loncapa uses a special format for sample strings: + # "x,y,z@4,5,3:10,12,8#4" means plug in values for (x,y,z) + # from within the box defined by points (4,5,3) and (10,12,8) + # The "#4" means to repeat 4 times. + variables = [str(v) for v in sample_dict.keys()] + low_range_vals = [str(f[0]) for f in sample_dict.values()] + high_range_vals = [str(f[1]) for f in sample_dict.values()] + sample_str = (",".join(sample_dict.keys()) + "@" + + ",".join(low_range_vals) + ":" + + ",".join(high_range_vals) + + "#" + str(num_samples)) + return sample_str class ImageResponseXMLFactory(ResponseXMLFactory): def create_response_element(self, **kwargs): diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 9f6bf7fe4b..09b80ce869 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -267,23 +267,80 @@ class OptionResponseTest(ResponseTest): self.assert_grade(problem, "invalid_option", "incorrect") -class FormulaResponseWithHintTest(unittest.TestCase): - ''' - Test Formula response problem with a hint - This problem also uses calc. - ''' - def test_or_grade(self): - problem_file = os.path.dirname(__file__) + "/test_files/formularesponse_with_hint.xml" - test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': '2.5*x-5.0'} - test_answers = {'1_2_1': '0.4*x-5.0'} - self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') - cmap = test_lcp.grade_answers(test_answers) - self.assertEquals(cmap.get_correctness('1_2_1'), 'incorrect') - self.assertTrue('You have inverted' in cmap.get_hint('1_2_1')) +class FormulaResponseTest(ResponseTest): + from response_xml_factory import FormulaResponseXMLFactory + xml_factory_class = FormulaResponseXMLFactory + + def test_grade(self): + # Sample variables x and y in the range [-10, 10] + sample_dict = {'x': (-10, 10), 'y': (-10, 10)} + + # The expected solution is numerically equivalent to x+2y + problem = self.build_problem(sample_dict=sample_dict, + num_samples=10, + tolerance=0.01, + answer="x+2*y") + + # Expect an equivalent formula to be marked correct + # 2x - x + y + y = x + 2y + input_formula = "2*x - x + y + y" + self.assert_grade(problem, input_formula, "correct") + + # Expect an incorrect formula to be marked incorrect + # x + y != x + 2y + input_formula = "x + y" + self.assert_grade(problem, input_formula, "incorrect") + + def test_hint(self): + # Sample variables x and y in the range [-10, 10] + sample_dict = {'x': (-10, 10), 'y': (-10,10) } + + # Give a hint if the user leaves off the coefficient + # or leaves out x + hints = [('x + 3*y', 'y_coefficient', 'Check the coefficient of y'), + ('2*y', 'missing_x', 'Try including the variable x')] -class StringResponseWithHintTest(ResponseTest): + # The expected solution is numerically equivalent to x+2y + problem = self.build_problem(sample_dict=sample_dict, + num_samples=10, + tolerance=0.01, + answer="x+2*y", + hints=hints) + + # Expect to receive a hint if we add an extra y + input_dict = {'1_2_1': "x + 2*y + y"} + correct_map = problem.grade_answers(input_dict) + self.assertEquals(correct_map.get_hint('1_2_1'), + 'Check the coefficient of y') + + # Expect to receive a hint if we leave out x + input_dict = {'1_2_1': "2*y"} + correct_map = problem.grade_answers(input_dict) + self.assertEquals(correct_map.get_hint('1_2_1'), + 'Try including the variable x') + + + def test_script(self): + # Calculate the answer using a script + script = "calculated_ans = 'x+x'" + + # Sample x in the range [-10,10] + sample_dict = {'x': (-10, 10)} + + # The expected solution is numerically equivalent to 2*x + problem = self.build_problem(sample_dict=sample_dict, + num_samples=10, + tolerance=0.01, + answer="$calculated_ans", + script=script) + + # Expect that the inputs are graded correctly + self.assert_grade(problem, '2*x', 'correct') + self.assert_grade(problem, '3*x', 'incorrect') + + +class StringResponseTest(ResponseTest): from response_xml_factory import StringResponseXMLFactory xml_factory_class = StringResponseXMLFactory