Commit f2bb9a2d by Will Daly

Implemented unit tests for <customresponse>

parent 8b5473b6
from lxml import etree
from abc import ABCMeta, abstractmethod
class ResponseXMLFactory(object):
""" Abstract base class for capa response XML factories.
Subclasses override create_response_element and
create_input_element to produce XML of particular response types"""
__metaclass__ = ABCMeta
@abstractmethod
def create_response_element(self, **kwargs):
""" Subclasses override to return an etree element
representing the capa response XML
(e.g. <numericalresponse>).
The tree should NOT contain any input elements
(such as <textline />) as these will be added later."""
return None
@abstractmethod
def create_input_element(self, **kwargs):
""" Subclasses override this to return an etree element
representing the capa input XML (such as <textline />)"""
return None
def build_xml(self, **kwargs):
""" Construct an XML string for a capa response
based on **kwargs.
**kwargs is a dictionary that will be passed
to create_response_element() and create_input_element().
See the subclasses below for other keyword arguments
you can specify.
For all response types, **kwargs can contain:
'question_text': The text of the question to display,
wrapped in <p> tags.
'explanation_text': The detailed explanation that will
be shown if the user answers incorrectly.
'script': The embedded Python script (a string)
'num_inputs': The number of input elements
to create [DEFAULT: 1]
Returns a string representation of the XML tree.
"""
# Retrieve keyward arguments
question_text = kwargs.get('question_text', '')
explanation_text = kwargs.get('explanation_text')
script = kwargs.get('script', None)
num_inputs = kwargs.get('num_inputs', 1)
# The root is <problem>
root = etree.Element("problem")
# Add a script if there is one
if script:
script_element = etree.SubElement(root, "script")
script_element.set("type", "loncapa/python")
script_element.text = str(script)
# The problem has a child <p> with question text
question = etree.SubElement(root, "p")
question.text = question_text
# Add the response
response_element = self.create_response_element(**kwargs)
root.append(response_element)
# Add input elements
for i in range(0, int(num_inputs)):
input_element = self.create_input_element(**kwargs)
response_element.append(input_element)
# The problem has an explanation of the solution
explanation = etree.SubElement(root, "solution")
explanation_div = etree.SubElement(explanation, "div")
explanation_div.set("class", "detailed-solution")
explanation_div.text = explanation_text
return etree.tostring(root)
@staticmethod
def textline_input_xml(**kwargs):
""" Create a <textline/> XML element
Uses **kwargs:
'math_display': If True, then includes a MathJax display of user input
'size': An integer representing the width of the text line
"""
math_display = kwargs.get('math_display', False)
size = kwargs.get('size', None)
input_element = etree.Element('textline')
if math_display:
input_element.set('math', '1')
if size:
input_element.set('size', str(size))
return input_element
class NumericalResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <numericalresponse> XML trees """
def create_response_element(self, **kwargs):
""" Create a <numericalresponse> XML element.
Uses **kwarg keys:
'answer': The correct answer (e.g. "5")
'tolerance': The tolerance within which a response
is considered correct. Can be a decimal (e.g. "0.01")
or percentage (e.g. "2%")
"""
answer = kwargs.get('answer', None)
tolerance = kwargs.get('tolerance', None)
response_element = etree.Element('numericalresponse')
if answer:
response_element.set('answer', str(answer))
if tolerance:
responseparam_element = etree.SubElement(response_element, 'responseparam')
responseparam_element.set('type', 'tolerance')
responseparam_element.set('default', str(tolerance))
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
class CustomResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <customresponse> XML trees """
def create_response_element(self, **kwargs):
""" Create a <customresponse> XML element.
Use **kwargs:
'cfn': the Python code to run. Can be inline code,
or the name of a function defined in earlier <script> tags.
Should have the form: cfn(expect, ans)
where expect is a value (see below)
and ans is a list of values.
'expect': The value passed as the first argument to the function cfn
'answer': Inline script that calculates the answer
"""
# Retrieve **kwargs
cfn = kwargs.get('cfn', None)
expect = kwargs.get('expect', None)
answer = kwargs.get('answer', None)
# Create the response element
response_element = etree.Element("customresponse")
if cfn:
response_element.set('cfn', str(cfn))
if expect:
response_element.set('expect', str(expect))
if answer:
answer_element = etree.SubElement(response_element, "answer")
answer_element.text = str(answer)
return response_element
def create_input_element(self, **kwargs):
return ResponseXMLFactory.textline_input_xml(**kwargs)
......@@ -493,3 +493,119 @@ class NumericalResponseTest(unittest.TestCase):
for input_str in incorrect_answers:
result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1')
self.assertEqual(result, 'incorrect')
from response_xml_factory import CustomResponseXMLFactory
class CustomResponseTest(unittest.TestCase):
def setUp(self):
self.xml_factory = CustomResponseXMLFactory()
def test_inline_code(self):
# For inline code, we directly modify global context variables
# 'answers' is a list of answers provided to us
# 'correct' is a list we fill in with True/False
# 'expect' is given to us (if provided in the XML)
inline_script = """correct[0] = 'correct' if (answers['1_2_1'] == expect) else 'incorrect'"""
xml = self.xml_factory.build_xml(answer=inline_script, expect="42")
problem = lcp.LoncapaProblem(xml, '1', system=test_system)
# Correct answer
input_dict = {'1_2_1': '42'}
result = problem.grade_answers(input_dict).get_correctness('1_2_1')
self.assertEqual(result, 'correct')
# Incorrect answer
input_dict = {'1_2_1': '0'}
result = problem.grade_answers(input_dict).get_correctness('1_2_1')
self.assertEqual(result, 'incorrect')
def test_inline_message(self):
# Inline code can update the global messages list
# to pass messages to the CorrectMap for a particular input
inline_script = """messages[0] = "Test Message" """
xml = self.xml_factory.build_xml(answer=inline_script)
problem = lcp.LoncapaProblem(xml, '1', system=test_system)
input_dict = {'1_2_1': '0'}
msg = problem.grade_answers(input_dict).get_msg('1_2_1')
self.assertEqual(msg, "Test Message")
def test_function_code(self):
# For function code, we pass in three arguments:
#
# 'expect' is the expect attribute of the <customresponse>
#
# 'answer_given' is the answer the student gave (if there is just one input)
# or an ordered list of answers (if there are multiple inputs)
#
# 'student_answers' is a dictionary of answers by input ID
#
#
# The function should return a dict of the form
# { 'ok': BOOL, 'msg': STRING }
#
script = """def check_func(expect, answer_given, student_answers):
return {'ok': answer_given == expect, 'msg': 'Message text'}"""
xml = self.xml_factory.build_xml(script=script, cfn="check_func", expect="42")
problem = lcp.LoncapaProblem(xml, '1', system=test_system)
# Correct answer
input_dict = {'1_2_1': '42'}
correct_map = problem.grade_answers(input_dict)
correctness = correct_map.get_correctness('1_2_1')
msg = correct_map.get_msg('1_2_1')
self.assertEqual(correctness, 'correct')
self.assertEqual(msg, "Message text\n")
# Incorrect answer
input_dict = {'1_2_1': '0'}
correct_map = problem.grade_answers(input_dict)
correctness = correct_map.get_correctness('1_2_1')
msg = correct_map.get_msg('1_2_1')
self.assertEqual(correctness, 'incorrect')
self.assertEqual(msg, "Message text\n")
def test_multiple_inputs(self):
# When given multiple inputs, the 'answer_given' argument
# to the check_func() is a list of inputs
# The sample script below marks the problem as correct
# if and only if it receives answer_given=[1,2,3]
# (or string values ['1','2','3'])
script = """def check_func(expect, answer_given, student_answers):
check1 = (int(answer_given[0]) == 1)
check2 = (int(answer_given[1]) == 2)
check3 = (int(answer_given[2]) == 3)
return {'ok': (check1 and check2 and check3), 'msg': 'Message text'}"""
xml = self.xml_factory.build_xml(script=script,
cfn="check_func", num_inputs=3)
problem = lcp.LoncapaProblem(xml, '1', system=test_system)
# Grade the inputs (one input incorrect)
input_dict = {'1_2_1': '-999', '1_2_2': '2', '1_2_3': '3' }
correct_map = problem.grade_answers(input_dict)
# Everything marked incorrect
self.assertEqual(correct_map.get_correctness('1_2_1'), 'incorrect')
self.assertEqual(correct_map.get_correctness('1_2_2'), 'incorrect')
self.assertEqual(correct_map.get_correctness('1_2_3'), 'incorrect')
# Grade the inputs (everything correct)
input_dict = {'1_2_1': '1', '1_2_2': '2', '1_2_3': '3' }
correct_map = problem.grade_answers(input_dict)
# Everything marked incorrect
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
self.assertEqual(correct_map.get_correctness('1_2_2'), 'correct')
self.assertEqual(correct_map.get_correctness('1_2_3'), 'correct')
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