).
+
+ 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 8a0c953d33..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,186 +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):
-
- def test_jr_grade(self):
- problem_file = os.path.dirname(__file__) + "/test_files/javascriptresponse.xml"
- 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')
-
-class AnnotationResponseTest(unittest.TestCase):
+class JavascriptResponseTest(ResponseTest):
+ from response_xml_factory import JavascriptResponseXMLFactory
+ xml_factory_class = JavascriptResponseXMLFactory
def test_grade(self):
- annotationresponse_file = os.path.dirname(__file__) + "/test_files/annotationresponse.xml"
- test_lcp = lcp.LoncapaProblem(open(annotationresponse_file).read(), '1', system=test_system)
- answers_for = {
- 'correct': {'1_2_1': json.dumps({'options':[0]})},
- 'incorrect': {'1_2_1': json.dumps({'options':[1]})},
- 'partially-correct': {'1_2_1': json.dumps({'options':[2]})}
- }
+ # 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))
- for expected_correctness in answers_for.keys():
- actual_correctness = test_lcp.grade_answers(answers_for[expected_correctness]).get_correctness('1_2_1')
- self.assertEquals(expected_correctness, actual_correctness)
\ No newline at end of file
+ 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')
diff --git a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
index 9b8062d60d..5161e658e7 100644
--- a/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
+++ b/common/lib/xmodule/xmodule/js/spec/problem/edit_spec.coffee
@@ -119,13 +119,13 @@ describe 'MarkdownEditingDescriptor', ->
The answer is correct if it is within a specified numerical tolerance of the expected answer.
Enter the numerical value of Pi:
-
+
Enter the approximate value of 502*9:
-
+
@@ -147,6 +147,20 @@ describe 'MarkdownEditingDescriptor', ->
+ """)
+ it 'will convert 0 as a numerical response (instead of string response)', ->
+ data = MarkdownEditingDescriptor.markdownToXml("""
+ Enter 0 with a tolerance:
+ = 0 +- .02
+ """)
+ expect(data).toEqual("""
+ Enter 0 with a tolerance:
+
+
+
+
+
+
""")
it 'converts multiple choice to xml', ->
data = MarkdownEditingDescriptor.markdownToXml("""A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.
diff --git a/common/lib/xmodule/xmodule/js/src/problem/edit.coffee b/common/lib/xmodule/xmodule/js/src/problem/edit.coffee
index 2bfe483a7f..b723f230e9 100644
--- a/common/lib/xmodule/xmodule/js/src/problem/edit.coffee
+++ b/common/lib/xmodule/xmodule/js/src/problem/edit.coffee
@@ -231,13 +231,14 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
// replace string and numerical
xml = xml.replace(/^\=\s*(.*?$)/gm, function(match, p) {
var string;
- var params = /(.*?)\+\-\s*(.*?$)/.exec(p);
- if(parseFloat(p)) {
+ var floatValue = parseFloat(p);
+ if(!isNaN(floatValue)) {
+ var params = /(.*?)\+\-\s*(.*?$)/.exec(p);
if(params) {
- string = '\n';
+ string = '\n';
string += ' \n';
} else {
- string = '\n';
+ string = '\n';
}
string += ' \n';
string += '\n\n';
diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py
index 012efb0c27..8068129559 100644
--- a/common/lib/xmodule/xmodule/modulestore/mongo.py
+++ b/common/lib/xmodule/xmodule/modulestore/mongo.py
@@ -157,10 +157,15 @@ class MongoModuleStore(ModuleStoreBase):
'''
# get all collections in the course, this query should not return any leaf nodes
- query = { '_id.org' : location.org,
- '_id.course' : location.course,
- '_id.revision' : None,
- 'definition.children':{'$ne': []}
+ query = {
+ '_id.org': location.org,
+ '_id.course': location.course,
+ '$or': [
+ {"_id.category":"course"},
+ {"_id.category":"chapter"},
+ {"_id.category":"sequential"},
+ {"_id.category":"vertical"}
+ ]
}
# we just want the Location, children, and metadata
record_filter = {'_id':1,'definition.children':1,'metadata':1}
@@ -279,6 +284,13 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs = OSFS(root)
+ metadata_inheritance_tree = None
+
+ # if we are loading a course object, there is no parent to inherit the metadata from
+ # so don't bother getting it
+ if item['location']['category'] != 'course':
+ metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']), 300)
+
# TODO (cdodge): When the 'split module store' work has been completed, we should remove
# the 'metadata_inheritance_tree' parameter
system = CachingDescriptorSystem(
@@ -288,7 +300,7 @@ class MongoModuleStore(ModuleStoreBase):
resource_fs,
self.error_tracker,
self.render_template,
- metadata_inheritance_tree = self.get_cached_metadata_inheritance_tree(Location(item['location']), 60)
+ metadata_inheritance_tree = metadata_inheritance_tree
)
return system.load_item(item['location'])
diff --git a/common/static/js/vendor/CodeMirror/codemirror.js b/common/static/js/vendor/CodeMirror/codemirror.js
index ff127f80f6..d345d64a26 100644
--- a/common/static/js/vendor/CodeMirror/codemirror.js
+++ b/common/static/js/vendor/CodeMirror/codemirror.js
@@ -1,3 +1,5 @@
+// CodeMirror version 2.23 (with edits)
+//
// All functions that need access to the editor's state live inside
// the CodeMirror function. Below that, at the bottom of the file,
// some utilities are defined.
diff --git a/common/static/js/vendor/CodeMirror/javascript.js b/common/static/js/vendor/CodeMirror/javascript.js
new file mode 100644
index 0000000000..462f486346
--- /dev/null
+++ b/common/static/js/vendor/CodeMirror/javascript.js
@@ -0,0 +1,360 @@
+CodeMirror.defineMode("javascript", function(config, parserConfig) {
+ var indentUnit = config.indentUnit;
+ var jsonMode = parserConfig.json;
+
+ // Tokenizer
+
+ var keywords = function(){
+ function kw(type) {return {type: type, style: "keyword"};}
+ var A = kw("keyword a"), B = kw("keyword b"), C = kw("keyword c");
+ var operator = kw("operator"), atom = {type: "atom", style: "atom"};
+ return {
+ "if": A, "while": A, "with": A, "else": B, "do": B, "try": B, "finally": B,
+ "return": C, "break": C, "continue": C, "new": C, "delete": C, "throw": C,
+ "var": kw("var"), "const": kw("var"), "let": kw("var"),
+ "function": kw("function"), "catch": kw("catch"),
+ "for": kw("for"), "switch": kw("switch"), "case": kw("case"), "default": kw("default"),
+ "in": operator, "typeof": operator, "instanceof": operator,
+ "true": atom, "false": atom, "null": atom, "undefined": atom, "NaN": atom, "Infinity": atom
+ };
+ }();
+
+ var isOperatorChar = /[+\-*&%=<>!?|]/;
+
+ function chain(stream, state, f) {
+ state.tokenize = f;
+ return f(stream, state);
+ }
+
+ function nextUntilUnescaped(stream, end) {
+ var escaped = false, next;
+ while ((next = stream.next()) != null) {
+ if (next == end && !escaped)
+ return false;
+ escaped = !escaped && next == "\\";
+ }
+ return escaped;
+ }
+
+ // Used as scratch variables to communicate multiple values without
+ // consing up tons of objects.
+ var type, content;
+ function ret(tp, style, cont) {
+ type = tp; content = cont;
+ return style;
+ }
+
+ function jsTokenBase(stream, state) {
+ var ch = stream.next();
+ if (ch == '"' || ch == "'")
+ return chain(stream, state, jsTokenString(ch));
+ else if (/[\[\]{}\(\),;\:\.]/.test(ch))
+ return ret(ch);
+ else if (ch == "0" && stream.eat(/x/i)) {
+ stream.eatWhile(/[\da-f]/i);
+ return ret("number", "number");
+ }
+ else if (/\d/.test(ch)) {
+ stream.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/);
+ return ret("number", "number");
+ }
+ else if (ch == "/") {
+ if (stream.eat("*")) {
+ return chain(stream, state, jsTokenComment);
+ }
+ else if (stream.eat("/")) {
+ stream.skipToEnd();
+ return ret("comment", "comment");
+ }
+ else if (state.reAllowed) {
+ nextUntilUnescaped(stream, "/");
+ stream.eatWhile(/[gimy]/); // 'y' is "sticky" option in Mozilla
+ return ret("regexp", "string-2");
+ }
+ else {
+ stream.eatWhile(isOperatorChar);
+ return ret("operator", null, stream.current());
+ }
+ }
+ else if (ch == "#") {
+ stream.skipToEnd();
+ return ret("error", "error");
+ }
+ else if (isOperatorChar.test(ch)) {
+ stream.eatWhile(isOperatorChar);
+ return ret("operator", null, stream.current());
+ }
+ else {
+ stream.eatWhile(/[\w\$_]/);
+ var word = stream.current(), known = keywords.propertyIsEnumerable(word) && keywords[word];
+ return (known && state.kwAllowed) ? ret(known.type, known.style, word) :
+ ret("variable", "variable", word);
+ }
+ }
+
+ function jsTokenString(quote) {
+ return function(stream, state) {
+ if (!nextUntilUnescaped(stream, quote))
+ state.tokenize = jsTokenBase;
+ return ret("string", "string");
+ };
+ }
+
+ function jsTokenComment(stream, state) {
+ var maybeEnd = false, ch;
+ while (ch = stream.next()) {
+ if (ch == "/" && maybeEnd) {
+ state.tokenize = jsTokenBase;
+ break;
+ }
+ maybeEnd = (ch == "*");
+ }
+ return ret("comment", "comment");
+ }
+
+ // Parser
+
+ var atomicTypes = {"atom": true, "number": true, "variable": true, "string": true, "regexp": true};
+
+ function JSLexical(indented, column, type, align, prev, info) {
+ this.indented = indented;
+ this.column = column;
+ this.type = type;
+ this.prev = prev;
+ this.info = info;
+ if (align != null) this.align = align;
+ }
+
+ function inScope(state, varname) {
+ for (var v = state.localVars; v; v = v.next)
+ if (v.name == varname) return true;
+ }
+
+ function parseJS(state, style, type, content, stream) {
+ var cc = state.cc;
+ // Communicate our context to the combinators.
+ // (Less wasteful than consing up a hundred closures on every call.)
+ cx.state = state; cx.stream = stream; cx.marked = null, cx.cc = cc;
+
+ if (!state.lexical.hasOwnProperty("align"))
+ state.lexical.align = true;
+
+ while(true) {
+ var combinator = cc.length ? cc.pop() : jsonMode ? expression : statement;
+ if (combinator(type, content)) {
+ while(cc.length && cc[cc.length - 1].lex)
+ cc.pop()();
+ if (cx.marked) return cx.marked;
+ if (type == "variable" && inScope(state, content)) return "variable-2";
+ return style;
+ }
+ }
+ }
+
+ // Combinator utils
+
+ var cx = {state: null, column: null, marked: null, cc: null};
+ function pass() {
+ for (var i = arguments.length - 1; i >= 0; i--) cx.cc.push(arguments[i]);
+ }
+ function cont() {
+ pass.apply(null, arguments);
+ return true;
+ }
+ function register(varname) {
+ var state = cx.state;
+ if (state.context) {
+ cx.marked = "def";
+ for (var v = state.localVars; v; v = v.next)
+ if (v.name == varname) return;
+ state.localVars = {name: varname, next: state.localVars};
+ }
+ }
+
+ // Combinators
+
+ var defaultVars = {name: "this", next: {name: "arguments"}};
+ function pushcontext() {
+ if (!cx.state.context) cx.state.localVars = defaultVars;
+ cx.state.context = {prev: cx.state.context, vars: cx.state.localVars};
+ }
+ function popcontext() {
+ cx.state.localVars = cx.state.context.vars;
+ cx.state.context = cx.state.context.prev;
+ }
+ function pushlex(type, info) {
+ var result = function() {
+ var state = cx.state;
+ state.lexical = new JSLexical(state.indented, cx.stream.column(), type, null, state.lexical, info)
+ };
+ result.lex = true;
+ return result;
+ }
+ function poplex() {
+ var state = cx.state;
+ if (state.lexical.prev) {
+ if (state.lexical.type == ")")
+ state.indented = state.lexical.indented;
+ state.lexical = state.lexical.prev;
+ }
+ }
+ poplex.lex = true;
+
+ function expect(wanted) {
+ return function expecting(type) {
+ if (type == wanted) return cont();
+ else if (wanted == ";") return pass();
+ else return cont(arguments.callee);
+ };
+ }
+
+ function statement(type) {
+ if (type == "var") return cont(pushlex("vardef"), vardef1, expect(";"), poplex);
+ if (type == "keyword a") return cont(pushlex("form"), expression, statement, poplex);
+ if (type == "keyword b") return cont(pushlex("form"), statement, poplex);
+ if (type == "{") return cont(pushlex("}"), block, poplex);
+ if (type == ";") return cont();
+ if (type == "function") return cont(functiondef);
+ if (type == "for") return cont(pushlex("form"), expect("("), pushlex(")"), forspec1, expect(")"),
+ poplex, statement, poplex);
+ if (type == "variable") return cont(pushlex("stat"), maybelabel);
+ if (type == "switch") return cont(pushlex("form"), expression, pushlex("}", "switch"), expect("{"),
+ block, poplex, poplex);
+ if (type == "case") return cont(expression, expect(":"));
+ if (type == "default") return cont(expect(":"));
+ if (type == "catch") return cont(pushlex("form"), pushcontext, expect("("), funarg, expect(")"),
+ statement, poplex, popcontext);
+ return pass(pushlex("stat"), expression, expect(";"), poplex);
+ }
+ function expression(type) {
+ if (atomicTypes.hasOwnProperty(type)) return cont(maybeoperator);
+ if (type == "function") return cont(functiondef);
+ if (type == "keyword c") return cont(maybeexpression);
+ if (type == "(") return cont(pushlex(")"), maybeexpression, expect(")"), poplex, maybeoperator);
+ if (type == "operator") return cont(expression);
+ if (type == "[") return cont(pushlex("]"), commasep(expression, "]"), poplex, maybeoperator);
+ if (type == "{") return cont(pushlex("}"), commasep(objprop, "}"), poplex, maybeoperator);
+ return cont();
+ }
+ function maybeexpression(type) {
+ if (type.match(/[;\}\)\],]/)) return pass();
+ return pass(expression);
+ }
+
+ function maybeoperator(type, value) {
+ if (type == "operator" && /\+\+|--/.test(value)) return cont(maybeoperator);
+ if (type == "operator") return cont(expression);
+ if (type == ";") return;
+ if (type == "(") return cont(pushlex(")"), commasep(expression, ")"), poplex, maybeoperator);
+ if (type == ".") return cont(property, maybeoperator);
+ if (type == "[") return cont(pushlex("]"), expression, expect("]"), poplex, maybeoperator);
+ }
+ function maybelabel(type) {
+ if (type == ":") return cont(poplex, statement);
+ return pass(maybeoperator, expect(";"), poplex);
+ }
+ function property(type) {
+ if (type == "variable") {cx.marked = "property"; return cont();}
+ }
+ function objprop(type) {
+ if (type == "variable") cx.marked = "property";
+ if (atomicTypes.hasOwnProperty(type)) return cont(expect(":"), expression);
+ }
+ function commasep(what, end) {
+ function proceed(type) {
+ if (type == ",") return cont(what, proceed);
+ if (type == end) return cont();
+ return cont(expect(end));
+ }
+ return function commaSeparated(type) {
+ if (type == end) return cont();
+ else return pass(what, proceed);
+ };
+ }
+ function block(type) {
+ if (type == "}") return cont();
+ return pass(statement, block);
+ }
+ function vardef1(type, value) {
+ if (type == "variable"){register(value); return cont(vardef2);}
+ return cont();
+ }
+ function vardef2(type, value) {
+ if (value == "=") return cont(expression, vardef2);
+ if (type == ",") return cont(vardef1);
+ }
+ function forspec1(type) {
+ if (type == "var") return cont(vardef1, forspec2);
+ if (type == ";") return pass(forspec2);
+ if (type == "variable") return cont(formaybein);
+ return pass(forspec2);
+ }
+ function formaybein(type, value) {
+ if (value == "in") return cont(expression);
+ return cont(maybeoperator, forspec2);
+ }
+ function forspec2(type, value) {
+ if (type == ";") return cont(forspec3);
+ if (value == "in") return cont(expression);
+ return cont(expression, expect(";"), forspec3);
+ }
+ function forspec3(type) {
+ if (type != ")") cont(expression);
+ }
+ function functiondef(type, value) {
+ if (type == "variable") {register(value); return cont(functiondef);}
+ if (type == "(") return cont(pushlex(")"), pushcontext, commasep(funarg, ")"), poplex, statement, popcontext);
+ }
+ function funarg(type, value) {
+ if (type == "variable") {register(value); return cont();}
+ }
+
+ // Interface
+
+ return {
+ startState: function(basecolumn) {
+ return {
+ tokenize: jsTokenBase,
+ reAllowed: true,
+ kwAllowed: true,
+ cc: [],
+ lexical: new JSLexical((basecolumn || 0) - indentUnit, 0, "block", false),
+ localVars: parserConfig.localVars,
+ context: parserConfig.localVars && {vars: parserConfig.localVars},
+ indented: 0
+ };
+ },
+
+ token: function(stream, state) {
+ if (stream.sol()) {
+ if (!state.lexical.hasOwnProperty("align"))
+ state.lexical.align = false;
+ state.indented = stream.indentation();
+ }
+ if (stream.eatSpace()) return null;
+ var style = state.tokenize(stream, state);
+ if (type == "comment") return style;
+ state.reAllowed = !!(type == "operator" || type == "keyword c" || type.match(/^[\[{}\(,;:]$/));
+ state.kwAllowed = type != '.';
+ return parseJS(state, style, type, content, stream);
+ },
+
+ indent: function(state, textAfter) {
+ if (state.tokenize != jsTokenBase) return 0;
+ var firstChar = textAfter && textAfter.charAt(0), lexical = state.lexical,
+ type = lexical.type, closing = firstChar == type;
+ if (type == "vardef") return lexical.indented + 4;
+ else if (type == "form" && firstChar == "{") return lexical.indented;
+ else if (type == "stat" || type == "form") return lexical.indented + indentUnit;
+ else if (lexical.info == "switch" && !closing)
+ return lexical.indented + (/^(?:case|default)\b/.test(textAfter) ? indentUnit : 2 * indentUnit);
+ else if (lexical.align) return lexical.column + (closing ? 0 : 1);
+ else return lexical.indented + (closing ? 0 : indentUnit);
+ },
+
+ electricChars: ":{}"
+ };
+});
+
+CodeMirror.defineMIME("text/javascript", "javascript");
+CodeMirror.defineMIME("application/json", {name: "javascript", json: true});
diff --git a/lms/djangoapps/courseware/access.py b/lms/djangoapps/courseware/access.py
index b41d231011..36ad388b43 100644
--- a/lms/djangoapps/courseware/access.py
+++ b/lms/djangoapps/courseware/access.py
@@ -342,6 +342,27 @@ def _does_course_group_name_exist(name):
return len(Group.objects.filter(name=name)) > 0
+def _course_org_staff_group_name(location, course_context=None):
+ """
+ Get the name of the staff group for an organization which corresponds
+ to the organization in the course id.
+
+ location: something that can passed to Location
+ course_context: A course_id that specifies the course run in which
+ the location occurs.
+ Required if location doesn't have category 'course'
+
+ """
+ loc = Location(location)
+ if loc.category == 'course':
+ course_id = loc.course_id
+ else:
+ if course_context is None:
+ raise CourseContextRequired()
+ course_id = course_context
+ return 'staff_%s' % course_id.split('/')[0]
+
+
def _course_staff_group_name(location, course_context=None):
"""
Get the name of the staff group for a location in the context of a course run.
@@ -382,6 +403,27 @@ def course_beta_test_group_name(location):
course_beta_test_group_name.__test__ = False
+def _course_org_instructor_group_name(location, course_context=None):
+ """
+ Get the name of the instructor group for an organization which corresponds
+ to the organization in the course id.
+
+ location: something that can passed to Location
+ course_context: A course_id that specifies the course run in which
+ the location occurs.
+ Required if location doesn't have category 'course'
+
+ """
+ loc = Location(location)
+ if loc.category == 'course':
+ course_id = loc.course_id
+ else:
+ if course_context is None:
+ raise CourseContextRequired()
+ course_id = course_context
+ return 'instructor_%s' % course_id.split('/')[0]
+
+
def _course_instructor_group_name(location, course_context=None):
"""
Get the name of the instructor group for a location, in the context of a course run.
@@ -499,14 +541,18 @@ def _has_access_to_location(user, location, access_level, course_context):
if access_level == 'staff':
staff_group = _course_staff_group_name(location, course_context)
- if staff_group in user_groups:
+ # org_staff_group is a group for an entire organization
+ org_staff_group = _course_org_staff_group_name(location, course_context)
+ if staff_group in user_groups or org_staff_group in user_groups:
debug("Allow: user in group %s", staff_group)
return True
debug("Deny: user not in group %s", staff_group)
if access_level == 'instructor' or access_level == 'staff': # instructors get staff privileges
instructor_group = _course_instructor_group_name(location, course_context)
- if instructor_group in user_groups:
+ instructor_staff_group = _course_org_instructor_group_name(
+ location, course_context)
+ if instructor_group in user_groups or instructor_staff_group in user_groups:
debug("Allow: user in group %s", instructor_group)
return True
debug("Deny: user not in group %s", instructor_group)
diff --git a/lms/lib/symmath/symmath_check.py b/lms/lib/symmath/symmath_check.py
index dc6fe60398..0a4ff84f70 100644
--- a/lms/lib/symmath/symmath_check.py
+++ b/lms/lib/symmath/symmath_check.py
@@ -325,53 +325,3 @@ def symmath_check(expect, ans, dynamath=None, options=None, debug=None, xml=None
msg += '
'
return {'ok': False, 'msg': msg, 'ex': fexpect, 'got': fsym}
-
-#-----------------------------------------------------------------------------
-# tests
-
-
-def sctest1():
- x = "1/2*(1+(k_e* Q* q)/(m *g *h^2))"
- y = '''
-
-'''.strip()
- z = "1/2(1+(k_e* Q* q)/(m *g *h^2))"
- r = sympy_check2(x, z, {'a': z, 'a_fromjs': y}, 'a')
- return r
diff --git a/lms/lib/symmath/test_symmath_check.py b/lms/lib/symmath/test_symmath_check.py
index 71b8f45aad..2d015fcb53 100644
--- a/lms/lib/symmath/test_symmath_check.py
+++ b/lms/lib/symmath/test_symmath_check.py
@@ -10,6 +10,64 @@ class SymmathCheckTest(TestCase):
number_list = [i + 0.01 for i in range(-100, 100)]
self._symmath_check_numbers(number_list)
+ def test_symmath_check_same_symbols(self):
+ expected_str = "x+2*y"
+ dynamath = '''
+'''.strip()
+
+ # Expect that the exact same symbolic string is marked correct
+ result = symmath_check(expected_str, expected_str, dynamath=[dynamath])
+ self.assertTrue('ok' in result and result['ok'])
+
+ def test_symmath_check_equivalent_symbols(self):
+ expected_str = "x+2*y"
+ input_str = "x+y+y"
+ dynamath = '''
+'''.strip()
+
+ # Expect that equivalent symbolic strings are marked correct
+ result = symmath_check(expected_str, input_str, dynamath=[dynamath])
+ self.assertTrue('ok' in result and result['ok'])
+
+ def test_symmath_check_different_symbols(self):
+ expected_str = "0"
+ input_str = "x+y"
+ dynamath = '''
+'''.strip()
+
+ # Expect that an incorrect response is marked incorrect
+ result = symmath_check(expected_str, input_str, dynamath=[dynamath])
+ self.assertTrue('ok' in result and not result['ok'])
+ self.assertFalse('fail' in result['msg'])
+
def _symmath_check_numbers(self, number_list):
for n in number_list:
diff --git a/lms/templates/static_pdfbook.html b/lms/templates/static_pdfbook.html
index 013413b62d..565a59977a 100644
--- a/lms/templates/static_pdfbook.html
+++ b/lms/templates/static_pdfbook.html
@@ -90,41 +90,40 @@
-%if 'chapters' in textbook:
-
+ %endif