From 8b5473b6f62587bcf51838c7eb15449abd2551c4 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Mon, 25 Feb 2013 15:30:55 -0500 Subject: [PATCH 01/17] Wrote unit tests for NumericalResponse capa response type --- common/lib/capa/capa/responsetypes.py | 29 ++++---- .../lib/capa/capa/tests/test_responsetypes.py | 67 +++++++++++++++++++ 2 files changed, 84 insertions(+), 12 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index a1a4e6b65e..8811e7d863 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1048,19 +1048,24 @@ def sympy_check2(): correct = ['correct'] * len(idset) if ret['ok'] else ['incorrect'] * len(idset) msg = ret['msg'] - if 1: - # try to clean up message html - msg = '' + msg + '' - msg = msg.replace('<', '<') - #msg = msg.replace('<','<') - msg = etree.tostring(fromstring_bs(msg, convertEntities=None), - pretty_print=True) - #msg = etree.tostring(fromstring_bs(msg),pretty_print=True) - msg = msg.replace(' ', '') - #msg = re.sub('(.*)','\\1',msg,flags=re.M|re.DOTALL) # python 2.7 - msg = re.sub('(?ms)(.*)', '\\1', msg) + def _cleanup_msg_html(msg_html): + cleaned = msg_html - messages[0] = msg + # try to clean up message html + cleaned = '' + cleaned + '' + cleaned = cleaned.replace('<', '<') + cleaned = etree.tostring(fromstring_bs(cleaned, convertEntities=None), + pretty_print=True) + cleaned = cleaned.replace(' ', '') + cleaned = re.sub('(?ms)(.*)', '\\1', cleaned) + + return cleaned + + if type(msg) == str: + messages[0] = _cleanup_msg_html(msg) + elif type(msg) == list: + for i in range(0, len(msg)): + messages[i] = _cleanup_msg_html(msg[i]) else: correct = ['correct'] * len(idset) if ret else ['incorrect'] * len(idset) diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 18da338b91..97d9ce33da 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -426,3 +426,70 @@ class JavascriptResponseTest(unittest.TestCase): 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') + +from response_xml_factory import NumericalResponseXMLFactory +class NumericalResponseTest(unittest.TestCase): + + def setUp(self): + self.xml_factory = NumericalResponseXMLFactory() + + def test_grade_exact(self): + xml = self.xml_factory.build_xml(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._test_grading(xml, correct_responses, incorrect_responses) + + + def test_grade_decimal_tolerance(self): + xml = self.xml_factory.build_xml(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._test_grading(xml, correct_responses, incorrect_responses) + + def test_grade_percent_tolerance(self): + xml = self.xml_factory.build_xml(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._test_grading(xml, correct_responses, incorrect_responses) + + def test_grade_with_script(self): + script_text = "computed_response = math.sqrt(4)" + xml = self.xml_factory.build_xml(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._test_grading(xml, correct_responses, incorrect_responses) + + def test_grade_with_script_and_tolerance(self): + script_text = "computed_response = math.sqrt(4)" + xml = self.xml_factory.build_xml(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._test_grading(xml, correct_responses, incorrect_responses) + + + def _test_grading(self, xml, correct_answers, incorrect_answers): + + problem = lcp.LoncapaProblem(xml, '1', system=test_system) + + for input_str in correct_answers: + result = problem.grade_answers({'1_2_1': input_str}).get_correctness('1_2_1') + self.assertEqual(result, 'correct') + + 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 f2bb9a2dbcca427f564d822628f06ebc9ca8b98d Mon Sep 17 00:00:00 2001 From: Will Daly Date: Tue, 26 Feb 2013 10:43:30 -0500 Subject: [PATCH 02/17] Implemented unit tests for --- .../capa/capa/tests/response_xml_factory.py | 184 ++++++++++++++++++ .../lib/capa/capa/tests/test_responsetypes.py | 116 +++++++++++ 2 files changed, 300 insertions(+) create mode 100644 common/lib/capa/capa/tests/response_xml_factory.py 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..fac6cf2215 --- /dev/null +++ b/common/lib/capa/capa/tests/response_xml_factory.py @@ -0,0 +1,184 @@ +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_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 + 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 + 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 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 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. + + Use **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/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. - - - - From 7e48de1ef3ac604310ad86be26d046f87ebb5a06 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Wed, 27 Feb 2013 17:22:26 -0500 Subject: [PATCH 16/17] Reset responsetypes.py to version on master --- common/lib/capa/capa/responsetypes.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/common/lib/capa/capa/responsetypes.py b/common/lib/capa/capa/responsetypes.py index 8811e7d863..a1a4e6b65e 100644 --- a/common/lib/capa/capa/responsetypes.py +++ b/common/lib/capa/capa/responsetypes.py @@ -1048,24 +1048,19 @@ def sympy_check2(): correct = ['correct'] * len(idset) if ret['ok'] else ['incorrect'] * len(idset) msg = ret['msg'] - def _cleanup_msg_html(msg_html): - cleaned = msg_html - + if 1: # try to clean up message html - cleaned = '' + cleaned + '' - cleaned = cleaned.replace('<', '<') - cleaned = etree.tostring(fromstring_bs(cleaned, convertEntities=None), + msg = '' + msg + '' + msg = msg.replace('<', '<') + #msg = msg.replace('<','<') + msg = etree.tostring(fromstring_bs(msg, convertEntities=None), pretty_print=True) - cleaned = cleaned.replace(' ', '') - cleaned = re.sub('(?ms)(.*)', '\\1', cleaned) + #msg = etree.tostring(fromstring_bs(msg),pretty_print=True) + msg = msg.replace(' ', '') + #msg = re.sub('(.*)','\\1',msg,flags=re.M|re.DOTALL) # python 2.7 + msg = re.sub('(?ms)(.*)', '\\1', msg) - return cleaned - - if type(msg) == str: - messages[0] = _cleanup_msg_html(msg) - elif type(msg) == list: - for i in range(0, len(msg)): - messages[i] = _cleanup_msg_html(msg[i]) + messages[0] = msg else: correct = ['correct'] * len(idset) if ret else ['incorrect'] * len(idset) From 71d0a367aaea27f9dda99e64f9eecea638f55777 Mon Sep 17 00:00:00 2001 From: Will Daly Date: Thu, 28 Feb 2013 09:45:27 -0500 Subject: [PATCH 17/17] Fixed an error in the JavascriptResponseTest: coffee files were not being compiled before the LoncapaProblem initializer was called. --- .../c9a9cd4242d84c924fe5f8324e9ae79d.js | 50 ------------------- .../js/compiled/javascriptresponse.js | 50 ------------------- .../lib/capa/capa/tests/test_responsetypes.py | 8 +-- 3 files changed, 4 insertions(+), 104 deletions(-) delete mode 100644 common/lib/capa/capa/tests/test_files/js/compiled/c9a9cd4242d84c924fe5f8324e9ae79d.js delete mode 100644 common/lib/capa/capa/tests/test_files/js/compiled/javascriptresponse.js 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_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 9a19bebf95..33b84d213d 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -575,16 +575,16 @@ class JavascriptResponseTest(ResponseTest): xml_factory_class = JavascriptResponseXMLFactory 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)) + 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'}) - # 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 that we get graded correctly self.assert_grade(problem, json.dumps({0:4}), "correct") self.assert_grade(problem, json.dumps({0:5}), "incorrect")