Commit 4d880db1 by RobertMarks

Updated behavior for split_answer_dict, get_score, and check_student_inputs (responsetypes.py)

parent 46ae2f9c
...@@ -460,10 +460,10 @@ class JSInput(InputTypeBase): ...@@ -460,10 +460,10 @@ class JSInput(InputTypeBase):
DO NOT USE! HAS NOT BEEN TESTED BEYOND 700X PROBLEMS, AND MAY CHANGE IN DO NOT USE! HAS NOT BEEN TESTED BEYOND 700X PROBLEMS, AND MAY CHANGE IN
BACKWARDS-INCOMPATIBLE WAYS. BACKWARDS-INCOMPATIBLE WAYS.
Inputtype for general javascript inputs. Intended to be used with Inputtype for general javascript inputs. Intended to be used with
customresponse. customresponse.
Loads in a sandboxed iframe to help prevent css and js conflicts between Loads in a sandboxed iframe to help prevent css and js conflicts between
frame and top-level window. frame and top-level window.
iframe sandbox whitelist: iframe sandbox whitelist:
- allow-scripts - allow-scripts
- allow-popups - allow-popups
...@@ -474,9 +474,9 @@ class JSInput(InputTypeBase): ...@@ -474,9 +474,9 @@ class JSInput(InputTypeBase):
window elements. window elements.
Example: Example:
<jsinput html_file="/static/test.html" <jsinput html_file="/static/test.html"
gradefn="grade" gradefn="grade"
height="500" height="500"
width="400"/> width="400"/>
See the documentation in the /doc/public folder for more information. See the documentation in the /doc/public folder for more information.
...@@ -500,7 +500,7 @@ class JSInput(InputTypeBase): ...@@ -500,7 +500,7 @@ class JSInput(InputTypeBase):
Attribute('width', "400"), # iframe width Attribute('width', "400"), # iframe width
Attribute('height', "300")] # iframe height Attribute('height', "300")] # iframe height
def _extra_context(self): def _extra_context(self):
context = { context = {
...@@ -510,11 +510,12 @@ class JSInput(InputTypeBase): ...@@ -510,11 +510,12 @@ class JSInput(InputTypeBase):
return context return context
registry.register(JSInput) registry.register(JSInput)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
class TextLine(InputTypeBase): class TextLine(InputTypeBase):
""" """
A text line input. Can do math preview if "math"="1" is specified. A text line input. Can do math preview if "math"="1" is specified.
...@@ -1373,8 +1374,6 @@ registry.register(AnnotationInput) ...@@ -1373,8 +1374,6 @@ registry.register(AnnotationInput)
class ChoiceTextGroup(InputTypeBase): class ChoiceTextGroup(InputTypeBase):
""" """
Groups of radiobutton/checkboxes with text inputs. Groups of radiobutton/checkboxes with text inputs.
Allows for a "not enough information" option to be added
to problems with numerical answers.
Examples: Examples:
RadioButton problem RadioButton problem
......
...@@ -2256,7 +2256,6 @@ class ChoiceTextResponse(LoncapaResponse): ...@@ -2256,7 +2256,6 @@ class ChoiceTextResponse(LoncapaResponse):
binary_choices, numtolerance_inputs = self._split_answers_dict(answer_dict) binary_choices, numtolerance_inputs = self._split_answers_dict(answer_dict)
# Check the binary choices first. # Check the binary choices first.
choices_correct = self._check_student_choices(binary_choices) choices_correct = self._check_student_choices(binary_choices)
inputs_correct = True
inputs_correct = self._check_student_inputs(numtolerance_inputs) inputs_correct = self._check_student_inputs(numtolerance_inputs)
# Only return correct if the student got both the binary # Only return correct if the student got both the binary
# and numtolerance_inputs are correct # and numtolerance_inputs are correct
...@@ -2376,7 +2375,6 @@ class ChoiceTextResponse(LoncapaResponse): ...@@ -2376,7 +2375,6 @@ class ChoiceTextResponse(LoncapaResponse):
Returns True if and only if all student inputs are correct. Returns True if and only if all student inputs are correct.
""" """
inputs_correct = True inputs_correct = True
for answer_name, answer_value in numtolerance_inputs.iteritems(): for answer_name, answer_value in numtolerance_inputs.iteritems():
# If `self.corrrect_inputs` does not contain an entry for # If `self.corrrect_inputs` does not contain an entry for
......
...@@ -857,15 +857,15 @@ class ChoiceTextResponseXMLFactory(ResponseXMLFactory): ...@@ -857,15 +857,15 @@ class ChoiceTextResponseXMLFactory(ResponseXMLFactory):
choice_element.set("correct", correct) choice_element.set("correct", correct)
choice_element.text = text choice_element.text = text
for inp in inputs: for inp in inputs:
# Add all of the inputs as children of this element # Add all of the inputs as children of this choice
choice_element.append(inp) choice_element.append(inp)
return choice_element return choice_element
def _create_numtolerance_input_element(self, params): def _create_numtolerance_input_element(self, params):
""" """
Creates a <numtolerance_input/> element with optionally Creates a <numtolerance_input/> or <decoy_input/> element with
specified tolerance and answer. optionally specified tolerance and answer.
""" """
answer = params['answer'] if 'answer' in params else None answer = params['answer'] if 'answer' in params else None
# If there is not an answer specified, Then create a <decoy_input/> # If there is not an answer specified, Then create a <decoy_input/>
......
...@@ -1432,54 +1432,15 @@ class AnnotationResponseTest(ResponseTest): ...@@ -1432,54 +1432,15 @@ class AnnotationResponseTest(ResponseTest):
class ChoiceTextResponseTest(ResponseTest): class ChoiceTextResponseTest(ResponseTest):
"""
Class containing setup and tests for ChoiceText responsetype.
"""
from response_xml_factory import ChoiceTextResponseXMLFactory from response_xml_factory import ChoiceTextResponseXMLFactory
xml_factory_class = ChoiceTextResponseXMLFactory xml_factory_class = ChoiceTextResponseXMLFactory
one_choice_one_input = lambda itype, inst: inst._make_problem( # `TEST_INPUTS` is a dictionary mapping from
("true", {"answer": "123", "tolerance": "1"}), # test_name to a representation of inputs for a test problem.
itype
)
one_choice_two_inputs = lambda itype, inst: inst._make_problem(
[("true", ({"answer": "123", "tolerance": "1"},
{"answer": "456", "tolerance": "10"}))
],
itype
)
one_input_script = lambda itype, inst: inst._make_problem(
("true", {"answer": "$computed_response", "tolerance": "1"}),
itype,
"computed_response = math.sqrt(4)"
)
one_choice_no_input = lambda itype, inst: inst._make_problem(
("true", {}),
itype
)
two_choices_no_inputs = lambda itype, inst: inst._make_problem(
[("false", {}), ("true", {})],
itype
)
two_choices_one_input_1 = lambda itype, inst: inst._make_problem(
[("false", {}), ("true", {"answer": "123", "tolerance": "0"})],
itype
)
two_choices_one_input_2 = lambda itype, inst: inst._make_problem(
[("true", {}), ("false", {"answer": "123", "tolerance": "0"})],
itype
)
two_choices_two_inputs = lambda itype, inst: inst._make_problem(
[("true", {"answer": "123", "tolerance": "0"}),
("false", {"answer": "999", "tolerance": "0"})],
itype
)
TEST_INPUTS = { TEST_INPUTS = {
"1_choice_0_input_correct": [(True, [])], "1_choice_0_input_correct": [(True, [])],
"1_choice_0_input_incorrect": [(False, [])], "1_choice_0_input_incorrect": [(False, [])],
...@@ -1511,6 +1472,10 @@ class ChoiceTextResponseTest(ResponseTest): ...@@ -1511,6 +1472,10 @@ class ChoiceTextResponseTest(ResponseTest):
"2_choices_2_inputs_wrong_input": [(True, ["321"]), (False, [])] "2_choices_2_inputs_wrong_input": [(True, ["321"]), (False, [])]
} }
# `TEST_SCENARIOS` is a dictionary of the form
# {Test_Name" : (Test_Problem_name, correctness)}
# correctness represents whether the problem should be graded as
# correct or incorrect when the test is run.
TEST_SCENARIOS = { TEST_SCENARIOS = {
"1_choice_0_input_correct": ("1_choice_0_input", "correct"), "1_choice_0_input_correct": ("1_choice_0_input", "correct"),
"1_choice_0_input_incorrect": ("1_choice_0_input", "incorrect"), "1_choice_0_input_incorrect": ("1_choice_0_input", "incorrect"),
...@@ -1542,15 +1507,53 @@ class ChoiceTextResponseTest(ResponseTest): ...@@ -1542,15 +1507,53 @@ class ChoiceTextResponseTest(ResponseTest):
"2_choices_2_inputs_wrong_input": ("2_choices_2_inputs", "incorrect") "2_choices_2_inputs_wrong_input": ("2_choices_2_inputs", "incorrect")
} }
TEST_PROBLEMS = { # Dictionary that maps from problem_name to arguments for
"1_choice_0_input": one_choice_no_input, # _make_problem, that will create the problem.
"1_choice_1_input": one_choice_one_input, TEST_PROBLEM_ARGS = {
"1_input_script": one_input_script, "1_choice_0_input": {"choices": ("true", {}), "script": ''},
"1_choice_2_inputs": one_choice_two_inputs, "1_choice_1_input": {
"2_choices_0_inputs": two_choices_no_inputs, "choices": ("true", {"answer": "123", "tolerance": "1"}),
"2_choices_1_input_1": two_choices_one_input_1, "script": ''
"2_choices_1_input_2": two_choices_one_input_2, },
"2_choices_2_inputs": two_choices_two_inputs
"1_input_script": {
"choices": ("true", {"answer": "$computed_response", "tolerance": "1"}),
"script": "computed_response = math.sqrt(4)"
},
"1_choice_2_inputs": {
"choices": [
(
"true", (
{"answer": "123", "tolerance": "1"},
{"answer": "456", "tolerance": "10"}
)
)
],
"script": ''
},
"2_choices_0_inputs": {
"choices": [("false", {}), ("true", {})],
"script": ''
},
"2_choices_1_input_1": {
"choices": [
("false", {}), ("true", {"answer": "123", "tolerance": "0"})
],
"script": ''
},
"2_choices_1_input_2": {
"choices": [("true", {}), ("false", {"answer": "123", "tolerance": "0"})],
"script": ''
},
"2_choices_2_inputs": {
"choices": [
("true", {"answer": "123", "tolerance": "0"}),
("false", {"answer": "999", "tolerance": "0"})
],
"script": ''
}
} }
def _make_problem(self, choices, in_type='radiotextgroup', script=''): def _make_problem(self, choices, in_type='radiotextgroup', script=''):
...@@ -1598,26 +1601,69 @@ class ChoiceTextResponseTest(ResponseTest): ...@@ -1598,26 +1601,69 @@ class ChoiceTextResponseTest(ResponseTest):
return answer_dict return answer_dict
def test_invalid_xml(self): def test_invalid_xml(self):
"""
Test that build problem raises errors for invalid options
"""
with self.assertRaises(Exception): with self.assertRaises(Exception):
self.build_problem(type="invalidtextgroup") self.build_problem(type="invalidtextgroup")
def test_valid_xml(self): def test_valid_xml(self):
"""
Test that `build_problem` builds valid xml
"""
self.build_problem() self.build_problem()
self.assertTrue(True) self.assertTrue(True)
def test_unchecked_input_not_validated(self):
"""
Test that a student can have a non numeric answer in an unselected
choice without causing an error to be raised when the problem is
checked.
"""
two_choice_two_input = self._make_problem(
[
("true", {"answer": "123", "tolerance": "1"}),
("false", {})
],
"checkboxtextgroup"
)
self.assert_grade(
two_choice_two_input,
self._make_answer_dict([(True, ["1"]), (False, ["Platypus"])]),
"incorrect"
)
def test_interpret_error(self): def test_interpret_error(self):
one_choice_one_input = lambda itype: self._make_problem( """
("true", {"answer": "123", "tolerance": "1"}), Test that student answers that cannot be interpeted as numbers
itype cause the response type to raise an error.
"""
two_choice_two_input = self._make_problem(
[
("true", {"answer": "123", "tolerance": "1"}),
("false", {})
],
"checkboxtextgroup"
) )
with self.assertRaisesRegexp(StudentInputError, "Could not interpret"): with self.assertRaisesRegexp(StudentInputError, "Could not interpret"):
# Test that error is raised for input in selected correct choice.
self.assert_grade( self.assert_grade(
one_choice_one_input('radiotextgroup'), two_choice_two_input,
self._make_answer_dict([(True, ["Platypus"])]), self._make_answer_dict([(True, ["Platypus"])]),
"correct" "correct"
) )
with self.assertRaisesRegexp(StudentInputError, "Could not interpret"):
# Test that error is raised for input in selected incorrect choice.
self.assert_grade(
two_choice_two_input,
self._make_answer_dict([(True, ["1"]), (True, ["Platypus"])]),
"correct"
)
def test_staff_answer_error(self): def test_staff_answer_error(self):
broken_problem = self._make_problem( broken_problem = self._make_problem(
[("true", {"answer": "Platypus", "tolerance": "0"}), [("true", {"answer": "Platypus", "tolerance": "0"}),
...@@ -1638,14 +1684,26 @@ class ChoiceTextResponseTest(ResponseTest): ...@@ -1638,14 +1684,26 @@ class ChoiceTextResponseTest(ResponseTest):
) )
def test_radio_grades(self): def test_radio_grades(self):
"""
Test that confirms correct operation of grading when the inputtag is
radiotextgroup.
"""
for name, inputs in self.TEST_INPUTS.iteritems(): for name, inputs in self.TEST_INPUTS.iteritems():
# Turn submission into the form expected when grading this problem.
submission = self._make_answer_dict(inputs) submission = self._make_answer_dict(inputs)
# Lookup the problem_name, and the whether this test problem
# and inputs should be graded as correct or incorrect.
problem_name, correctness = self.TEST_SCENARIOS[name] problem_name, correctness = self.TEST_SCENARIOS[name]
problem = self.TEST_PROBLEMS[problem_name] # Load the args needed to build the problem for this test.
problem_args = self.TEST_PROBLEM_ARGS[problem_name]
test_choices = problem_args["choices"]
test_script = problem_args["script"]
# Build the actual problem for the test.
test_problem = self._make_problem(test_choices, 'radiotextgroup', test_script)
# Make sure the actual grade matches the expected grade.
self.assert_grade( self.assert_grade(
problem('radiotextgroup', self), test_problem,
submission, submission,
correctness, correctness,
msg="{0} should be {1}".format( msg="{0} should be {1}".format(
...@@ -1655,9 +1713,16 @@ class ChoiceTextResponseTest(ResponseTest): ...@@ -1655,9 +1713,16 @@ class ChoiceTextResponseTest(ResponseTest):
) )
def test_checkbox_grades(self): def test_checkbox_grades(self):
"""
Test that confirms correct operation of grading when the inputtag is
checkboxtextgroup.
"""
# Dictionary from name of test_scenario to (problem_name, correctness)
# Correctness is used to test whether the problem was graded properly
scenarios = { scenarios = {
"2_choices_correct": ("checkbox_two_choices", "correct"), "2_choices_correct": ("checkbox_two_choices", "correct"),
"2_choices_incorrect": ("checkbox_two_choices", "incorrect"), "2_choices_incorrect": ("checkbox_two_choices", "incorrect"),
"2_choices_2_inputs_correct": ( "2_choices_2_inputs_correct": (
"checkbox_2_choices_2_inputs", "checkbox_2_choices_2_inputs",
"correct" "correct"
...@@ -1673,6 +1738,7 @@ class ChoiceTextResponseTest(ResponseTest): ...@@ -1673,6 +1738,7 @@ class ChoiceTextResponseTest(ResponseTest):
"incorrect" "incorrect"
) )
} }
# Dictionary scenario_name: test_inputs
inputs = { inputs = {
"2_choices_correct": [(True, []), (True, [])], "2_choices_correct": [(True, []), (True, [])],
"2_choices_incorrect": [(True, []), (False, [])], "2_choices_incorrect": [(True, []), (False, [])],
...@@ -1685,9 +1751,11 @@ class ChoiceTextResponseTest(ResponseTest): ...@@ -1685,9 +1751,11 @@ class ChoiceTextResponseTest(ResponseTest):
] ]
} }
# Two choice zero input problem with both choices being correct.
checkbox_two_choices = self._make_problem( checkbox_two_choices = self._make_problem(
[("true", {}), ("true", {})], "checkboxtextgroup" [("true", {}), ("true", {})], "checkboxtextgroup"
) )
# Two choice two input problem with both choices correct.
checkbox_two_choices_two_inputs = self._make_problem( checkbox_two_choices_two_inputs = self._make_problem(
[("true", {"answer": "123", "tolerance": "0"}), [("true", {"answer": "123", "tolerance": "0"}),
("true", {"answer": "456", "tolerance": "0"}) ("true", {"answer": "456", "tolerance": "0"})
...@@ -1695,17 +1763,20 @@ class ChoiceTextResponseTest(ResponseTest): ...@@ -1695,17 +1763,20 @@ class ChoiceTextResponseTest(ResponseTest):
"checkboxtextgroup" "checkboxtextgroup"
) )
# Dictionary problem_name: problem
problems = { problems = {
"checkbox_two_choices": checkbox_two_choices, "checkbox_two_choices": checkbox_two_choices,
"checkbox_2_choices_2_inputs": checkbox_two_choices_two_inputs "checkbox_2_choices_2_inputs": checkbox_two_choices_two_inputs
} }
problems.update(self.TEST_PROBLEMS)
for name, inputs in inputs.iteritems(): for name, inputs in inputs.iteritems():
submission = self._make_answer_dict(inputs) submission = self._make_answer_dict(inputs)
# Load the test problem's name and desired correctness
problem_name, correctness = scenarios[name] problem_name, correctness = scenarios[name]
# Load the problem
problem = problems[problem_name] problem = problems[problem_name]
# Make sure the actual grade matches the expected grade
self.assert_grade( self.assert_grade(
problem, problem,
submission, submission,
......
...@@ -820,10 +820,11 @@ class CapaModule(CapaFields, XModule): ...@@ -820,10 +820,11 @@ class CapaModule(CapaFields, XModule):
elif is_dict_key: elif is_dict_key:
try: try:
val = json.loads(data[key]) val = json.loads(data[key])
# If the submission wasn't deserializable, raise an error.
except(KeyError, ValueError): except(KeyError, ValueError):
# Send this information along to be reported by raise ValueError(
# The grading method u"Invalid submission: {val} for {key}".format(val=data[key], key=key)
val = {"error": "error"} )
else: else:
val = data[key] val = data[key]
......
...@@ -388,8 +388,9 @@ def assert_choicetext_values(problem_type, choices, expected_values): ...@@ -388,8 +388,9 @@ def assert_choicetext_values(problem_type, choices, expected_values):
Asserts that only the given choices are checked, and given Asserts that only the given choices are checked, and given
text fields have a desired value text fields have a desired value
""" """
# Names of the radio buttons or checkboxes
all_choices = ['choiceinput_0bc', 'choiceinput_1bc'] all_choices = ['choiceinput_0bc', 'choiceinput_1bc']
# Names of the numtolerance_inputs
all_inputs = [ all_inputs = [
"choiceinput_0_numtolerance_input_0", "choiceinput_0_numtolerance_input_0",
"choiceinput_1_numtolerance_input_0" "choiceinput_1_numtolerance_input_0"
......
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