diff --git a/common/lib/capa/capa/tests/response_xml_factory.py b/common/lib/capa/capa/tests/response_xml_factory.py new file mode 100644 index 0000000000..fe918ec5db --- /dev/null +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -0,0 +1,668 @@ +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. ). + + The tree should NOT contain any input elements + (such as ) 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 )""" + 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

tags. + + *explanation_text*: The detailed explanation that will + be shown if the user answers incorrectly. + + *script*: The embedded Python script (a string) + + *num_responses*: The number of responses to create [DEFAULT: 1] + + *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_responses = kwargs.get('num_responses', 1) + num_inputs = kwargs.get('num_inputs', 1) + + # The root is + 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

with question text + question = etree.SubElement(root, "p") + question.text = question_text + + # Add the response(s) + for i in range(0, int(num_responses)): + response_element = self.create_response_element(**kwargs) + root.append(response_element) + + # Add input elements + for j in range(0, int(num_inputs)): + input_element = self.create_input_element(**kwargs) + if not (None == input_element): + response_element.append(input_element) + + # The problem has an explanation of the solution + if explanation_text: + 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 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 + + @staticmethod + def choicegroup_input_xml(**kwargs): + """ Create a XML element + + Uses **kwargs: + + *choice_type*: Can be "checkbox", "radio", or "multiple" + + *choices*: List of True/False values indicating whether + a particular choice is correct or not. + Users must choose *all* correct options in order + to be marked correct. + DEFAULT: [True] + + *choice_names": List of strings identifying the choices. + If specified, you must ensure that + len(choice_names) == len(choices) + """ + # Names of group elements + group_element_names = {'checkbox': 'checkboxgroup', + 'radio': 'radiogroup', + 'multiple': 'choicegroup' } + + # Retrieve **kwargs + choices = kwargs.get('choices', [True]) + choice_type = kwargs.get('choice_type', 'multiple') + choice_names = kwargs.get('choice_names', [None] * len(choices)) + + # Create the , , or element + assert(choice_type in group_element_names) + group_element = etree.Element(group_element_names[choice_type]) + + # Create the elements + for (correct_val, name) in zip(choices, choice_names): + choice_element = etree.SubElement(group_element, "choice") + choice_element.set("correct", "true" if correct_val else "false") + + # Add some text describing the choice + etree.SubElement(choice_element, "startouttext") + etree.text = "Choice description" + etree.SubElement(choice_element, "endouttext") + + # Add a name identifying the choice, if one exists + if name: + choice_element.set("name", str(name)) + + return group_element + + +class NumericalResponseXMLFactory(ResponseXMLFactory): + """ Factory for producing XML trees """ + + def create_response_element(self, **kwargs): + """ Create a 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 XML trees """ + + def create_response_element(self, **kwargs): + """ Create a XML element. + + Uses **kwargs: + + *cfn*: the Python code to run. Can be inline code, + or the name of a function defined in earlier - - -

Hints can be provided to students, based on the last response given, as well as the history of responses given. Here is an example of a hint produced by a Formula Response problem.

- -

-What is the equation of the line which passess through ($x1,$y1) and -($x2,$y2)?

- -

The correct answer is $answer. A common error is to invert the equation for the slope. Enter -$wrongans to see a hint.

- - - - - - y = - - - - - You have inverted the slope in the question. - - - - - diff --git a/common/lib/capa/capa/tests/test_files/imageresponse.xml b/common/lib/capa/capa/tests/test_files/imageresponse.xml deleted file mode 100644 index 41c9f01218..0000000000 --- a/common/lib/capa/capa/tests/test_files/imageresponse.xml +++ /dev/null @@ -1,40 +0,0 @@ - -

-Two skiers are on frictionless black diamond ski slopes. -Hello

- - - -Click on the image where the top skier will stop momentarily if the top skier starts from rest. - -Click on the image where the lower skier will stop momentarily if the lower skier starts from rest. - -Click on either of the two positions as discussed previously. - -Click on either of the two positions as discussed previously. - -Click on either of the two positions as discussed previously. - -

Use conservation of energy.

-
-
- - - - - - - -Click on either of the two positions as discussed previously. - -Click on either of the two positions as discussed previously. - - -Click on either of the two positions as discussed previously. - -

Use conservation of energy.

-
-
- - -
diff --git a/common/lib/capa/capa/tests/test_files/javascriptresponse.xml b/common/lib/capa/capa/tests/test_files/javascriptresponse.xml deleted file mode 100644 index 439866e62c..0000000000 --- a/common/lib/capa/capa/tests/test_files/javascriptresponse.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - diff --git a/common/lib/capa/capa/tests/test_files/js/compiled/c9a9cd4242d84c924fe5f8324e9ae79d.js b/common/lib/capa/capa/tests/test_files/js/compiled/c9a9cd4242d84c924fe5f8324e9ae79d.js deleted file mode 100644 index 6670c6a09a..0000000000 --- a/common/lib/capa/capa/tests/test_files/js/compiled/c9a9cd4242d84c924fe5f8324e9ae79d.js +++ /dev/null @@ -1,50 +0,0 @@ -// Generated by CoffeeScript 1.3.3 -(function() { - var MinimaxProblemDisplay, root, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - - MinimaxProblemDisplay = (function(_super) { - - __extends(MinimaxProblemDisplay, _super); - - function MinimaxProblemDisplay(state, submission, evaluation, container, submissionField, parameters) { - this.state = state; - this.submission = submission; - this.evaluation = evaluation; - this.container = container; - this.submissionField = submissionField; - this.parameters = parameters != null ? parameters : {}; - MinimaxProblemDisplay.__super__.constructor.call(this, this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters); - } - - MinimaxProblemDisplay.prototype.render = function() {}; - - MinimaxProblemDisplay.prototype.createSubmission = function() { - var id, value, _ref, _results; - this.newSubmission = {}; - if (this.submission != null) { - _ref = this.submission; - _results = []; - for (id in _ref) { - value = _ref[id]; - _results.push(this.newSubmission[id] = value); - } - return _results; - } - }; - - MinimaxProblemDisplay.prototype.getCurrentSubmission = function() { - return this.newSubmission; - }; - - return MinimaxProblemDisplay; - - })(XProblemDisplay); - - root = typeof exports !== "undefined" && exports !== null ? exports : this; - - root.TestProblemDisplay = TestProblemDisplay; - -}).call(this); -; diff --git a/common/lib/capa/capa/tests/test_files/js/compiled/javascriptresponse.js b/common/lib/capa/capa/tests/test_files/js/compiled/javascriptresponse.js deleted file mode 100644 index 6670c6a09a..0000000000 --- a/common/lib/capa/capa/tests/test_files/js/compiled/javascriptresponse.js +++ /dev/null @@ -1,50 +0,0 @@ -// Generated by CoffeeScript 1.3.3 -(function() { - var MinimaxProblemDisplay, root, - __hasProp = {}.hasOwnProperty, - __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; - - MinimaxProblemDisplay = (function(_super) { - - __extends(MinimaxProblemDisplay, _super); - - function MinimaxProblemDisplay(state, submission, evaluation, container, submissionField, parameters) { - this.state = state; - this.submission = submission; - this.evaluation = evaluation; - this.container = container; - this.submissionField = submissionField; - this.parameters = parameters != null ? parameters : {}; - MinimaxProblemDisplay.__super__.constructor.call(this, this.state, this.submission, this.evaluation, this.container, this.submissionField, this.parameters); - } - - MinimaxProblemDisplay.prototype.render = function() {}; - - MinimaxProblemDisplay.prototype.createSubmission = function() { - var id, value, _ref, _results; - this.newSubmission = {}; - if (this.submission != null) { - _ref = this.submission; - _results = []; - for (id in _ref) { - value = _ref[id]; - _results.push(this.newSubmission[id] = value); - } - return _results; - } - }; - - MinimaxProblemDisplay.prototype.getCurrentSubmission = function() { - return this.newSubmission; - }; - - return MinimaxProblemDisplay; - - })(XProblemDisplay); - - root = typeof exports !== "undefined" && exports !== null ? exports : this; - - root.TestProblemDisplay = TestProblemDisplay; - -}).call(this); -; diff --git a/common/lib/capa/capa/tests/test_files/multi_bare.xml b/common/lib/capa/capa/tests/test_files/multi_bare.xml deleted file mode 100644 index 20bc8f853d..0000000000 --- a/common/lib/capa/capa/tests/test_files/multi_bare.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - This is foil One. - - - This is foil Two. - - - This is foil Three. - - - This is foil Four. - - - This is foil Five. - - - - diff --git a/common/lib/capa/capa/tests/test_files/multichoice.xml b/common/lib/capa/capa/tests/test_files/multichoice.xml deleted file mode 100644 index 60bf02ec59..0000000000 --- a/common/lib/capa/capa/tests/test_files/multichoice.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - This is foil One. - - - This is foil Two. - - - This is foil Three. - - - This is foil Four. - - - This is foil Five. - - - - diff --git a/common/lib/capa/capa/tests/test_files/optionresponse.xml b/common/lib/capa/capa/tests/test_files/optionresponse.xml deleted file mode 100644 index 99a17e8fac..0000000000 --- a/common/lib/capa/capa/tests/test_files/optionresponse.xml +++ /dev/null @@ -1,63 +0,0 @@ - - -

-Why do bicycles benefit from having larger wheels when going up a bump as shown in the picture?
-Assume that for both bicycles:
-1.) The tires have equal air pressure.
-2.) The bicycles never leave the contact with the bump.
-3.) The bicycles have the same mass. The bicycle tires (regardless of size) have the same mass.
-

-
- -
    -
  • - -

    The bicycles with larger wheels have more time to go over the bump. This decreases the magnitude of the force needed to lift the bicycle.

    -
    - - -
  • -
  • - -

    The bicycles with larger wheels always have a smaller vertical displacement regardless of speed.

    -
    - - -
  • -
  • - -

    The bicycles with larger wheels experience a force backward with less magnitude for the same amount of time.

    -
    - - -
  • -
  • - -

    The bicycles with larger wheels experience a force backward with less magnitude for a greater amount of time.

    -
    - - -
  • -
  • - -

    The bicycles with larger wheels have more kinetic energy turned into gravitational potential energy.

    -
    - - -
  • -
  • - -

    The bicycles with larger wheels have more rotational kinetic energy, so the horizontal velocity of the biker changes less.

    -
    - - -
  • -
- - -
-
-
-
-
-
diff --git a/common/lib/capa/capa/tests/test_files/stringresponse_with_hint.xml b/common/lib/capa/capa/tests/test_files/stringresponse_with_hint.xml deleted file mode 100644 index 86efdf0f18..0000000000 --- a/common/lib/capa/capa/tests/test_files/stringresponse_with_hint.xml +++ /dev/null @@ -1,25 +0,0 @@ - -

Example: String Response Problem

-
-
- - Which US state has Lansing as its capital? - - - - - - - - - The state capital of Wisconsin is Madison. - - - The state capital of Minnesota is St. Paul. - - - The state you are looking for is also known as the 'Great Lakes State' - - - -
diff --git a/common/lib/capa/capa/tests/test_files/symbolicresponse.xml b/common/lib/capa/capa/tests/test_files/symbolicresponse.xml deleted file mode 100644 index 4dc2bc9d7b..0000000000 --- a/common/lib/capa/capa/tests/test_files/symbolicresponse.xml +++ /dev/null @@ -1,29 +0,0 @@ - - -

Example: Symbolic Math Response Problem

- -

-A symbolic math response problem presents one or more symbolic math -input fields for input. Correctness of input is evaluated based on -the symbolic properties of the expression entered. The student enters -text, but sees a proper symbolic rendition of the entered formula, in -real time, next to the input box. -

- -

This is a correct answer which may be entered below:

-

cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]

- - - Compute [mathjax] U = \exp\left( i \theta \left[ \begin{matrix} 0 & 1 \\ 1 & 0 \end{matrix} \right] \right) [/mathjax] - and give the resulting \(2 \times 2\) matrix.
- Your input should be typed in as a list of lists, eg [[1,2],[3,4]].
- [mathjax]U=[/mathjax] - - -
-
- -
-
diff --git a/common/lib/capa/capa/tests/test_files/truefalse.xml b/common/lib/capa/capa/tests/test_files/truefalse.xml deleted file mode 100644 index 60018f7a2d..0000000000 --- a/common/lib/capa/capa/tests/test_files/truefalse.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - This is foil One. - - - This is foil Two. - - - This is foil Three. - - - This is foil Four. - - - This is foil Five. - - - - diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 18da338b91..33b84d213d 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -16,93 +16,151 @@ from capa.correctmap import CorrectMap from capa.util import convert_files_to_filenames from capa.xqueue_interface import dateformat +class ResponseTest(unittest.TestCase): + """ Base class for tests of capa responses.""" + + xml_factory_class = None -class MultiChoiceTest(unittest.TestCase): - def test_MC_grade(self): - multichoice_file = os.path.dirname(__file__) + "/test_files/multichoice.xml" - test_lcp = lcp.LoncapaProblem(open(multichoice_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': 'choice_foil3'} - self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') - false_answers = {'1_2_1': 'choice_foil2'} - self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') + def setUp(self): + if self.xml_factory_class: + self.xml_factory = self.xml_factory_class() - def test_MC_bare_grades(self): - multichoice_file = os.path.dirname(__file__) + "/test_files/multi_bare.xml" - test_lcp = lcp.LoncapaProblem(open(multichoice_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': 'choice_2'} - self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') - false_answers = {'1_2_1': 'choice_1'} - self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') + def build_problem(self, **kwargs): + xml = self.xml_factory.build_xml(**kwargs) + return lcp.LoncapaProblem(xml, '1', system=test_system) - def test_TF_grade(self): - truefalse_file = os.path.dirname(__file__) + "/test_files/truefalse.xml" - test_lcp = lcp.LoncapaProblem(open(truefalse_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': ['choice_foil2', 'choice_foil1']} - self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') - false_answers = {'1_2_1': ['choice_foil1']} - self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') - false_answers = {'1_2_1': ['choice_foil1', 'choice_foil3']} - self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') - false_answers = {'1_2_1': ['choice_foil3']} - self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') - false_answers = {'1_2_1': ['choice_foil1', 'choice_foil2', 'choice_foil3']} - self.assertEquals(test_lcp.grade_answers(false_answers).get_correctness('1_2_1'), 'incorrect') + def assert_grade(self, problem, submission, expected_correctness): + input_dict = {'1_2_1': submission} + correct_map = problem.grade_answers(input_dict) + self.assertEquals(correct_map.get_correctness('1_2_1'), expected_correctness) + + def assert_multiple_grade(self, problem, correct_answers, incorrect_answers): + for input_str in correct_answers: + result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1') + self.assertEqual(result, 'correct', + msg="%s should be marked correct" % str(input_str)) + + for input_str in incorrect_answers: + result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1') + self.assertEqual(result, 'incorrect', + msg="%s should be marked incorrect" % str(input_str)) + +class MultiChoiceResponseTest(ResponseTest): + from response_xml_factory import MultipleChoiceResponseXMLFactory + xml_factory_class = MultipleChoiceResponseXMLFactory + + def test_multiple_choice_grade(self): + problem = self.build_problem(choices=[False, True, False]) + + # Ensure that we get the expected grades + self.assert_grade(problem, 'choice_0', 'incorrect') + self.assert_grade(problem, 'choice_1', 'correct') + self.assert_grade(problem, 'choice_2', 'incorrect') + + def test_named_multiple_choice_grade(self): + problem = self.build_problem(choices=[False, True, False], + choice_names=["foil_1", "foil_2", "foil_3"]) + + # Ensure that we get the expected grades + self.assert_grade(problem, 'choice_foil_1', 'incorrect') + self.assert_grade(problem, 'choice_foil_2', 'correct') + self.assert_grade(problem, 'choice_foil_3', 'incorrect') -class ImageResponseTest(unittest.TestCase): - def test_ir_grade(self): - imageresponse_file = os.path.dirname(__file__) + "/test_files/imageresponse.xml" - test_lcp = lcp.LoncapaProblem(open(imageresponse_file).read(), '1', system=test_system) - # testing regions only - correct_answers = { - #regions - '1_2_1': '(490,11)-(556,98)', - '1_2_2': '(242,202)-(296,276)', - '1_2_3': '(490,11)-(556,98);(242,202)-(296,276)', - '1_2_4': '(490,11)-(556,98);(242,202)-(296,276)', - '1_2_5': '(490,11)-(556,98);(242,202)-(296,276)', - #testing regions and rectanges - '1_3_1': 'rectangle="(490,11)-(556,98)" \ - regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', - '1_3_2': 'rectangle="(490,11)-(556,98)" \ - regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', - '1_3_3': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', - '1_3_4': 'regions="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]"', - '1_3_5': 'regions="[[[10,10], [20,10], [20, 30]]]"', - '1_3_6': 'regions="[[10,10], [30,30], [15, 15]]"', - '1_3_7': 'regions="[[10,10], [30,30], [10, 30], [30, 10]]"', - } - test_answers = { - '1_2_1': '[500,20]', - '1_2_2': '[250,300]', - '1_2_3': '[500,20]', - '1_2_4': '[250,250]', - '1_2_5': '[10,10]', +class TrueFalseResponseTest(ResponseTest): + from response_xml_factory import TrueFalseResponseXMLFactory + xml_factory_class = TrueFalseResponseXMLFactory - '1_3_1': '[500,20]', - '1_3_2': '[15,15]', - '1_3_3': '[500,20]', - '1_3_4': '[115,115]', - '1_3_5': '[15,15]', - '1_3_6': '[20,20]', - '1_3_7': '[20,15]', - } + def test_true_false_grade(self): + problem = self.build_problem(choices=[False, True, True]) - # regions - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_3'), 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_4'), 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_5'), 'incorrect') + # Check the results + # Mark correct if and only if ALL (and only) correct choices selected + self.assert_grade(problem, 'choice_0', 'incorrect') + self.assert_grade(problem, 'choice_1', 'incorrect') + self.assert_grade(problem, 'choice_2', 'incorrect') + self.assert_grade(problem, ['choice_0', 'choice_1', 'choice_2'], 'incorrect') + self.assert_grade(problem, ['choice_0', 'choice_2'], 'incorrect') + self.assert_grade(problem, ['choice_0', 'choice_1'], 'incorrect') + self.assert_grade(problem, ['choice_1', 'choice_2'], 'correct') - # regions and rectangles - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_2'), 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_3'), 'incorrect') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_4'), 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_5'), 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_6'), 'incorrect') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_7'), 'correct') + # Invalid choices should be marked incorrect (we have no choice 3) + self.assert_grade(problem, 'choice_3', 'incorrect') + self.assert_grade(problem, 'not_a_choice', 'incorrect') + + def test_named_true_false_grade(self): + problem = self.build_problem(choices=[False, True, True], + choice_names=['foil_1','foil_2','foil_3']) + + # Check the results + # Mark correct if and only if ALL (and only) correct chocies selected + self.assert_grade(problem, 'choice_foil_1', 'incorrect') + self.assert_grade(problem, 'choice_foil_2', 'incorrect') + self.assert_grade(problem, 'choice_foil_3', 'incorrect') + self.assert_grade(problem, ['choice_foil_1', 'choice_foil_2', 'choice_foil_3'], 'incorrect') + self.assert_grade(problem, ['choice_foil_1', 'choice_foil_3'], 'incorrect') + self.assert_grade(problem, ['choice_foil_1', 'choice_foil_2'], 'incorrect') + self.assert_grade(problem, ['choice_foil_2', 'choice_foil_3'], 'correct') + + # Invalid choices should be marked incorrect + self.assert_grade(problem, 'choice_foil_4', 'incorrect') + self.assert_grade(problem, 'not_a_choice', 'incorrect') + +class ImageResponseTest(ResponseTest): + from response_xml_factory import ImageResponseXMLFactory + xml_factory_class = ImageResponseXMLFactory + + def test_rectangle_grade(self): + # Define a rectangle with corners (10,10) and (20,20) + problem = self.build_problem(rectangle="(10,10)-(20,20)") + + # Anything inside the rectangle (and along the borders) is correct + # Everything else is incorrect + correct_inputs = ["[12,19]", "[10,10]", "[20,20]", + "[10,15]", "[20,15]", "[15,10]", "[15,20]"] + incorrect_inputs = ["[4,6]", "[25,15]", "[15,40]", "[15,4]"] + self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs) + + def test_multiple_rectangles_grade(self): + # Define two rectangles + rectangle_str = "(10,10)-(20,20);(100,100)-(200,200)" + + # Expect that only points inside the rectangles are marked correct + problem = self.build_problem(rectangle=rectangle_str) + correct_inputs = ["[12,19]", "[120, 130]"] + incorrect_inputs = ["[4,6]", "[25,15]", "[15,40]", "[15,4]", + "[50,55]", "[300, 14]", "[120, 400]"] + self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs) + + def test_region_grade(self): + # Define a triangular region with corners (0,0), (5,10), and (0, 10) + region_str = "[ [1,1], [5,10], [0,10] ]" + + # Expect that only points inside the triangle are marked correct + problem = self.build_problem(regions=region_str) + correct_inputs = ["[2,4]", "[1,3]"] + incorrect_inputs = ["[0,0]", "[3,5]", "[5,15]", "[30, 12]"] + self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs) + + def test_multiple_regions_grade(self): + # Define multiple regions that the user can select + region_str="[[[10,10], [20,10], [20, 30]], [[100,100], [120,100], [120,150]]]" + + # Expect that only points inside the regions are marked correct + problem = self.build_problem(regions=region_str) + correct_inputs = ["[15,12]", "[110,112]"] + incorrect_inputs = ["[0,0]", "[600,300]"] + self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs) + + def test_region_and_rectangle_grade(self): + rectangle_str = "(100,100)-(200,200)" + region_str="[[10,10], [20,10], [20, 30]]" + + # Expect that only points inside the rectangle or region are marked correct + problem = self.build_problem(regions=region_str, rectangle=rectangle_str) + correct_inputs = ["[13,12]", "[110,112]"] + incorrect_inputs = ["[0,0]", "[600,300]"] + self.assert_multiple_grade(problem, correct_inputs, incorrect_inputs) class SymbolicResponseTest(unittest.TestCase): @@ -195,60 +253,165 @@ class SymbolicResponseTest(unittest.TestCase): self.assertEquals(test_lcp.grade_answers(wrong_answers).get_correctness('1_2_1'), 'incorrect') -class OptionResponseTest(unittest.TestCase): - ''' - Run this with +class OptionResponseTest(ResponseTest): + from response_xml_factory import OptionResponseXMLFactory + xml_factory_class = OptionResponseXMLFactory - python manage.py test courseware.OptionResponseTest - ''' - def test_or_grade(self): - optionresponse_file = os.path.dirname(__file__) + "/test_files/optionresponse.xml" - test_lcp = lcp.LoncapaProblem(open(optionresponse_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': 'True', - '1_2_2': 'False'} - test_answers = {'1_2_1': 'True', - '1_2_2': 'True', - } - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_2'), 'incorrect') + def test_grade(self): + problem = self.build_problem(options=["first", "second", "third"], + correct_option="second") + + # Assert that we get the expected grades + self.assert_grade(problem, "first", "incorrect") + self.assert_grade(problem, "second", "correct") + self.assert_grade(problem, "third", "incorrect") + + # Options not in the list should be marked incorrect + 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(unittest.TestCase): - ''' - Test String response problem with a hint - ''' - def test_or_grade(self): - problem_file = os.path.dirname(__file__) + "/test_files/stringresponse_with_hint.xml" - test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': 'Michigan'} - test_answers = {'1_2_1': 'Minnesota'} - 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('St. Paul' in cmap.get_hint('1_2_1')) + # 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') -class CodeResponseTest(unittest.TestCase): - ''' - Test CodeResponse - TODO: Add tests for external grader messages - ''' + 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 + + + def test_case_sensitive(self): + problem = self.build_problem(answer="Second", case_sensitive=True) + + # Exact string should be correct + self.assert_grade(problem, "Second", "correct") + + # Other strings and the lowercase version of the string are incorrect + self.assert_grade(problem, "Other String", "incorrect") + self.assert_grade(problem, "second", "incorrect") + + def test_case_insensitive(self): + problem = self.build_problem(answer="Second", case_sensitive=False) + + # Both versions of the string should be allowed, regardless + # of capitalization + self.assert_grade(problem, "Second", "correct") + self.assert_grade(problem, "second", "correct") + + # Other strings are not allowed + self.assert_grade(problem, "Other String", "incorrect") + + def test_hints(self): + hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"), + ("minnesota", "minn", "The state capital of Minnesota is St. Paul")] + + problem = self.build_problem(answer="Michigan", + case_sensitive=False, + hints=hints) + + # We should get a hint for Wisconsin + input_dict = {'1_2_1': 'Wisconsin'} + correct_map = problem.grade_answers(input_dict) + self.assertEquals(correct_map.get_hint('1_2_1'), + "The state capital of Wisconsin is Madison") + + # We should get a hint for Minnesota + input_dict = {'1_2_1': 'Minnesota'} + correct_map = problem.grade_answers(input_dict) + self.assertEquals(correct_map.get_hint('1_2_1'), + "The state capital of Minnesota is St. Paul") + + # We should NOT get a hint for Michigan (the correct answer) + input_dict = {'1_2_1': 'Michigan'} + correct_map = problem.grade_answers(input_dict) + self.assertEquals(correct_map.get_hint('1_2_1'), "") + + # We should NOT get a hint for any other string + input_dict = {'1_2_1': 'California'} + correct_map = problem.grade_answers(input_dict) + self.assertEquals(correct_map.get_hint('1_2_1'), "") + +class CodeResponseTest(ResponseTest): + from response_xml_factory import CodeResponseXMLFactory + xml_factory_class = CodeResponseXMLFactory + + def setUp(self): + super(CodeResponseTest, self).setUp() + + grader_payload = json.dumps({"grader": "ps04/grade_square.py"}) + self.problem = self.build_problem(initial_display="def square(x):", + answer_display="answer", + grader_payload=grader_payload, + num_responses=2) + @staticmethod def make_queuestate(key, time): timestr = datetime.strftime(time, dateformat) @@ -258,171 +421,354 @@ class CodeResponseTest(unittest.TestCase): """ Simple test of whether LoncapaProblem knows when it's been queued """ - problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") - with open(problem_file) as input_file: - test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=test_system) - answer_ids = sorted(test_lcp.get_question_answers()) + answer_ids = sorted(self.problem.get_question_answers()) - # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state - cmap = CorrectMap() - for answer_id in answer_ids: - cmap.update(CorrectMap(answer_id=answer_id, queuestate=None)) - test_lcp.correct_map.update(cmap) + # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state + cmap = CorrectMap() + for answer_id in answer_ids: + cmap.update(CorrectMap(answer_id=answer_id, queuestate=None)) + self.problem.correct_map.update(cmap) - self.assertEquals(test_lcp.is_queued(), False) + self.assertEquals(self.problem.is_queued(), False) - # Now we queue the LCP - cmap = CorrectMap() - for i, answer_id in enumerate(answer_ids): - queuestate = CodeResponseTest.make_queuestate(i, datetime.now()) - cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) - test_lcp.correct_map.update(cmap) + # Now we queue the LCP + cmap = CorrectMap() + for i, answer_id in enumerate(answer_ids): + queuestate = CodeResponseTest.make_queuestate(i, datetime.now()) + cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) + self.problem.correct_map.update(cmap) - self.assertEquals(test_lcp.is_queued(), True) + self.assertEquals(self.problem.is_queued(), True) def test_update_score(self): ''' Test whether LoncapaProblem.update_score can deliver queued result to the right subproblem ''' - problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") - with open(problem_file) as input_file: - test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=test_system) + answer_ids = sorted(self.problem.get_question_answers()) - answer_ids = sorted(test_lcp.get_question_answers()) + # CodeResponse requires internal CorrectMap state. Build it now in the queued state + old_cmap = CorrectMap() + for i, answer_id in enumerate(answer_ids): + queuekey = 1000 + i + queuestate = CodeResponseTest.make_queuestate(1000 + i, datetime.now()) + old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) - # CodeResponse requires internal CorrectMap state. Build it now in the queued state - old_cmap = CorrectMap() + # Message format common to external graders + grader_msg = 'MESSAGE' # Must be valid XML + correct_score_msg = json.dumps({'correct': True, 'score': 1, 'msg': grader_msg}) + incorrect_score_msg = json.dumps({'correct': False, 'score': 0, 'msg': grader_msg}) + + xserver_msgs = {'correct': correct_score_msg, + 'incorrect': incorrect_score_msg, } + + # Incorrect queuekey, state should not be updated + for correctness in ['correct', 'incorrect']: + self.problem.correct_map = CorrectMap() + self.problem.correct_map.update(old_cmap) # Deep copy + + self.problem.update_score(xserver_msgs[correctness], queuekey=0) + self.assertEquals(self.problem.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison + + for answer_id in answer_ids: + self.assertTrue(self.problem.correct_map.is_queued(answer_id)) # Should be still queued, since message undelivered + + # Correct queuekey, state should be updated + for correctness in ['correct', 'incorrect']: for i, answer_id in enumerate(answer_ids): - queuekey = 1000 + i - queuestate = CodeResponseTest.make_queuestate(1000 + i, datetime.now()) - old_cmap.update(CorrectMap(answer_id=answer_ids[i], queuestate=queuestate)) + self.problem.correct_map = CorrectMap() + self.problem.correct_map.update(old_cmap) - # Message format common to external graders - grader_msg = 'MESSAGE' # Must be valid XML - correct_score_msg = json.dumps({'correct': True, 'score': 1, 'msg': grader_msg}) - incorrect_score_msg = json.dumps({'correct': False, 'score': 0, 'msg': grader_msg}) + new_cmap = CorrectMap() + new_cmap.update(old_cmap) + npoints = 1 if correctness == 'correct' else 0 + new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg=grader_msg, queuestate=None) - xserver_msgs = {'correct': correct_score_msg, - 'incorrect': incorrect_score_msg, } + self.problem.update_score(xserver_msgs[correctness], queuekey=1000 + i) + self.assertEquals(self.problem.correct_map.get_dict(), new_cmap.get_dict()) - # Incorrect queuekey, state should not be updated - for correctness in ['correct', 'incorrect']: - test_lcp.correct_map = CorrectMap() - test_lcp.correct_map.update(old_cmap) # Deep copy - - test_lcp.update_score(xserver_msgs[correctness], queuekey=0) - self.assertEquals(test_lcp.correct_map.get_dict(), old_cmap.get_dict()) # Deep comparison - - for answer_id in answer_ids: - self.assertTrue(test_lcp.correct_map.is_queued(answer_id)) # Should be still queued, since message undelivered - - # Correct queuekey, state should be updated - for correctness in ['correct', 'incorrect']: - for i, answer_id in enumerate(answer_ids): - test_lcp.correct_map = CorrectMap() - test_lcp.correct_map.update(old_cmap) - - new_cmap = CorrectMap() - new_cmap.update(old_cmap) - npoints = 1 if correctness == 'correct' else 0 - new_cmap.set(answer_id=answer_id, npoints=npoints, correctness=correctness, msg=grader_msg, queuestate=None) - - test_lcp.update_score(xserver_msgs[correctness], queuekey=1000 + i) - self.assertEquals(test_lcp.correct_map.get_dict(), new_cmap.get_dict()) - - for j, test_id in enumerate(answer_ids): - if j == i: - self.assertFalse(test_lcp.correct_map.is_queued(test_id)) # Should be dequeued, message delivered - else: - self.assertTrue(test_lcp.correct_map.is_queued(test_id)) # Should be queued, message undelivered + for j, test_id in enumerate(answer_ids): + if j == i: + self.assertFalse(self.problem.correct_map.is_queued(test_id)) # Should be dequeued, message delivered + else: + self.assertTrue(self.problem.correct_map.is_queued(test_id)) # Should be queued, message undelivered def test_recentmost_queuetime(self): ''' Test whether the LoncapaProblem knows about the time of queue requests ''' - problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") - with open(problem_file) as input_file: - test_lcp = lcp.LoncapaProblem(input_file.read(), '1', system=test_system) + answer_ids = sorted(self.problem.get_question_answers()) - answer_ids = sorted(test_lcp.get_question_answers()) + # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state + cmap = CorrectMap() + for answer_id in answer_ids: + cmap.update(CorrectMap(answer_id=answer_id, queuestate=None)) + self.problem.correct_map.update(cmap) - # CodeResponse requires internal CorrectMap state. Build it now in the unqueued state - cmap = CorrectMap() - for answer_id in answer_ids: - cmap.update(CorrectMap(answer_id=answer_id, queuestate=None)) - test_lcp.correct_map.update(cmap) + self.assertEquals(self.problem.get_recentmost_queuetime(), None) - self.assertEquals(test_lcp.get_recentmost_queuetime(), None) + # CodeResponse requires internal CorrectMap state. Build it now in the queued state + cmap = CorrectMap() + for i, answer_id in enumerate(answer_ids): + queuekey = 1000 + i + latest_timestamp = datetime.now() + queuestate = CodeResponseTest.make_queuestate(1000 + i, latest_timestamp) + cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate)) + self.problem.correct_map.update(cmap) - # CodeResponse requires internal CorrectMap state. Build it now in the queued state - cmap = CorrectMap() - for i, answer_id in enumerate(answer_ids): - queuekey = 1000 + i - latest_timestamp = datetime.now() - queuestate = CodeResponseTest.make_queuestate(1000 + i, latest_timestamp) - cmap.update(CorrectMap(answer_id=answer_id, queuestate=queuestate)) - test_lcp.correct_map.update(cmap) + # Queue state only tracks up to second + latest_timestamp = datetime.strptime(datetime.strftime(latest_timestamp, dateformat), dateformat) - # Queue state only tracks up to second - latest_timestamp = datetime.strptime(datetime.strftime(latest_timestamp, dateformat), dateformat) + self.assertEquals(self.problem.get_recentmost_queuetime(), latest_timestamp) - self.assertEquals(test_lcp.get_recentmost_queuetime(), latest_timestamp) + def test_convert_files_to_filenames(self): + ''' + Test whether file objects are converted to filenames without altering other structures + ''' + problem_file = os.path.join(os.path.dirname(__file__), "test_files/filename_convert_test.txt") + with open(problem_file) as fp: + answers_with_file = {'1_2_1': 'String-based answer', + '1_3_1': ['answer1', 'answer2', 'answer3'], + '1_4_1': [fp, fp]} + answers_converted = convert_files_to_filenames(answers_with_file) + self.assertEquals(answers_converted['1_2_1'], 'String-based answer') + self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3']) + self.assertEquals(answers_converted['1_4_1'], [fp.name, fp.name]) - def test_convert_files_to_filenames(self): - ''' - Test whether file objects are converted to filenames without altering other structures - ''' - problem_file = os.path.join(os.path.dirname(__file__), "test_files/coderesponse.xml") - with open(problem_file) as fp: - answers_with_file = {'1_2_1': 'String-based answer', - '1_3_1': ['answer1', 'answer2', 'answer3'], - '1_4_1': [fp, fp]} - answers_converted = convert_files_to_filenames(answers_with_file) - self.assertEquals(answers_converted['1_2_1'], 'String-based answer') - self.assertEquals(answers_converted['1_3_1'], ['answer1', 'answer2', 'answer3']) - self.assertEquals(answers_converted['1_4_1'], [fp.name, fp.name]) +class ChoiceResponseTest(ResponseTest): + from response_xml_factory import ChoiceResponseXMLFactory + xml_factory_class = ChoiceResponseXMLFactory + + def test_radio_group_grade(self): + problem = self.build_problem(choice_type='radio', + choices=[False, True, False]) + + # Check that we get the expected results + self.assert_grade(problem, 'choice_0', 'incorrect') + self.assert_grade(problem, 'choice_1', 'correct') + self.assert_grade(problem, 'choice_2', 'incorrect') + + # No choice 3 exists --> mark incorrect + self.assert_grade(problem, 'choice_3', 'incorrect') -class ChoiceResponseTest(unittest.TestCase): + def test_checkbox_group_grade(self): + problem = self.build_problem(choice_type='checkbox', + choices=[False, True, True]) - def test_cr_rb_grade(self): - problem_file = os.path.dirname(__file__) + "/test_files/choiceresponse_radio.xml" - test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': 'choice_2', - '1_3_1': ['choice_2', 'choice_3']} - test_answers = {'1_2_1': 'choice_2', - '1_3_1': 'choice_2', - } - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect') + # Check that we get the expected results + # (correct if and only if BOTH correct choices chosen) + self.assert_grade(problem, ['choice_1', 'choice_2'], 'correct') + self.assert_grade(problem, 'choice_1', 'incorrect') + self.assert_grade(problem, 'choice_2', 'incorrect') + self.assert_grade(problem, ['choice_0', 'choice_1'], 'incorrect') + self.assert_grade(problem, ['choice_0', 'choice_2'], 'incorrect') - def test_cr_cb_grade(self): - problem_file = os.path.dirname(__file__) + "/test_files/choiceresponse_checkbox.xml" - test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': 'choice_2', - '1_3_1': ['choice_2', 'choice_3'], - '1_4_1': ['choice_2', 'choice_3']} - test_answers = {'1_2_1': 'choice_2', - '1_3_1': 'choice_2', - '1_4_1': ['choice_2', 'choice_3'], - } - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_2_1'), 'correct') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_3_1'), 'incorrect') - self.assertEquals(test_lcp.grade_answers(test_answers).get_correctness('1_4_1'), 'correct') + # No choice 3 exists --> mark incorrect + self.assert_grade(problem, 'choice_3', 'incorrect') -class JavascriptResponseTest(unittest.TestCase): +class JavascriptResponseTest(ResponseTest): + from response_xml_factory import JavascriptResponseXMLFactory + xml_factory_class = JavascriptResponseXMLFactory - def test_jr_grade(self): - problem_file = os.path.dirname(__file__) + "/test_files/javascriptresponse.xml" + def test_grade(self): + # Compile coffee files into javascript used by the response coffee_file_path = os.path.dirname(__file__) + "/test_files/js/*.coffee" os.system("coffee -c %s" % (coffee_file_path)) - test_lcp = lcp.LoncapaProblem(open(problem_file).read(), '1', system=test_system) - correct_answers = {'1_2_1': json.dumps({0: 4})} - incorrect_answers = {'1_2_1': json.dumps({0: 5})} - self.assertEquals(test_lcp.grade_answers(incorrect_answers).get_correctness('1_2_1'), 'incorrect') - self.assertEquals(test_lcp.grade_answers(correct_answers).get_correctness('1_2_1'), 'correct') + problem = self.build_problem(generator_src="test_problem_generator.js", + grader_src="test_problem_grader.js", + display_class="TestProblemDisplay", + display_src="test_problem_display.js", + param_dict={'value': '4'}) + + # Test that we get graded correctly + self.assert_grade(problem, json.dumps({0:4}), "correct") + self.assert_grade(problem, json.dumps({0:5}), "incorrect") + +class NumericalResponseTest(ResponseTest): + from response_xml_factory import NumericalResponseXMLFactory + xml_factory_class = NumericalResponseXMLFactory + + def test_grade_exact(self): + problem = self.build_problem(question_text="What is 2 + 2?", + explanation="The answer is 4", + answer=4) + correct_responses = ["4", "4.0", "4.00"] + incorrect_responses = ["", "3.9", "4.1", "0"] + self.assert_multiple_grade(problem, correct_responses, incorrect_responses) + + + def test_grade_decimal_tolerance(self): + problem = self.build_problem(question_text="What is 2 + 2 approximately?", + explanation="The answer is 4", + answer=4, + tolerance=0.1) + correct_responses = ["4.0", "4.00", "4.09", "3.91"] + incorrect_responses = ["", "4.11", "3.89", "0"] + self.assert_multiple_grade(problem, correct_responses, incorrect_responses) + + def test_grade_percent_tolerance(self): + problem = self.build_problem(question_text="What is 2 + 2 approximately?", + explanation="The answer is 4", + answer=4, + tolerance="10%") + correct_responses = ["4.0", "4.3", "3.7", "4.30", "3.70"] + incorrect_responses = ["", "4.5", "3.5", "0"] + self.assert_multiple_grade(problem, correct_responses, incorrect_responses) + + def test_grade_with_script(self): + script_text = "computed_response = math.sqrt(4)" + problem = self.build_problem(question_text="What is sqrt(4)?", + explanation="The answer is 2", + answer="$computed_response", + script=script_text) + correct_responses = ["2", "2.0"] + incorrect_responses = ["", "2.01", "1.99", "0"] + self.assert_multiple_grade(problem, correct_responses, incorrect_responses) + + def test_grade_with_script_and_tolerance(self): + script_text = "computed_response = math.sqrt(4)" + problem = self.build_problem(question_text="What is sqrt(4)?", + explanation="The answer is 2", + answer="$computed_response", + tolerance="0.1", + script=script_text) + correct_responses = ["2", "2.0", "2.05", "1.95"] + incorrect_responses = ["", "2.11", "1.89", "0"] + self.assert_multiple_grade(problem, correct_responses, incorrect_responses) + + +class CustomResponseTest(ResponseTest): + from response_xml_factory import CustomResponseXMLFactory + xml_factory_class = 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'""" + problem = self.build_problem(answer=inline_script, expect="42") + + # Check results + self.assert_grade(problem, '42', 'correct') + self.assert_grade(problem, '0', '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" """ + problem = self.build_problem(answer=inline_script) + + 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 + # + # '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'}""" + + problem = self.build_problem(script=script, cfn="check_func", expect="42") + + # 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'}""" + + problem = self.build_problem(script=script, + cfn="check_func", num_inputs=3) + + # 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') + + +class SchematicResponseTest(ResponseTest): + from response_xml_factory import SchematicResponseXMLFactory + xml_factory_class = SchematicResponseXMLFactory + + def test_grade(self): + + # Most of the schematic-specific work is handled elsewhere + # (in client-side JavaScript) + # The is responsible only for executing the + # Python code in with *submission* (list) + # in the global context. + + # To test that the context is set up correctly, + # we create a script that sets *correct* to true + # if and only if we find the *submission* (list) + script="correct = ['correct' if 'test' in submission[0] else 'incorrect']" + problem = self.build_problem(answer=script) + + # The actual dictionary would contain schematic information + # sent from the JavaScript simulation + submission_dict = {'test': 'test'} + input_dict = { '1_2_1': json.dumps(submission_dict) } + correct_map = problem.grade_answers(input_dict) + + # Expect that the problem is graded as true + # (That is, our script verifies that the context + # is what we expect) + self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')