From 54a33a0a3b676b66411964d9f123777e21248d54 Mon Sep 17 00:00:00 2001 From: marco Date: Mon, 13 May 2013 11:04:51 -0400 Subject: [PATCH 01/49] updated pinning icons --- lms/static/images/pinned.png | Bin 518 -> 50653 bytes lms/static/images/unpinned.png | Bin 498 -> 49255 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/lms/static/images/pinned.png b/lms/static/images/pinned.png index 76bb207ffffbe470ecfa81ff8b9b3ee9d8de9100..e70df7f9db45dd890dcbd374baf67f9de585e889 100644 GIT binary patch literal 50653 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 delta 400 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 delta 380 zcmV-?0fYYMfCKUakUR?O000W>0fLJST9Ixekw_?i0ZU0lK~yM_bx^x%0zni#cO7Sj zT}KqMc7iNO1_DXgCZ+8{J|JZR{=$4gTK#~O$tPG?i=_m_q+AOF>0$)IHH8m0MY79e zA*Z>o!@2h!f(2vDC4d@$b-&-A0YI9j003qY9zD;yV2t5#IKUY50^kn73;@U&Q>#=e zukCh!yXkpeuD;!FF`v)hlO#Fs^?GYZ2+`GA=Oo71r`GyjDfOkbZVDl~0N?~c(2`Q- zN=cHO3xg3jmyQxm>azl2W2xuiunPKkaPp_`d&)0ve4*o@a9>ilU)X zYFs1(fKqB4MbQud=+~>O>2!L#TrNAi-LA!BIe+tge;kJ46M#wXBN0u~H2oNjMw{dD zc+EKU!ajr9h3mS!TCD;AFW&1E8~@=CkukQh aqm6IkKV#L?XS9O=0000 Date: Thu, 16 May 2013 15:29:01 -0400 Subject: [PATCH 02/49] Added tests for capa input templates. This includes tests for HTML we expect to be unescaped. --- .../capa/capa/tests/test_input_templates.py | 361 ++++++++++++++++-- 1 file changed, 335 insertions(+), 26 deletions(-) diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py index 92c4d8b3b7..00a9b3f6c2 100644 --- a/common/lib/capa/capa/tests/test_input_templates.py +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -1,20 +1,27 @@ -"""Tests for the logic in input type mako templates.""" +""" +Tests for the logic in input type mako templates. +""" import unittest import capa import os.path +import json from lxml import etree from mako.template import Template as MakoTemplate from mako import exceptions class TemplateError(Exception): - """Error occurred while rendering a Mako template""" + """ + Error occurred while rendering a Mako template. + """ pass class TemplateTestCase(unittest.TestCase): - """Utilitites for testing templates""" + """ + Utilitites for testing templates. + """ # Subclasses override this to specify the file name of the template # to be loaded from capa/templates. @@ -23,7 +30,9 @@ class TemplateTestCase(unittest.TestCase): TEMPLATE_NAME = None def setUp(self): - """Load the template""" + """ + Load the template under test. + """ capa_path = capa.__path__[0] self.template_path = os.path.join(capa_path, 'templates', @@ -33,18 +42,31 @@ class TemplateTestCase(unittest.TestCase): template_file.close() def render_to_xml(self, context_dict): - """Render the template using the `context_dict` dict. - - Returns an `etree` XML element.""" + """ + Render the template using the `context_dict` dict. + Returns an `etree` XML element. + """ try: xml_str = self.template.render_unicode(**context_dict) except: raise TemplateError(exceptions.text_error_template().render()) - return etree.fromstring(xml_str) + # Attempt to construct an XML tree from the template + # This makes it easy to use XPath to make assertions, rather + # than dealing with a string. + # We modify the string slightly by wrapping it in + # tags, to ensure it has one root element. + try: + xml = etree.fromstring("" + xml_str + "") + except Exception as exc: + raise TemplateError("Could not parse XML from '{0}': {1}".format( + xml_str, str(exc))) + else: + return xml def assert_has_xpath(self, xml_root, xpath, context_dict, exact_num=1): - """Asserts that the xml tree has an element satisfying `xpath`. + """ + Asserts that the xml tree has an element satisfying `xpath`. `xml_root` is an etree XML element `xpath` is an XPath string, such as `'/foo/bar'` @@ -57,7 +79,8 @@ class TemplateTestCase(unittest.TestCase): self.assertEqual(len(xml_root.xpath(xpath)), exact_num, msg=message) def assert_no_xpath(self, xml_root, xpath, context_dict): - """Asserts that the xml tree does NOT have an element + """ + Asserts that the xml tree does NOT have an element satisfying `xpath`. `xml_root` is an etree XML element @@ -67,7 +90,8 @@ class TemplateTestCase(unittest.TestCase): self.assert_has_xpath(xml_root, xpath, context_dict, exact_num=0) def assert_has_text(self, xml_root, xpath, text, exact=True): - """Find the element at `xpath` in `xml_root` and assert + """ + Find the element at `xpath` in `xml_root` and assert that its text is `text`. `xml_root` is an etree XML element @@ -88,7 +112,9 @@ class TemplateTestCase(unittest.TestCase): class ChoiceGroupTemplateTest(TemplateTestCase): - """Test mako template for `` input""" + """ + Test mako template for `` input. + """ TEMPLATE_NAME = 'choicegroup.html' @@ -103,8 +129,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): super(ChoiceGroupTemplateTest, self).setUp() def test_problem_marked_correct(self): - """Test conditions under which the entire problem - (not a particular option) is marked correct""" + """ + Test conditions under which the entire problem + (not a particular option) is marked correct. + """ self.context['status'] = 'correct' self.context['input_type'] = 'checkbox' @@ -123,8 +151,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): self.context) def test_problem_marked_incorrect(self): - """Test all conditions under which the entire problem - (not a particular option) is marked incorrect""" + """ + Test all conditions under which the entire problem + (not a particular option) is marked incorrect. + """ conditions = [ {'status': 'incorrect', 'input_type': 'radio', 'value': ''}, {'status': 'incorrect', 'input_type': 'checkbox', 'value': []}, @@ -151,8 +181,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): self.context) def test_problem_marked_unsubmitted(self): - """Test all conditions under which the entire problem - (not a particular option) is marked unanswered""" + """ + Test all conditions under which the entire problem + (not a particular option) is marked unanswered. + """ conditions = [ {'status': 'unsubmitted', 'input_type': 'radio', 'value': ''}, {'status': 'unsubmitted', 'input_type': 'radio', 'value': []}, @@ -181,8 +213,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): self.context) def test_option_marked_correct(self): - """Test conditions under which a particular option - (not the entire problem) is marked correct.""" + """ + Test conditions under which a particular option + (not the entire problem) is marked correct. + """ conditions = [ {'input_type': 'radio', 'value': '2'}, {'input_type': 'radio', 'value': ['2']}] @@ -200,8 +234,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): self.assert_no_xpath(xml, xpath, self.context) def test_option_marked_incorrect(self): - """Test conditions under which a particular option - (not the entire problem) is marked incorrect.""" + """ + Test conditions under which a particular option + (not the entire problem) is marked incorrect. + """ conditions = [ {'input_type': 'radio', 'value': '2'}, {'input_type': 'radio', 'value': ['2']}] @@ -219,7 +255,8 @@ class ChoiceGroupTemplateTest(TemplateTestCase): self.assert_no_xpath(xml, xpath, self.context) def test_never_show_correctness(self): - """Test conditions under which we tell the template to + """ + Test conditions under which we tell the template to NOT show correct/incorrect, but instead show a message. This is used, for example, by the Justice course to ask @@ -268,8 +305,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): self.context['submitted_message']) def test_no_message_before_submission(self): - """Ensure that we don't show the `submitted_message` - before submitting""" + """ + Ensure that we don't show the `submitted_message` + before submitting. + """ conditions = [ {'input_type': 'radio', 'status': 'unsubmitted', 'value': ''}, @@ -298,7 +337,9 @@ class ChoiceGroupTemplateTest(TemplateTestCase): class TextlineTemplateTest(TemplateTestCase): - """Test mako template for `` input""" + """ + Test mako template for `` input. + """ TEMPLATE_NAME = 'textline.html' @@ -405,3 +446,271 @@ class TextlineTemplateTest(TemplateTestCase): xpath = "//span[@class='message']" self.assert_has_text(xml, xpath, self.context['msg']) + + +class AnnotationInputTemplateTest(TemplateTestCase): + """ + Test mako template for `` input. + """ + + TEMPLATE_NAME = 'annotationinput.html' + + def setUp(self): + self.context = {'id': 2, + 'value': '

Test value

', + 'title': '

This is a title

', + 'text': '

This is a test.

', + 'comment': '

This is a test comment

', + 'comment_prompt': '

This is a test comment prompt

', + 'comment_value': '

This is the value of a test comment

', + 'tag_prompt': '

This is a tag prompt

', + 'options': [], + 'has_options_value': False, + 'debug': False, + 'status': 'unsubmitted', + 'return_to_annotation': False, + 'msg': '

This is a test message

', } + super(AnnotationInputTemplateTest, self).setUp() + + def test_return_to_annotation(self): + """ + Test link for `Return to Annotation` appears if and only if + the flag is set. + """ + + xpath = "//a[@class='annotation-return']" + + # If return_to_annotation set, then show the link + self.context['return_to_annotation'] = True + xml = self.render_to_xml(self.context) + self.assert_has_xpath(xml, xpath, self.context) + + # Otherwise, do not show the links + self.context['return_to_annotation'] = False + xml = self.render_to_xml(self.context) + self.assert_no_xpath(xml, xpath, self.context) + + def test_option_selection(self): + """ + Test that selected options are selected. + """ + + # Create options 0-4 and select option 2 + self.context['options_value'] = [2] + self.context['options'] = [ + {'id': id_num, + 'choice': 'correct', + 'description': '

Unescaped HTML {0}

'.format(id_num)} + for id_num in range(0, 5)] + + xml = self.render_to_xml(self.context) + + # Expect that each option description is visible + # with unescaped HTML. + # Since the HTML is unescaped, we can traverse the XML tree + for id_num in range(0, 5): + xpath = "//span[@data-id='{0}']/p/b".format(id_num) + self.assert_has_text(xml, xpath, 'HTML {0}'.format(id_num), exact=False) + + # Expect that the correct option is selected + xpath = "//span[contains(@class,'selected')]/p/b" + self.assert_has_text(xml, xpath, 'HTML 2', exact=False) + + def test_submission_status(self): + """ + Test that the submission status displays correctly. + """ + + # Test cases of `(input_status, expected_css_class)` tuples + test_cases = [('unsubmitted', 'unanswered'), + ('incomplete', 'incorrect'), + ('incorrect', 'incorrect')] + + for (input_status, expected_css_class) in test_cases: + self.context['status'] = input_status + xml = self.render_to_xml(self.context) + + xpath = "//span[@class='{0}']".format(expected_css_class) + self.assert_has_xpath(xml, xpath, self.context) + + # If individual options are being marked, then expect + # just the option to be marked incorrect, not the whole problem + self.context['has_options_value'] = True + self.context['status'] = 'incorrect' + xpath = "//span[@class='incorrect']" + xml = self.render_to_xml(self.context) + self.assert_no_xpath(xml, xpath, self.context) + + def test_display_html_comment(self): + """ + Test that HTML comment and comment prompt render. + """ + self.context['comment'] = "

Unescaped comment HTML

" + self.context['comment_prompt'] = "

Prompt prompt HTML

" + self.context['text'] = "

Unescaped text

" + xml = self.render_to_xml(self.context) + + # Because the HTML is unescaped, we should be able to + # descend to the tag + xpath = "//div[@class='block']/p/b" + self.assert_has_text(xml, xpath, 'prompt HTML') + + xpath = "//div[@class='block block-comment']/p/b" + self.assert_has_text(xml, xpath, 'comment HTML') + + xpath = "//div[@class='block block-highlight']/p/b" + self.assert_has_text(xml, xpath, 'text') + + def test_display_html_tag_prompt(self): + """ + Test that HTML tag prompts render. + """ + self.context['tag_prompt'] = "

Unescaped HTML

" + xml = self.render_to_xml(self.context) + + # Because the HTML is unescaped, we should be able to + # descend to the tag + xpath = "//div[@class='block']/p/b" + self.assert_has_text(xml, xpath, 'HTML') + + +class MathStringTemplateTest(TemplateTestCase): + """ + Test mako template for `` input. + """ + + TEMPLATE_NAME = 'mathstring.html' + + def setUp(self): + self.context = {'isinline': False, 'mathstr': '', 'tail': ''} + super(MathStringTemplateTest, self).setUp() + + def test_math_string_inline(self): + self.context['isinline'] = True + self.context['mathstr'] = 'y = ax^2 + bx + c' + + xml = self.render_to_xml(self.context) + xpath = "//section[@class='math-string']/span[1]" + self.assert_has_text(xml, xpath, + '[mathjaxinline]y = ax^2 + bx + c[/mathjaxinline]') + + def test_math_string_not_inline(self): + self.context['isinline'] = False + self.context['mathstr'] = 'y = ax^2 + bx + c' + + xml = self.render_to_xml(self.context) + xpath = "//section[@class='math-string']/span[1]" + self.assert_has_text(xml, xpath, + '[mathjax]y = ax^2 + bx + c[/mathjax]') + + def test_tail_html(self): + self.context['tail'] = "

This is some tail HTML

" + xml = self.render_to_xml(self.context) + + # HTML from `tail` should NOT be escaped. + # We should be able to traverse it as part of the XML tree + xpath = "//section[@class='math-string']/span[2]/p/b" + self.assert_has_text(xml, xpath, 'tail') + + xpath = "//section[@class='math-string']/span[2]/p/em" + self.assert_has_text(xml, xpath, 'HTML') + + +class OptionInputTemplateTest(TemplateTestCase): + """ + Test mako template for `` input. + """ + + TEMPLATE_NAME = 'optioninput.html' + + def setUp(self): + self.context = {'id': 2, 'options': [], 'status': 'unsubmitted', 'value': 0} + super(OptionInputTemplateTest, self).setUp() + + def test_select_options(self): + + # Create options 0-4, and select option 2 + self.context['options'] = [(id_num, 'Option {0}'.format(id_num)) + for id_num in range(0, 5)] + self.context['value'] = 2 + + xml = self.render_to_xml(self.context) + + # Should have a dummy default + xpath = "//option[@value='option_2_dummy_default']" + self.assert_has_xpath(xml, xpath, self.context) + + # Should have each of the options, with the correct description + # The description HTML should NOT be escaped + # (that's why we descend into the tag) + for id_num in range(0, 5): + xpath = "//option[@value='{0}']/b".format(id_num) + self.assert_has_text(xml, xpath, 'Option {0}'.format(id_num)) + + # Should have the correct option selected + xpath = "//option[@selected='true']/b" + self.assert_has_text(xml, xpath, 'Option 2') + + def test_status(self): + + # Test cases, where each tuple represents + # `(input_status, expected_css_class)` + test_cases = [('unsubmitted', 'unanswered'), + ('correct', 'correct'), + ('incorrect', 'incorrect'), + ('incomplete', 'incorrect')] + + for (input_status, expected_css_class) in test_cases: + self.context['status'] = input_status + xml = self.render_to_xml(self.context) + + xpath = "//span[@class='{0}']".format(expected_css_class) + self.assert_has_xpath(xml, xpath, self.context) + + +class DragAndDropTemplateTest(TemplateTestCase): + """ + Test mako template for `` input. + """ + + TEMPLATE_NAME = 'drag_and_drop_input.html' + + def setUp(self): + self.context = {'id': 2, + 'drag_and_drop_json': '', + 'value': 0, + 'status': 'unsubmitted', + 'msg': ''} + super(DragAndDropTemplateTest, self).setUp() + + def test_status(self): + + # Test cases, where each tuple represents + # `(input_status, expected_css_class, expected_text)` + test_cases = [('unsubmitted', 'unanswered', 'unanswered'), + ('correct', 'correct', 'correct'), + ('incorrect', 'incorrect', 'incorrect'), + ('incomplete', 'incorrect', 'incomplete')] + + for (input_status, expected_css_class, expected_text) in test_cases: + self.context['status'] = input_status + xml = self.render_to_xml(self.context) + + # Expect a
with the status + xpath = "//div[@class='{0}']".format(expected_css_class) + self.assert_has_xpath(xml, xpath, self.context) + + # Expect a

with the status + xpath = "//p[@class='status']" + self.assert_has_text(xml, xpath, expected_text, exact=False) + + def test_drag_and_drop_json_html(self): + + json_with_html = json.dumps({'test': '

Unescaped HTML

'}) + self.context['drag_and_drop_json'] = json_with_html + xml = self.render_to_xml(self.context) + + # Assert that the JSON-encoded string was inserted without + # escaping the HTML. We should be able to traverse the XML tree. + xpath = "//div[@class='drag_and_drop_problem_json']/p/b" + self.assert_has_text(xml, xpath, 'HTML') From 5e8d2ce4f7a12d286184101a4859d7da6be9d5e5 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Mon, 20 May 2013 13:59:12 -0400 Subject: [PATCH 03/49] Start making comprehensive calc tests --- common/lib/calc/tests/test_calc.py | 266 +++++++++++++++++++++++++++++ 1 file changed, 266 insertions(+) create mode 100644 common/lib/calc/tests/test_calc.py diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py new file mode 100644 index 0000000000..87b68e36be --- /dev/null +++ b/common/lib/calc/tests/test_calc.py @@ -0,0 +1,266 @@ +""" +unittests for calc.py + +Run like this: + + rake test_common/lib/calc ?? + +""" + +import unittest +import numpy +import calc + + +class EvaluatorTest(unittest.TestCase): + ''' Run tests for calc.evaluator + Go through all functionalities as specifically as possible-- + work from number input to functions and complex expressions + Also test custom variable substitutions (i.e. + `evaluator({'x':3.0},{}, '3*x')` + gives 9.0) and more. + ''' + + def test_number_input(self): + ''' Test different kinds of float inputs ''' + easy_eval = lambda x: calc.evaluator({}, {}, x) + + self.assertEqual(easy_eval("13"), 13) + self.assertEqual(easy_eval("3.14"), 3.14) + self.assertEqual(easy_eval(".618033989"), 0.618033989) + + self.assertEqual(easy_eval("-13"), -13) + self.assertEqual(easy_eval("-3.14"), -3.14) + self.assertEqual(easy_eval("-.618033989"), -0.618033989) + # see also test_exponential_answer + # and test_si_suffix + + def test_exponential_answer(self): + ''' Test for correct interpretation of scientific notation''' + answer = 50 + correct_responses = ["50", "50.0", "5e1", "5e+1", "50e0", "50.0e0", "500e-1"] + incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"] + + for input_str in correct_responses: + result = calc.evaluator({}, {}, input_str) + fail_msg = "Failed when checking '{0}' against {1} (expected equality)".format( + input_str, answer) + self.assertEqual(answer, result, msg=fail_msg) + + for input_str in incorrect_responses: + result = calc.evaluator({}, {}, input_str) + fail_msg = "Failed when checking '{0}' against {1} (expected inequality)".format( + input_str, answer) + self.assertNotEqual(answer, result, msg=fail_msg) + + def test_si_suffix(self): + ''' Test calc.py's unique functionality of interpreting si 'suffixes'. + For instance 'k' stand for 'kilo-' so '1k' should be 1,000 ''' + test_mapping = [('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000), + ('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074), + ('5.4m', 0.0054), ('8.7u', 0.0000087), + ('5.6n', 5.6e-9), ('4.2p', 4.2e-12)] + + for (expr, answer) in test_mapping: + tolerance = answer * 1e-6 # Testing exactly fails for the large values + fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}".format( + expr[-1], expr, answer) + self.assertAlmostEqual(calc.evaluator({}, {}, expr), answer, + delta=tolerance, msg=fail_msg) + + def test_operator_sanity(self): + ''' Test for simple things like "5+2" and "5/2"''' + var1 = 5.0 + var2 = 2.0 + operators = [('+', 7), ('-', 3), ('*', 10), ('/', 2.5), ('^', 25)] + + for (operator, answer) in operators: + input_str = "{0} {1} {2}".format(var1, operator, var2) + result = calc.evaluator({}, {}, input_str) + fail_msg = "Failed on operator '{0}': '{1}' was not {2}".format( + operator, input_str, answer) + self.assertEqual(answer, result, msg=fail_msg) + + self.assertRaises(ZeroDivisionError, calc.evaluator, + {}, {}, '1/0') + + def test_parallel_resistors(self): + ''' Test the special operator || + The formula is given by + a || b || c ... + = 1 / (1/a + 1/b + 1/c + ...) + It is the resistance of a parallel circuit of resistors with resistance + a, b, c, etc&. See if this evaulates correctly.''' + self.assertEqual(calc.evaluator({}, {}, '1||1'), 0.5) + self.assertEqual(calc.evaluator({}, {}, '1||1||2'), 0.4) + + # I don't know why you would want this, but it works. + self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j) + + self.assertRaises(ZeroDivisionError, calc.evaluator, + {}, {}, '0||1') + + + def assert_function_values(self, fname, ins, outs, tolerance=1e-3): + ''' Helper function to test many values at once + Test the accuracy of evaluator's use of the function given by fname + Specifically, the equality of `fname(ins[i])` against outs[i]. + Used later to test a whole bunch of f(x) = y at a time ''' + + for (arg, val) in zip(ins, outs): + input_str = "{0}({1})".format(fname, arg) + result = calc.evaluator({}, {}, input_str) + fail_msg = "Failed on function {0}: '{1}' was not {2}".format( + fname, input_str, val) + self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg) + + def test_trig_functions(self): + """Test the trig functions provided in common/lib/calc/calc.py""" + # which are: sin, cos, tan, arccos, arcsin, arctan + + angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', 'j', '1 + j'] + sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.175j, 1.298 + 0.635j] + cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 1.543, 0.834 - 0.989j] + tan_values = [-1, 0, 0.577, 0.727, 1, 1, 0.762j, 0.272 + 1.084j] + # Cannot test tan(pi/2) b/c pi/2 is a float and not precise... + + self.assert_function_values('sin', angles, sin_values) + self.assert_function_values('cos', angles, cos_values) + self.assert_function_values('tan', angles, tan_values) + + # include those where the real part is between -pi/2 and pi/2 + arcsin_inputs = ['-0.707', '0', '0.5', '0.588', '1.298 + 0.635*j'] + arcsin_angles = [-0.785, 0, 0.524, 0.629, 1 + 1j] + self.assert_function_values('arcsin', arcsin_inputs, arcsin_angles) + # rather than throwing an exception, numpy.arcsin gives nan + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(-1.1)'))) + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(1.1)'))) + # disabled for now because they are giving a runtime warning... :-/ + + # include those where the real part is between 0 and pi + arccos_inputs = ['1', '0.866', '0.809', '0.834-0.989*j'] + arccos_angles = [0, 0.524, 0.628, 1 + 1j] + self.assert_function_values('arccos', arccos_inputs, arccos_angles) + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(-1.1)'))) + # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(1.1)'))) + + # has the same range as arcsin + arctan_inputs = ['-1', '0', '0.577', '0.727', '0.272 + 1.084*j'] + arctan_angles = arcsin_angles + self.assert_function_values('arctan', arctan_inputs, arctan_angles) + + def test_other_functions(self): + """Test the other functions provided in common/lib/calc/calc.py""" + # sqrt, log10, log2, ln, abs, + # fact, factorial + + # test sqrt + self.assert_function_values('sqrt', + [0, 1, 2, 1024], # -1 + [0, 1, 1.414, 32]) # 1j + # sqrt(-1) is NAN not j (!!). + + # test logs + self.assert_function_values('log10', + [0.1, 1, 3.162, 1000000, '1+j'], + [-1, 0, 0.5, 6, 0.151 + 0.341j]) + self.assert_function_values('log2', + [0.5, 1, 1.414, 1024, '1+j'], + [-1, 0, 0.5, 10, 0.5 + 1.133j]) + self.assert_function_values('ln', + [0.368, 1, 1.649, 2.718, 42, '1+j'], + [-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j]) + + # test abs + self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1]) + + # test factorial + fact_inputs = [0, 1, 3, 7] + fact_values = [1, 1, 6, 5040] + self.assert_function_values('fact', fact_inputs, fact_values) + self.assert_function_values('factorial', fact_inputs, fact_values) + + self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(-1)") + self.assertRaises(ValueError, calc.evaluator, {}, {}, "fact(0.5)") + self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(-1)") + self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(0.5)") + + def test_constants(self): + """Test the default constants provided in common/lib/calc/calc.py""" + # which are: j (complex number), e, pi, k, c, T, q + + # ('expr', python value, tolerance (or None for exact)) + default_variables = [('j', 1j, None), + ('e', 2.7183, 1e-3), + ('pi', 3.1416, 1e-3), + # c = speed of light + ('c', 2.998e8, 1e5), + # 0 deg C = T Kelvin + ('T', 298.15, 0.01), + # note k = scipy.constants.k = 1.3806488e-23 + ('k', 1.3806488e-23, 1e-26), + # note q = scipy.constants.e = 1.602176565e-19 + ('q', 1.602176565e-19, 1e-22)] + for (variable, value, tolerance) in default_variables: + fail_msg = "Failed on constant '{0}', not within bounds".format( + variable) + result = calc.evaluator({}, {}, variable) + if tolerance is None: + self.assertEqual(value, result, msg=fail_msg) + else: + self.assertAlmostEqual(value, result, + delta=tolerance, msg=fail_msg) + + def test_complex_expression(self): + """We've only tried simple things so far, make sure it can handle a + more complexity than this.""" + + self.assertAlmostEqual( + calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"), + 10.180, + delta=1e-3) + + self.assertAlmostEqual( + calc.evaluator({}, {}, "1+1/(1+1/(1+1/(1+1)))"), + 1.6, + delta=1e-3) + self.assertAlmostEqual( + calc.evaluator({}, {}, "10||sin(7+5)"), + -0.567, delta=0.01) + + def test_calc(self): + variables = {'R1': 2.0, 'R3': 4.0} + functions = {'sin': numpy.sin, 'cos': numpy.cos} + + self.assertAlmostEqual( + calc.evaluator(variables, functions, "10||sin(7+5)"), + -0.567, delta=0.01) + self.assertEqual(calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13"), 13) + self.assertEqual(calc.evaluator(variables, functions, "13"), 13) + self.assertEqual( + calc.evaluator({ + 'a': 2.2997471478310274, 'k': 9, 'm': 8, + 'x': 0.66009498411213041}, + {}, "5"), + 5) + self.assertEqual(calc.evaluator(variables, functions, "R1*R3"), 8.0) + self.assertAlmostEqual(calc.evaluator(variables, functions, "sin(e)"), 0.41, delta=0.01) + self.assertAlmostEqual(calc.evaluator(variables, functions, "k*T/q"), 0.025, delta=1e-3) + self.assertAlmostEqual(calc.evaluator(variables, functions, "e^(j*pi)"), -1, delta=1e-5) + + variables['t'] = 1.0 + self.assertAlmostEqual(calc.evaluator(variables, functions, "t"), 1.0, delta=1e-5) + self.assertAlmostEqual(calc.evaluator(variables, functions, "T"), 1.0, delta=1e-5) + self.assertAlmostEqual(calc.evaluator(variables, functions, "t", cs=True), 1.0, delta=1e-5) + self.assertAlmostEqual(calc.evaluator(variables, functions, "T", cs=True), 298, delta=0.2) + + self.assertRaises(calc.UndefinedVariable, calc.evaluator, + {}, {}, "5+7 QWSEKO") + + self.assertRaises(calc.UndefinedVariable, calc.evaluator, + {'r1': 5}, {}, "r1+r2") + + self.assertEqual(calc.evaluator(variables, functions, "r1*r3"), 8.0) + + self.assertRaises(calc.UndefinedVariable, calc.evaluator, + variables, functions, "r1*r3", cs=True) From 82482e89ea2bab3eb426bc7ce0fabf2e6b9fe731 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Tue, 21 May 2013 11:07:19 -0400 Subject: [PATCH 04/49] Finish calc.py tests; add docstrings, etc --- common/lib/calc/tests/test_calc.py | 126 ++++++++++++++++++++--------- 1 file changed, 86 insertions(+), 40 deletions(-) diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py index 87b68e36be..2e77aeec6c 100644 --- a/common/lib/calc/tests/test_calc.py +++ b/common/lib/calc/tests/test_calc.py @@ -38,18 +38,19 @@ class EvaluatorTest(unittest.TestCase): def test_exponential_answer(self): ''' Test for correct interpretation of scientific notation''' answer = 50 - correct_responses = ["50", "50.0", "5e1", "5e+1", "50e0", "50.0e0", "500e-1"] + correct_responses = ["50", "50.0", "5e1", "5e+1", + "50e0", "50.0e0", "500e-1"] incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"] for input_str in correct_responses: result = calc.evaluator({}, {}, input_str) - fail_msg = "Failed when checking '{0}' against {1} (expected equality)".format( + fail_msg = "Expected '{0}' to equal {1}".format( input_str, answer) self.assertEqual(answer, result, msg=fail_msg) for input_str in incorrect_responses: result = calc.evaluator({}, {}, input_str) - fail_msg = "Failed when checking '{0}' against {1} (expected inequality)".format( + fail_msg = "Expected '{0}' to not equal {1}".format( input_str, answer) self.assertNotEqual(answer, result, msg=fail_msg) @@ -62,9 +63,9 @@ class EvaluatorTest(unittest.TestCase): ('5.6n', 5.6e-9), ('4.2p', 4.2e-12)] for (expr, answer) in test_mapping: - tolerance = answer * 1e-6 # Testing exactly fails for the large values - fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}".format( - expr[-1], expr, answer) + tolerance = answer * 1e-6 # Make rel. tolerance, because of floats + fail_msg = "Failure in testing suffix '{0}': '{1}' was not {2}" + fail_msg = fail_msg.format(expr[-1], expr, answer) self.assertAlmostEqual(calc.evaluator({}, {}, expr), answer, delta=tolerance, msg=fail_msg) @@ -85,7 +86,7 @@ class EvaluatorTest(unittest.TestCase): {}, {}, '1/0') def test_parallel_resistors(self): - ''' Test the special operator || + ''' Test the parallel resistor operator || The formula is given by a || b || c ... = 1 / (1/a + 1/b + 1/c + ...) @@ -100,7 +101,6 @@ class EvaluatorTest(unittest.TestCase): self.assertRaises(ZeroDivisionError, calc.evaluator, {}, {}, '0||1') - def assert_function_values(self, fname, ins, outs, tolerance=1e-3): ''' Helper function to test many values at once Test the accuracy of evaluator's use of the function given by fname @@ -115,12 +115,12 @@ class EvaluatorTest(unittest.TestCase): self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg) def test_trig_functions(self): - """Test the trig functions provided in common/lib/calc/calc.py""" - # which are: sin, cos, tan, arccos, arcsin, arctan + """Test the trig functions provided in common/lib/calc/calc.py + which are: sin, cos, tan, arccos, arcsin, arctan""" - angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', 'j', '1 + j'] - sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.175j, 1.298 + 0.635j] - cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 1.543, 0.834 - 0.989j] + angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] + sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.298 + 0.635j] + cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 0.834 - 0.989j] tan_values = [-1, 0, 0.577, 0.727, 1, 1, 0.762j, 0.272 + 1.084j] # Cannot test tan(pi/2) b/c pi/2 is a float and not precise... @@ -150,9 +150,10 @@ class EvaluatorTest(unittest.TestCase): self.assert_function_values('arctan', arctan_inputs, arctan_angles) def test_other_functions(self): - """Test the other functions provided in common/lib/calc/calc.py""" - # sqrt, log10, log2, ln, abs, - # fact, factorial + """Test the non-trig functions provided in common/lib/calc/calc.py + Specifically: + sqrt, log10, log2, ln, abs, + fact, factorial""" # test sqrt self.assert_function_values('sqrt', @@ -212,8 +213,7 @@ class EvaluatorTest(unittest.TestCase): delta=tolerance, msg=fail_msg) def test_complex_expression(self): - """We've only tried simple things so far, make sure it can handle a - more complexity than this.""" + """ Calculate combinations of operators and default functions """ self.assertAlmostEqual( calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"), @@ -227,40 +227,86 @@ class EvaluatorTest(unittest.TestCase): self.assertAlmostEqual( calc.evaluator({}, {}, "10||sin(7+5)"), -0.567, delta=0.01) + self.assertAlmostEqual(calc.evaluator({}, {}, "sin(e)"), + 0.41, delta=0.01) + self.assertAlmostEqual(calc.evaluator({}, {}, "k*T/q"), + 0.025, delta=1e-3) + self.assertAlmostEqual(calc.evaluator({}, {}, "e^(j*pi)"), + -1, delta=1e-5) - def test_calc(self): - variables = {'R1': 2.0, 'R3': 4.0} - functions = {'sin': numpy.sin, 'cos': numpy.cos} + def test_simple_vars(self): + """ Substitution of variables into simple equations """ + variables = {'x': 9.72, 'y': 7.91, 'loooooong': 6.4} - self.assertAlmostEqual( - calc.evaluator(variables, functions, "10||sin(7+5)"), - -0.567, delta=0.01) - self.assertEqual(calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13"), 13) - self.assertEqual(calc.evaluator(variables, functions, "13"), 13) + # Should not change value of constant + self.assertEqual(calc.evaluator(variables, {}, '13'), 13) + + # Easy evaluation + self.assertEqual(calc.evaluator(variables, {}, 'x'), 9.72) + self.assertEqual(calc.evaluator(variables, {}, 'y'), 7.91) + self.assertEqual(calc.evaluator(variables, {}, 'loooooong'), 6.4) + + # Test a simple equation + self.assertAlmostEqual(calc.evaluator(variables, {}, '3*x-y'), + 21.25, delta=0.01) # = 3 * 9.72 - 7.91 + self.assertAlmostEqual(calc.evaluator(variables, {}, 'x*y'), + 76.89, delta=0.01) + + # more, I guess + self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, "13"), 13) + self.assertEqual(calc.evaluator(variables, {}, "13"), 13) self.assertEqual( calc.evaluator({ 'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5"), 5) - self.assertEqual(calc.evaluator(variables, functions, "R1*R3"), 8.0) - self.assertAlmostEqual(calc.evaluator(variables, functions, "sin(e)"), 0.41, delta=0.01) - self.assertAlmostEqual(calc.evaluator(variables, functions, "k*T/q"), 0.025, delta=1e-3) - self.assertAlmostEqual(calc.evaluator(variables, functions, "e^(j*pi)"), -1, delta=1e-5) - variables['t'] = 1.0 - self.assertAlmostEqual(calc.evaluator(variables, functions, "t"), 1.0, delta=1e-5) - self.assertAlmostEqual(calc.evaluator(variables, functions, "T"), 1.0, delta=1e-5) - self.assertAlmostEqual(calc.evaluator(variables, functions, "t", cs=True), 1.0, delta=1e-5) - self.assertAlmostEqual(calc.evaluator(variables, functions, "T", cs=True), 298, delta=0.2) + def test_variable_case_sensitivity(self): + """ Test the case sensitivity flag and corresponding behavior """ + self.assertEqual( + calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"), + 8.0) + + variables = {'t': 1.0} + self.assertEqual(calc.evaluator(variables, {}, "t"), 1.0) + self.assertEqual(calc.evaluator(variables, {}, "T"), 1.0) + self.assertEqual(calc.evaluator(variables, {}, "t", cs=True), 1.0) + # Recall 'T' is a default constant, with value 298.15 + self.assertAlmostEqual(calc.evaluator(variables, {}, "T", cs=True), + 298, delta=0.2) + + def test_simple_funcs(self): + """ Subsitution of custom functions """ + variables = {'x': 4.712} + functions = {'id': lambda x: x} + self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81) + self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81) + self.assertEqual(calc.evaluator(variables, functions, 'id(x)'), 4.712) + + functions.update({'f': numpy.sin}) + self.assertAlmostEqual(calc.evaluator(variables, functions, 'f(x)'), + -1, delta=1e-3) + + def test_function_case_sensitivity(self): + """ Test the case sensitivity of functions """ + functions = {'f': lambda x: x, + 'F': lambda x: x + 1} + # Which is it? will it call f or F? + # In any case, they should both be the same... + self.assertEqual(calc.evaluator({}, functions, 'f(6)'), + calc.evaluator({}, functions, 'F(6)')) + # except if we want case sensitivity... + self.assertNotEqual(calc.evaluator({}, functions, 'f(6)', cs=True), + calc.evaluator({}, functions, 'F(6)', cs=True)) + + def test_undefined_vars(self): + """ Check to see if the evaluator catches undefined variables """ + variables = {'R1': 2.0, 'R3': 4.0} self.assertRaises(calc.UndefinedVariable, calc.evaluator, {}, {}, "5+7 QWSEKO") - self.assertRaises(calc.UndefinedVariable, calc.evaluator, {'r1': 5}, {}, "r1+r2") - - self.assertEqual(calc.evaluator(variables, functions, "r1*r3"), 8.0) - self.assertRaises(calc.UndefinedVariable, calc.evaluator, - variables, functions, "r1*r3", cs=True) + variables, {}, "r1*r3", cs=True) From 43395b70b7356bf794ed224a86eb716c10c5f27a Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Tue, 21 May 2013 15:20:51 -0400 Subject: [PATCH 05/49] Enable coverage for calc module; split off tests --- common/lib/calc/.coveragerc | 15 +++++++++++++++ common/lib/calc/calc.py | 24 ------------------------ common/lib/calc/tests/test_calc.py | 11 +++++++++-- 3 files changed, 24 insertions(+), 26 deletions(-) create mode 100644 common/lib/calc/.coveragerc diff --git a/common/lib/calc/.coveragerc b/common/lib/calc/.coveragerc new file mode 100644 index 0000000000..352ddf399e --- /dev/null +++ b/common/lib/calc/.coveragerc @@ -0,0 +1,15 @@ +# .coveragerc for common/lib/calc +[run] +data_file = reports/common/lib/calc/.coverage +source = common/lib/calc +branch = true + +[report] +ignore_errors = True + +[html] +title = Calc Python Test Coverage Report +directory = reports/common/lib/calc/cover + +[xml] +output = reports/common/lib/calc/coverage.xml diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index bb1fb97153..2271676f34 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -230,27 +230,3 @@ def evaluator(variables, functions, string, cs=False): expr << Optional((plus | minus)) + term + ZeroOrMore((plus | minus) + term) # -5 + 4 - 3 expr = expr.setParseAction(sum_parse_action) return (expr + stringEnd).parseString(string)[0] - -if __name__ == '__main__': - variables = {'R1': 2.0, 'R3': 4.0} - functions = {'sin': numpy.sin, 'cos': numpy.cos} - print "X", evaluator(variables, functions, "10000||sin(7+5)-6k") - print "X", evaluator(variables, functions, "13") - print evaluator({'R1': 2.0, 'R3': 4.0}, {}, "13") - - print evaluator({'e1': 1, 'e2': 1.0, 'R3': 7, 'V0': 5, 'R5': 15, 'I1': 1, 'R4': 6}, {}, "e2") - - print evaluator({'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, {}, "5") - print evaluator({}, {}, "-1") - print evaluator({}, {}, "-(7+5)") - print evaluator({}, {}, "-0.33") - print evaluator({}, {}, "-.33") - print evaluator({}, {}, "5+1*j") - print evaluator({}, {}, "j||1") - print evaluator({}, {}, "e^(j*pi)") - print evaluator({}, {}, "fact(5)") - print evaluator({}, {}, "factorial(5)") - try: - print evaluator({}, {}, "5+7 QWSEKO") - except UndefinedVariable: - print "Successfully caught undefined variable" diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py index 2e77aeec6c..41710030ca 100644 --- a/common/lib/calc/tests/test_calc.py +++ b/common/lib/calc/tests/test_calc.py @@ -3,7 +3,7 @@ unittests for calc.py Run like this: - rake test_common/lib/calc ?? + rake test_common/lib/calc """ @@ -82,6 +82,8 @@ class EvaluatorTest(unittest.TestCase): operator, input_str, answer) self.assertEqual(answer, result, msg=fail_msg) + def test_raises_zero_division_err(self): + ''' Ensure division by zero gives an error ''' self.assertRaises(ZeroDivisionError, calc.evaluator, {}, {}, '1/0') @@ -98,6 +100,8 @@ class EvaluatorTest(unittest.TestCase): # I don't know why you would want this, but it works. self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j) + def test_parallel_resistors_zero_error(self): + ''' Check the behavior of the || operator with 0 ''' self.assertRaises(ZeroDivisionError, calc.evaluator, {}, {}, '0||1') @@ -121,7 +125,7 @@ class EvaluatorTest(unittest.TestCase): angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.298 + 0.635j] cos_values = [0.707, 1, 0.866, 0.809, -0.707, 0.707, 0.834 - 0.989j] - tan_values = [-1, 0, 0.577, 0.727, 1, 1, 0.762j, 0.272 + 1.084j] + tan_values = [-1, 0, 0.577, 0.727, 1, 1, 0.272 + 1.084j] # Cannot test tan(pi/2) b/c pi/2 is a float and not precise... self.assert_function_values('sin', angles, sin_values) @@ -239,6 +243,9 @@ class EvaluatorTest(unittest.TestCase): variables = {'x': 9.72, 'y': 7.91, 'loooooong': 6.4} # Should not change value of constant + # even with different numbers of variables... + self.assertEqual(calc.evaluator({'x': 9.72}, {}, '13'), 13) + self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, '13'), 13) self.assertEqual(calc.evaluator(variables, {}, '13'), 13) # Easy evaluation From e52acbbf2220f60182faff501db06b9a972480da Mon Sep 17 00:00:00 2001 From: Arthur Barrett Date: Tue, 21 May 2013 17:49:20 -0400 Subject: [PATCH 06/49] Fix advanced modules list. --- cms/djangoapps/contentstore/views/component.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cms/djangoapps/contentstore/views/component.py b/cms/djangoapps/contentstore/views/component.py index 34a659ab29..89b5e8bdc7 100644 --- a/cms/djangoapps/contentstore/views/component.py +++ b/cms/djangoapps/contentstore/views/component.py @@ -42,7 +42,7 @@ COMPONENT_TYPES = ['customtag', 'discussion', 'html', 'problem', 'video'] OPEN_ENDED_COMPONENT_TYPES = ["combinedopenended", "peergrading"] NOTE_COMPONENT_TYPES = ['notes'] -ADVANCED_COMPONENT_TYPES = ['annotatable' + 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES +ADVANCED_COMPONENT_TYPES = ['annotatable', 'word_cloud'] + OPEN_ENDED_COMPONENT_TYPES + NOTE_COMPONENT_TYPES ADVANCED_COMPONENT_CATEGORY = 'advanced' ADVANCED_COMPONENT_POLICY_KEY = 'advanced_modules' From 534d67378bd20223bc5b4eedf290a67f36120369 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 21 May 2013 17:25:37 -0400 Subject: [PATCH 07/49] Sandbox-installed packages will be really installed instead of -e installed. --- common/lib/calc/setup.py | 2 +- common/lib/chem/setup.py | 2 +- common/lib/sandbox-packages/setup.py | 2 +- requirements/edx-sandbox/post.txt | 10 +++++++--- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/common/lib/calc/setup.py b/common/lib/calc/setup.py index f7bb1708af..cb638914f9 100644 --- a/common/lib/calc/setup.py +++ b/common/lib/calc/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="calc", - version="0.1", + version="0.1.1", py_modules=["calc"], install_requires=[ "pyparsing==1.5.6", diff --git a/common/lib/chem/setup.py b/common/lib/chem/setup.py index 4f2b24ddee..642c9a4fe5 100644 --- a/common/lib/chem/setup.py +++ b/common/lib/chem/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="chem", - version="0.1", + version="0.1.1", packages=["chem"], install_requires=[ "pyparsing==1.5.6", diff --git a/common/lib/sandbox-packages/setup.py b/common/lib/sandbox-packages/setup.py index 1b99118aca..96c1190e38 100644 --- a/common/lib/sandbox-packages/setup.py +++ b/common/lib/sandbox-packages/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="sandbox-packages", - version="0.1", + version="0.1.1", packages=[ "verifiers", ], diff --git a/requirements/edx-sandbox/post.txt b/requirements/edx-sandbox/post.txt index f99e8a8c4b..d122795d18 100644 --- a/requirements/edx-sandbox/post.txt +++ b/requirements/edx-sandbox/post.txt @@ -1,6 +1,10 @@ # Packages to install in the Python sandbox for secured execution. scipy==0.11.0 lxml==3.0.1 --e common/lib/calc --e common/lib/chem --e common/lib/sandbox-packages + +# Install these packages from the edx-platform working tree +# NOTE: if you change code in these packages, you MUST change the version +# number in its setup.py or the code WILL NOT be installed during deploy. +common/lib/calc +common/lib/chem +common/lib/sandbox-packages From 0647a5b0a360b588ce4009e61d1dddfd16571103 Mon Sep 17 00:00:00 2001 From: e0d Date: Wed, 22 May 2013 10:19:30 -0400 Subject: [PATCH 08/49] changes to refactor local requirements. --- requirements/edx-sandbox/local.txt | 6 ++++++ requirements/edx-sandbox/post.txt | 7 ------- 2 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 requirements/edx-sandbox/local.txt diff --git a/requirements/edx-sandbox/local.txt b/requirements/edx-sandbox/local.txt new file mode 100644 index 0000000000..ba24805057 --- /dev/null +++ b/requirements/edx-sandbox/local.txt @@ -0,0 +1,6 @@ +# Install these packages from the edx-platform working tree +# NOTE: if you change code in these packages, you MUST change the version +# number in its setup.py or the code WILL NOT be installed during deploy. +common/lib/calc +common/lib/chem +common/lib/sandbox-packages diff --git a/requirements/edx-sandbox/post.txt b/requirements/edx-sandbox/post.txt index d122795d18..218fdf307e 100644 --- a/requirements/edx-sandbox/post.txt +++ b/requirements/edx-sandbox/post.txt @@ -1,10 +1,3 @@ # Packages to install in the Python sandbox for secured execution. scipy==0.11.0 lxml==3.0.1 - -# Install these packages from the edx-platform working tree -# NOTE: if you change code in these packages, you MUST change the version -# number in its setup.py or the code WILL NOT be installed during deploy. -common/lib/calc -common/lib/chem -common/lib/sandbox-packages From db261dc1c32d4c4e5408c81d89eef9eb37d02dea Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Tue, 21 May 2013 15:20:51 -0400 Subject: [PATCH 09/49] Add docstrings; test divsion by zero at Response level --- .../lib/capa/capa/tests/test_responsetypes.py | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 8bf6954139..25fef0cd3f 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -349,10 +349,13 @@ class OptionResponseTest(ResponseTest): class FormulaResponseTest(ResponseTest): + """ Test the FormulaResponse class """ from response_xml_factory import FormulaResponseXMLFactory xml_factory_class = FormulaResponseXMLFactory def test_grade(self): + """ Test basic functionality of FormulaResponse + Specifically, if it can understand equivalence of formulae""" # Sample variables x and y in the range [-10, 10] sample_dict = {'x': (-10, 10), 'y': (-10, 10)} @@ -373,6 +376,7 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, input_formula, "incorrect") def test_hint(self): + """ Test the hint-giving functionality of FormulaResponse""" # Sample variables x and y in the range [-10, 10] sample_dict = {'x': (-10, 10), 'y': (-10, 10)} @@ -401,6 +405,8 @@ class FormulaResponseTest(ResponseTest): 'Try including the variable x') def test_script(self): + """ Test if python script can be used to generate answers""" + # Calculate the answer using a script script = "calculated_ans = 'x+x'" @@ -496,8 +502,8 @@ class FormulaResponseTest(ResponseTest): msg="Failed on function {0}; the given, incorrect answer was {1} but graded 'correct'".format(func, incorrect)) def test_grade_infinity(self): - # This resolves a bug where a problem with relative tolerance would - # pass with any arbitrarily large student answer. + """This resolves a bug where a problem with relative tolerance would + pass with any arbitrarily large student answer""" sample_dict = {'x': (1, 2)} @@ -514,8 +520,8 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, input_formula, "incorrect") def test_grade_nan(self): - # Attempt to produce a value which causes the student's answer to be - # evaluated to nan. See if this is resolved correctly. + """Attempt to produce a value which causes the student's answer to be + evaluated to nan. See if this is resolved correctly.""" sample_dict = {'x': (1, 2)} @@ -532,6 +538,15 @@ class FormulaResponseTest(ResponseTest): input_formula = "x + 0*1e999" self.assert_grade(problem, input_formula, "incorrect") + def test_raises_zero_division_err(self): + """See if division by zero is handled correctly""" + sample_dict = {'x': (1, 2)} + problem = self.build_problem(sample_dict=sample_dict, + num_samples=10, + tolerance="1%", + answer="x") # Answer doesn't matter + input_dict = {'1_2_1': '1/0'} + self.assertRaises(StudentInputError, problem.grade_answers, input_dict) class StringResponseTest(ResponseTest): from response_xml_factory import StringResponseXMLFactory @@ -898,6 +913,13 @@ class NumericalResponseTest(ResponseTest): incorrect_responses = ["", "3.9", "4.1", "0", "5.01e1"] self.assert_multiple_grade(problem, correct_responses, incorrect_responses) + def test_raises_zero_division_err(self): + """See if division by zero is handled correctly""" + problem = self.build_problem(question_text="What 5 * 10?", + explanation="The answer is 50", + answer="5e+1") # Answer doesn't matter + input_dict = {'1_2_1': '1/0'} + self.assertRaises(StudentInputError, problem.grade_answers, input_dict) class CustomResponseTest(ResponseTest): from response_xml_factory import CustomResponseXMLFactory From 70898a06daecce393590cde93dae4e03092dcc59 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Thu, 23 May 2013 11:09:23 -0400 Subject: [PATCH 10/49] Pep8 fix --- common/lib/capa/capa/tests/test_responsetypes.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 25fef0cd3f..18ca9b3cf5 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -223,7 +223,6 @@ class SymbolicResponseTest(ResponseTest): for (input_str, input_mathml) in incorrect_inputs: self._assert_symbolic_grade(problem, input_str, input_mathml, 'incorrect') - def test_complex_number_grade(self): problem = self.build_problem(math_display=True, expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]", @@ -241,7 +240,7 @@ class SymbolicResponseTest(ResponseTest): # Correct answer with mock.patch.object(requests, 'post') as mock_post: - # Simulate what the LaTeX-to-MathML server would + # Simulate what the LaTeX-to-MathML server would # send for the correct response input mock_post.return_value.text = correct_snuggletex_response @@ -323,7 +322,7 @@ class SymbolicResponseTest(ResponseTest): dynamath_input, expected_correctness): input_dict = {'1_2_1': str(student_input), - '1_2_1_dynamath': str(dynamath_input) } + '1_2_1_dynamath': str(dynamath_input)} correct_map = problem.grade_answers(input_dict) @@ -548,6 +547,7 @@ class FormulaResponseTest(ResponseTest): input_dict = {'1_2_1': '1/0'} self.assertRaises(StudentInputError, problem.grade_answers, input_dict) + class StringResponseTest(ResponseTest): from response_xml_factory import StringResponseXMLFactory xml_factory_class = StringResponseXMLFactory @@ -607,7 +607,7 @@ class StringResponseTest(ResponseTest): problem = self.build_problem( answer="Michigan", hintfn="gimme_a_hint", - script = textwrap.dedent(""" + script=textwrap.dedent(""" def gimme_a_hint(answer_ids, student_answers, new_cmap, old_cmap): aid = answer_ids[0] answer = student_answers[aid] @@ -917,10 +917,11 @@ class NumericalResponseTest(ResponseTest): """See if division by zero is handled correctly""" problem = self.build_problem(question_text="What 5 * 10?", explanation="The answer is 50", - answer="5e+1") # Answer doesn't matter + answer="5e+1") # Answer doesn't matter input_dict = {'1_2_1': '1/0'} self.assertRaises(StudentInputError, problem.grade_answers, input_dict) + class CustomResponseTest(ResponseTest): from response_xml_factory import CustomResponseXMLFactory xml_factory_class = CustomResponseXMLFactory @@ -969,8 +970,8 @@ class CustomResponseTest(ResponseTest): # # '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) - # - # The function should return a dict of the form + # + # The function should return a dict of the form # { 'ok': BOOL, 'msg': STRING } # script = textwrap.dedent(""" From a16cb4b1342d98af8ebfb230945b0b815ea13be2 Mon Sep 17 00:00:00 2001 From: e0d Date: Thu, 23 May 2013 12:35:36 -0400 Subject: [PATCH 11/49] resolved confict based on change branching later than the live hotfix branch --- requirements/edx/github.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index d3f90d5abc..936c5709ae 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -9,4 +9,4 @@ # Our libraries: -e git+https://github.com/edx/XBlock.git@483e0cb1#egg=XBlock --e git+https://github.com/edx/codejail.git@07494f1#egg=codejail +-e git+https://github.com/edx/codejail.git@72cf791#egg=codejail From 2486f7c2719e91745f121c6b509b5f8155d7ace4 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Thu, 23 May 2013 17:29:14 -0400 Subject: [PATCH 12/49] Fixed docstrings and other comments from PR review --- common/lib/calc/tests/test_calc.py | 138 +++++++++++------- .../lib/capa/capa/tests/test_responsetypes.py | 58 +++++--- doc/testing.md | 5 + 3 files changed, 130 insertions(+), 71 deletions(-) diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py index 41710030ca..cd28801d83 100644 --- a/common/lib/calc/tests/test_calc.py +++ b/common/lib/calc/tests/test_calc.py @@ -1,10 +1,5 @@ """ -unittests for calc.py - -Run like this: - - rake test_common/lib/calc - +Unit tests for calc.py """ import unittest @@ -13,16 +8,19 @@ import calc class EvaluatorTest(unittest.TestCase): - ''' Run tests for calc.evaluator + """ + Run tests for calc.evaluator Go through all functionalities as specifically as possible-- work from number input to functions and complex expressions Also test custom variable substitutions (i.e. `evaluator({'x':3.0},{}, '3*x')` gives 9.0) and more. - ''' + """ def test_number_input(self): - ''' Test different kinds of float inputs ''' + """ + Test different kinds of float inputs + """ easy_eval = lambda x: calc.evaluator({}, {}, x) self.assertEqual(easy_eval("13"), 13) @@ -32,11 +30,12 @@ class EvaluatorTest(unittest.TestCase): self.assertEqual(easy_eval("-13"), -13) self.assertEqual(easy_eval("-3.14"), -3.14) self.assertEqual(easy_eval("-.618033989"), -0.618033989) - # see also test_exponential_answer - # and test_si_suffix + # See also test_exponential_answer and test_si_suffix def test_exponential_answer(self): - ''' Test for correct interpretation of scientific notation''' + """ + Test for correct interpretation of scientific notation + """ answer = 50 correct_responses = ["50", "50.0", "5e1", "5e+1", "50e0", "50.0e0", "500e-1"] @@ -55,8 +54,11 @@ class EvaluatorTest(unittest.TestCase): self.assertNotEqual(answer, result, msg=fail_msg) def test_si_suffix(self): - ''' Test calc.py's unique functionality of interpreting si 'suffixes'. - For instance 'k' stand for 'kilo-' so '1k' should be 1,000 ''' + """ + Test calc.py's unique functionality of interpreting si 'suffixes'. + + For instance 'k' stand for 'kilo-' so '1k' should be 1,000 + """ test_mapping = [('4.2%', 0.042), ('2.25k', 2250), ('8.3M', 8300000), ('9.9G', 9.9e9), ('1.2T', 1.2e12), ('7.4c', 0.074), ('5.4m', 0.0054), ('8.7u', 0.0000087), @@ -70,7 +72,9 @@ class EvaluatorTest(unittest.TestCase): delta=tolerance, msg=fail_msg) def test_operator_sanity(self): - ''' Test for simple things like "5+2" and "5/2"''' + """ + Test for simple things like '5+2' and '5/2' + """ var1 = 5.0 var2 = 2.0 operators = [('+', 7), ('-', 3), ('*', 10), ('/', 2.5), ('^', 25)] @@ -83,33 +87,41 @@ class EvaluatorTest(unittest.TestCase): self.assertEqual(answer, result, msg=fail_msg) def test_raises_zero_division_err(self): - ''' Ensure division by zero gives an error ''' + """ + Ensure division by zero gives an error + """ self.assertRaises(ZeroDivisionError, calc.evaluator, {}, {}, '1/0') def test_parallel_resistors(self): - ''' Test the parallel resistor operator || + """ + Test the parallel resistor operator || + The formula is given by a || b || c ... = 1 / (1/a + 1/b + 1/c + ...) It is the resistance of a parallel circuit of resistors with resistance - a, b, c, etc&. See if this evaulates correctly.''' + a, b, c, etc&. See if this evaulates correctly. + """ self.assertEqual(calc.evaluator({}, {}, '1||1'), 0.5) self.assertEqual(calc.evaluator({}, {}, '1||1||2'), 0.4) - - # I don't know why you would want this, but it works. self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j) def test_parallel_resistors_zero_error(self): - ''' Check the behavior of the || operator with 0 ''' + """ + Check the behavior of the || operator with 0 + """ self.assertRaises(ZeroDivisionError, calc.evaluator, {}, {}, '0||1') def assert_function_values(self, fname, ins, outs, tolerance=1e-3): - ''' Helper function to test many values at once + """ + Helper function to test many values at once + Test the accuracy of evaluator's use of the function given by fname Specifically, the equality of `fname(ins[i])` against outs[i]. - Used later to test a whole bunch of f(x) = y at a time ''' + This is used later to test a whole bunch of f(x) = y at a time + """ for (arg, val) in zip(ins, outs): input_str = "{0}({1})".format(fname, arg) @@ -119,8 +131,11 @@ class EvaluatorTest(unittest.TestCase): self.assertAlmostEqual(val, result, delta=tolerance, msg=fail_msg) def test_trig_functions(self): - """Test the trig functions provided in common/lib/calc/calc.py - which are: sin, cos, tan, arccos, arcsin, arctan""" + """ + Test the trig functions provided in calc.py + + which are: sin, cos, tan, arccos, arcsin, arctan + """ angles = ['-pi/4', '0', 'pi/6', 'pi/5', '5*pi/4', '9*pi/4', '1 + j'] sin_values = [-0.707, 0, 0.5, 0.588, -0.707, 0.707, 1.298 + 0.635j] @@ -132,40 +147,43 @@ class EvaluatorTest(unittest.TestCase): self.assert_function_values('cos', angles, cos_values) self.assert_function_values('tan', angles, tan_values) - # include those where the real part is between -pi/2 and pi/2 + # Include those where the real part is between -pi/2 and pi/2 arcsin_inputs = ['-0.707', '0', '0.5', '0.588', '1.298 + 0.635*j'] arcsin_angles = [-0.785, 0, 0.524, 0.629, 1 + 1j] self.assert_function_values('arcsin', arcsin_inputs, arcsin_angles) - # rather than throwing an exception, numpy.arcsin gives nan + # Rather than throwing an exception, numpy.arcsin gives nan # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(-1.1)'))) # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arcsin(1.1)'))) - # disabled for now because they are giving a runtime warning... :-/ + # Disabled for now because they are giving a runtime warning... :-/ - # include those where the real part is between 0 and pi + # Include those where the real part is between 0 and pi arccos_inputs = ['1', '0.866', '0.809', '0.834-0.989*j'] arccos_angles = [0, 0.524, 0.628, 1 + 1j] self.assert_function_values('arccos', arccos_inputs, arccos_angles) # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(-1.1)'))) # self.assertTrue(numpy.isnan(calc.evaluator({}, {}, 'arccos(1.1)'))) - # has the same range as arcsin + # Has the same range as arcsin arctan_inputs = ['-1', '0', '0.577', '0.727', '0.272 + 1.084*j'] arctan_angles = arcsin_angles self.assert_function_values('arctan', arctan_inputs, arctan_angles) def test_other_functions(self): - """Test the non-trig functions provided in common/lib/calc/calc.py + """ + Test the non-trig functions provided in calc.py + Specifically: sqrt, log10, log2, ln, abs, - fact, factorial""" + fact, factorial + """ - # test sqrt + # Test sqrt self.assert_function_values('sqrt', [0, 1, 2, 1024], # -1 [0, 1, 1.414, 32]) # 1j # sqrt(-1) is NAN not j (!!). - # test logs + # Test logs self.assert_function_values('log10', [0.1, 1, 3.162, 1000000, '1+j'], [-1, 0, 0.5, 6, 0.151 + 0.341j]) @@ -176,10 +194,10 @@ class EvaluatorTest(unittest.TestCase): [0.368, 1, 1.649, 2.718, 42, '1+j'], [-1, 0, 0.5, 1, 3.738, 0.347 + 0.785j]) - # test abs + # Test abs self.assert_function_values('abs', [-1, 0, 1, 'j'], [1, 0, 1, 1]) - # test factorial + # Test factorial fact_inputs = [0, 1, 3, 7] fact_values = [1, 1, 6, 5040] self.assert_function_values('fact', fact_inputs, fact_values) @@ -191,10 +209,13 @@ class EvaluatorTest(unittest.TestCase): self.assertRaises(ValueError, calc.evaluator, {}, {}, "factorial(0.5)") def test_constants(self): - """Test the default constants provided in common/lib/calc/calc.py""" - # which are: j (complex number), e, pi, k, c, T, q + """ + Test the default constants provided in calc.py - # ('expr', python value, tolerance (or None for exact)) + which are: j (complex number), e, pi, k, c, T, q + """ + + # Of the form ('expr', python value, tolerance (or None for exact)) default_variables = [('j', 1j, None), ('e', 2.7183, 1e-3), ('pi', 3.1416, 1e-3), @@ -202,9 +223,9 @@ class EvaluatorTest(unittest.TestCase): ('c', 2.998e8, 1e5), # 0 deg C = T Kelvin ('T', 298.15, 0.01), - # note k = scipy.constants.k = 1.3806488e-23 + # Note k = scipy.constants.k = 1.3806488e-23 ('k', 1.3806488e-23, 1e-26), - # note q = scipy.constants.e = 1.602176565e-19 + # Note q = scipy.constants.e = 1.602176565e-19 ('q', 1.602176565e-19, 1e-22)] for (variable, value, tolerance) in default_variables: fail_msg = "Failed on constant '{0}', not within bounds".format( @@ -214,10 +235,12 @@ class EvaluatorTest(unittest.TestCase): self.assertEqual(value, result, msg=fail_msg) else: self.assertAlmostEqual(value, result, - delta=tolerance, msg=fail_msg) + delta=tolerance, msg=fail_msg) def test_complex_expression(self): - """ Calculate combinations of operators and default functions """ + """ + Calculate combinations of operators and default functions + """ self.assertAlmostEqual( calc.evaluator({}, {}, "(2^2+1.0)/sqrt(5e0)*5-1"), @@ -239,7 +262,9 @@ class EvaluatorTest(unittest.TestCase): -1, delta=1e-5) def test_simple_vars(self): - """ Substitution of variables into simple equations """ + """ + Substitution of variables into simple equations + """ variables = {'x': 9.72, 'y': 7.91, 'loooooong': 6.4} # Should not change value of constant @@ -259,18 +284,19 @@ class EvaluatorTest(unittest.TestCase): self.assertAlmostEqual(calc.evaluator(variables, {}, 'x*y'), 76.89, delta=0.01) - # more, I guess self.assertEqual(calc.evaluator({'x': 9.72, 'y': 7.91}, {}, "13"), 13) self.assertEqual(calc.evaluator(variables, {}, "13"), 13) self.assertEqual( calc.evaluator({ 'a': 2.2997471478310274, 'k': 9, 'm': 8, 'x': 0.66009498411213041}, - {}, "5"), + {}, "5"), 5) def test_variable_case_sensitivity(self): - """ Test the case sensitivity flag and corresponding behavior """ + """ + Test the case sensitivity flag and corresponding behavior + """ self.assertEqual( calc.evaluator({'R1': 2.0, 'R3': 4.0}, {}, "r1*r3"), 8.0) @@ -284,7 +310,9 @@ class EvaluatorTest(unittest.TestCase): 298, delta=0.2) def test_simple_funcs(self): - """ Subsitution of custom functions """ + """ + Subsitution of custom functions + """ variables = {'x': 4.712} functions = {'id': lambda x: x} self.assertEqual(calc.evaluator({}, functions, 'id(2.81)'), 2.81) @@ -296,19 +324,23 @@ class EvaluatorTest(unittest.TestCase): -1, delta=1e-3) def test_function_case_sensitivity(self): - """ Test the case sensitivity of functions """ + """ + Test the case sensitivity of functions + """ functions = {'f': lambda x: x, 'F': lambda x: x + 1} - # Which is it? will it call f or F? - # In any case, they should both be the same... + # Test case insensitive evaluation + # Both evaulations should call the same function self.assertEqual(calc.evaluator({}, functions, 'f(6)'), calc.evaluator({}, functions, 'F(6)')) - # except if we want case sensitivity... + # Test case sensitive evaluation self.assertNotEqual(calc.evaluator({}, functions, 'f(6)', cs=True), calc.evaluator({}, functions, 'F(6)', cs=True)) def test_undefined_vars(self): - """ Check to see if the evaluator catches undefined variables """ + """ + Check to see if the evaluator catches undefined variables + """ variables = {'R1': 2.0, 'R3': 4.0} self.assertRaises(calc.UndefinedVariable, calc.evaluator, diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 18ca9b3cf5..77cd547e55 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -190,7 +190,7 @@ class SymbolicResponseTest(ResponseTest): def test_grade_single_input(self): problem = self.build_problem(math_display=True, - expect="2*x+3*y") + expect="2*x+3*y") # Correct answers correct_inputs = [ @@ -348,13 +348,18 @@ class OptionResponseTest(ResponseTest): class FormulaResponseTest(ResponseTest): - """ Test the FormulaResponse class """ + """ + Test the FormulaResponse class + """ from response_xml_factory import FormulaResponseXMLFactory xml_factory_class = FormulaResponseXMLFactory def test_grade(self): - """ Test basic functionality of FormulaResponse - Specifically, if it can understand equivalence of formulae""" + """ + Test basic functionality of FormulaResponse + + Specifically, if it can understand equivalence of formulae + """ # Sample variables x and y in the range [-10, 10] sample_dict = {'x': (-10, 10), 'y': (-10, 10)} @@ -375,7 +380,9 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, input_formula, "incorrect") def test_hint(self): - """ Test the hint-giving functionality of FormulaResponse""" + """ + Test the hint-giving functionality of FormulaResponse + """ # Sample variables x and y in the range [-10, 10] sample_dict = {'x': (-10, 10), 'y': (-10, 10)} @@ -404,7 +411,9 @@ class FormulaResponseTest(ResponseTest): 'Try including the variable x') def test_script(self): - """ Test if python script can be used to generate answers""" + """ + Test if python script can be used to generate answers + """ # Calculate the answer using a script script = "calculated_ans = 'x+x'" @@ -424,7 +433,9 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, '3*x', 'incorrect') def test_parallel_resistors(self): - """Test parallel resistors""" + """ + Test parallel resistors + """ sample_dict = {'R1': (10, 10), 'R2': (2, 2), 'R3': (5, 5), 'R4': (1, 1)} # Test problem @@ -445,8 +456,11 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, input_formula, "incorrect") def test_default_variables(self): - """Test the default variables provided in common/lib/capa/capa/calc.py""" - # which are: j (complex number), e, pi, k, c, T, q + """ + Test the default variables provided in calc.py + + which are: j (complex number), e, pi, k, c, T, q + """ # Sample x in the range [-10,10] sample_dict = {'x': (-10, 10)} @@ -469,11 +483,14 @@ class FormulaResponseTest(ResponseTest): msg="Failed on variable {0}; the given, incorrect answer was {1} but graded 'correct'".format(var, incorrect)) def test_default_functions(self): - """Test the default functions provided in common/lib/capa/capa/calc.py""" - # which are: sin, cos, tan, sqrt, log10, log2, ln, - # arccos, arcsin, arctan, abs, - # fact, factorial + """ + Test the default functions provided in common/lib/capa/capa/calc.py + which are: + sin, cos, tan, sqrt, log10, log2, ln, + arccos, arcsin, arctan, abs, + fact, factorial + """ w = random.randint(3, 10) sample_dict = {'x': (-10, 10), # Sample x in the range [-10,10] 'y': (1, 10), # Sample y in the range [1,10] - logs, arccos need positive inputs @@ -501,8 +518,10 @@ class FormulaResponseTest(ResponseTest): msg="Failed on function {0}; the given, incorrect answer was {1} but graded 'correct'".format(func, incorrect)) def test_grade_infinity(self): - """This resolves a bug where a problem with relative tolerance would - pass with any arbitrarily large student answer""" + """ + Test that a large input on a problem with relative tolerance isn't + erroneously marked as correct. + """ sample_dict = {'x': (1, 2)} @@ -519,8 +538,9 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, input_formula, "incorrect") def test_grade_nan(self): - """Attempt to produce a value which causes the student's answer to be - evaluated to nan. See if this is resolved correctly.""" + """ + Test that expressions that evaluate to NaN are not marked as correct. + """ sample_dict = {'x': (1, 2)} @@ -538,7 +558,9 @@ class FormulaResponseTest(ResponseTest): self.assert_grade(problem, input_formula, "incorrect") def test_raises_zero_division_err(self): - """See if division by zero is handled correctly""" + """ + See if division by zero raises an error. + """ sample_dict = {'x': (1, 2)} problem = self.build_problem(sample_dict=sample_dict, num_samples=10, diff --git a/doc/testing.md b/doc/testing.md index d6c7b7ee86..8b80177798 100644 --- a/doc/testing.md +++ b/doc/testing.md @@ -115,6 +115,11 @@ xmodule can be tested independently, with this: rake test_common/lib/xmodule +other module level tests include + +* `rake test_common/lib/capa` +* `rake test_common/lib/calc` + To run a single django test class: django-admin.py test --settings=lms.envs.test --pythonpath=. lms/djangoapps/courseware/tests/tests.py:TestViewAuth From 2793bb43373ff860556854694a17412c8f020e26 Mon Sep 17 00:00:00 2001 From: Peter Baratta Date: Thu, 23 May 2013 17:23:17 -0400 Subject: [PATCH 13/49] Add few more tests; fix a small bug with parallel resistors --- common/lib/calc/calc.py | 2 ++ common/lib/calc/tests/test_calc.py | 11 ++++++++--- common/lib/capa/capa/tests/test_responsetypes.py | 1 - 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/common/lib/calc/calc.py b/common/lib/calc/calc.py index 2271676f34..2f33b66bfd 100644 --- a/common/lib/calc/calc.py +++ b/common/lib/calc/calc.py @@ -144,6 +144,8 @@ def evaluator(variables, functions, string, cs=False): return x def parallel(x): # Parallel resistors [ 1 2 ] => 2/3 + # convert from pyparsing.ParseResults, which doesn't support '0 in x' + x = list(x) if len(x) == 1: return x[0] if 0 in x: diff --git a/common/lib/calc/tests/test_calc.py b/common/lib/calc/tests/test_calc.py index cd28801d83..58d0860af6 100644 --- a/common/lib/calc/tests/test_calc.py +++ b/common/lib/calc/tests/test_calc.py @@ -92,6 +92,10 @@ class EvaluatorTest(unittest.TestCase): """ self.assertRaises(ZeroDivisionError, calc.evaluator, {}, {}, '1/0') + self.assertRaises(ZeroDivisionError, calc.evaluator, + {}, {}, '1/0.0') + self.assertRaises(ZeroDivisionError, calc.evaluator, + {'x': 0.0}, {}, '1/x') def test_parallel_resistors(self): """ @@ -107,12 +111,13 @@ class EvaluatorTest(unittest.TestCase): self.assertEqual(calc.evaluator({}, {}, '1||1||2'), 0.4) self.assertEqual(calc.evaluator({}, {}, "j||1"), 0.5 + 0.5j) - def test_parallel_resistors_zero_error(self): + def test_parallel_resistors_with_zero(self): """ Check the behavior of the || operator with 0 """ - self.assertRaises(ZeroDivisionError, calc.evaluator, - {}, {}, '0||1') + self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0||1'))) + self.assertTrue(numpy.isnan(calc.evaluator({}, {}, '0.0||1'))) + self.assertTrue(numpy.isnan(calc.evaluator({'x': 0.0}, {}, 'x||1'))) def assert_function_values(self, fname, ins, outs, tolerance=1e-3): """ diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 77cd547e55..780c475b09 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -10,7 +10,6 @@ import random import unittest import textwrap import mock -import textwrap from . import new_loncapa_problem, test_system From 6794b73d99ea57f003d086ff87e26a585db5e2e5 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 21 Mar 2013 16:28:39 +0200 Subject: [PATCH 14/49] Added videoalpha version 1 to the advanced components in studio. --- .../xmodule/xmodule/templates/videoalpha/default.yaml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml index 69ed22cc1e..6fe7b2b0e1 100644 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -1,7 +1,13 @@ --- metadata: - display_name: default + display_name: Video Alpha 1 data_dir: a_made_up_name + is_graded: False + version: 1 data: | - + + + + + children: [] From 53f80d39915168a44c229735843a41c2ec9787a6 Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Mon, 29 Apr 2013 12:08:31 -0400 Subject: [PATCH 15/49] standard video introduction --- .../lib/xmodule/xmodule/templates/videoalpha/default.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml index 6fe7b2b0e1..a0d215c64f 100644 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -5,9 +5,9 @@ metadata: is_graded: False version: 1 data: | - - - - + + + + children: [] From c238f76fe856df23226c875a3cbfab80b0b3144b Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Mon, 29 Apr 2013 13:01:38 -0400 Subject: [PATCH 16/49] different default section --- common/lib/xmodule/xmodule/templates/videoalpha/default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml index a0d215c64f..a87a2a571c 100644 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -5,7 +5,7 @@ metadata: is_graded: False version: 1 data: | - + From d68162f9fae3fd6b136bea6f23dd5d0e8c2e96b8 Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Thu, 9 May 2013 09:08:22 -0400 Subject: [PATCH 17/49] removed from and to labels --- common/lib/xmodule/xmodule/templates/videoalpha/default.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml index a87a2a571c..d655c26d37 100644 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -5,7 +5,7 @@ metadata: is_graded: False version: 1 data: | - + From 56f60e06e4aed12d6ce48713dfaa296d3833a888 Mon Sep 17 00:00:00 2001 From: Lyla Fischer Date: Mon, 13 May 2013 08:14:54 -0400 Subject: [PATCH 18/49] changed 'from' and 'to' to 'start_time' and 'end_time' --- common/lib/xmodule/xmodule/videoalpha_module.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 6754f8f664..83d0b4badd 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -93,7 +93,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): return result def _get_timeframe(self, xmltree): - """ Converts 'from' and 'to' parameters in video tag to seconds. + """ Converts 'start_time' and 'end_time' parameters in video tag to seconds. If there are no parameters, returns empty string. """ def parse_time(s): @@ -107,7 +107,7 @@ class VideoAlphaModule(VideoAlphaFields, XModule): minutes=x.tm_min, seconds=x.tm_sec).total_seconds() - return parse_time(xmltree.get('from')), parse_time(xmltree.get('to')) + return parse_time(xmltree.get('start_time')), parse_time(xmltree.get('end_time')) def handle_ajax(self, dispatch, get): """Handle ajax calls to this video. From aaeb3a862dca331f9a5d9169802f3c151232f989 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Fri, 17 May 2013 12:48:34 +0300 Subject: [PATCH 19/49] Added Python tests for start_time and end_time parameters. --- .../lib/xmodule/xmodule/tests/test_logic.py | 34 ++++++++++++++++++- .../lib/xmodule/xmodule/videoalpha_module.py | 8 +++-- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/common/lib/xmodule/xmodule/tests/test_logic.py b/common/lib/xmodule/xmodule/tests/test_logic.py index 6cd46a26ee..e60af63921 100644 --- a/common/lib/xmodule/xmodule/tests/test_logic.py +++ b/common/lib/xmodule/xmodule/tests/test_logic.py @@ -4,10 +4,12 @@ import json import unittest +from lxml import etree + from xmodule.poll_module import PollDescriptor from xmodule.conditional_module import ConditionalDescriptor from xmodule.word_cloud_module import WordCloudDescriptor - +from xmodule.videoalpha_module import VideoAlphaDescriptor class PostData: """Class which emulate postdata.""" @@ -117,3 +119,33 @@ class WordCloudModuleTest(LogicTest): ) self.assertEqual(100.0, sum(i['percent'] for i in response['top_words']) ) + + +class VideoAlphaModuleTest(LogicTest): + descriptor_class = VideoAlphaDescriptor + + raw_model_data = { + 'data': '' + } + + def test_get_timeframe_no_parameters(self): + xmltree = etree.fromstring('test') + output = self.xmodule._get_timeframe(xmltree) + self.assertEqual(output, ('', '')) + + def test_get_timeframe_with_one_parameter(self): + xmltree = etree.fromstring( + 'test' + ) + output = self.xmodule._get_timeframe(xmltree) + self.assertEqual(output, (247, '')) + + def test_get_timeframe_with_two_parameters(self): + xmltree = etree.fromstring( + '''test''' + ) + output = self.xmodule._get_timeframe(xmltree) + self.assertEqual(output, (247, 47079)) diff --git a/common/lib/xmodule/xmodule/videoalpha_module.py b/common/lib/xmodule/xmodule/videoalpha_module.py index 83d0b4badd..16230480a7 100644 --- a/common/lib/xmodule/xmodule/videoalpha_module.py +++ b/common/lib/xmodule/xmodule/videoalpha_module.py @@ -103,9 +103,11 @@ class VideoAlphaModule(VideoAlphaFields, XModule): return '' else: x = time.strptime(s, '%H:%M:%S') - return datetime.timedelta(hours=x.tm_hour, - minutes=x.tm_min, - seconds=x.tm_sec).total_seconds() + return datetime.timedelta( + hours=x.tm_hour, + minutes=x.tm_min, + seconds=x.tm_sec + ).total_seconds() return parse_time(xmltree.get('start_time')), parse_time(xmltree.get('end_time')) From 1e8c4e9edfd74dc292d1cf1e58b0f22fd6a10a73 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 23 May 2013 11:59:12 +0300 Subject: [PATCH 20/49] Rebase + updated videoalpha template. Now no data_dir attribute and is_graded set to false. --- common/lib/xmodule/xmodule/templates/videoalpha/default.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml index d655c26d37..5ff3a8f004 100644 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -1,7 +1,6 @@ --- metadata: display_name: Video Alpha 1 - data_dir: a_made_up_name is_graded: False version: 1 data: | From 587ac0ecf0d44046aa0cd0e913a50b516c67bda7 Mon Sep 17 00:00:00 2001 From: Valera Rozuvan Date: Thu, 23 May 2013 13:24:06 +0300 Subject: [PATCH 21/49] Removed is_graded attribute. In HTML yaml it is not used, and that is not graded. --- common/lib/xmodule/xmodule/templates/videoalpha/default.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml index 5ff3a8f004..dba8bbd0b4 100644 --- a/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml +++ b/common/lib/xmodule/xmodule/templates/videoalpha/default.yaml @@ -1,7 +1,6 @@ --- metadata: display_name: Video Alpha 1 - is_graded: False version: 1 data: | From 7d3d34c69e1938914e3134e93b37619f8c3523f4 Mon Sep 17 00:00:00 2001 From: Greg Price Date: Wed, 22 May 2013 13:08:07 -0400 Subject: [PATCH 22/49] Remove tags for comment client request time histogram in Datadog According to someone from Datadog, this was generating tags like "knowledgeable_ people_who_put_this_course_together._this_is_harvard._you_can_t_tell_us_there_s_ a_shortage_of_editorial_talent." They say that they can handle tens or hundreds of unique tags but not thousands. Given that we have a unique URL for each thread, we can't even use that as a tag. Thus, all tags are removed for now until we can determine whether there is a useful set of tags with small enough cardinality. In light of this, I did not investigate why the long tag mentioned above was being generated. --- lms/lib/comment_client/utils.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lms/lib/comment_client/utils.py b/lms/lib/comment_client/utils.py index 1ce03ed3c7..fce9516739 100644 --- a/lms/lib/comment_client/utils.py +++ b/lms/lib/comment_client/utils.py @@ -31,14 +31,9 @@ def merge_dict(dic1, dic2): def perform_request(method, url, data_or_params=None, *args, **kwargs): if data_or_params is None: data_or_params = {} - tags = [ - "{k}:{v}".format(k=k, v=v) - for (k, v) in data_or_params.items() + [("method", method), ("url", url)] - if k != 'api_key' - ] data_or_params['api_key'] = settings.API_KEY try: - with dog_stats_api.timer('comment_client.request.time', tags=tags): + with dog_stats_api.timer('comment_client.request.time'): if method in ['post', 'put', 'patch']: response = requests.request(method, url, data=data_or_params, timeout=5) else: From 8d7b46b51b44a7c97cf0a2786e8e62e4f3efcfa3 Mon Sep 17 00:00:00 2001 From: Giulio Gratta Date: Tue, 28 May 2013 10:15:39 -0700 Subject: [PATCH 23/49] fix footer element spacing issues --- lms/static/sass/shared/_footer.scss | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss index e3e99ae301..b7ff7973ec 100644 --- a/lms/static/sass/shared/_footer.scss +++ b/lms/static/sass/shared/_footer.scss @@ -33,8 +33,8 @@ // colophon .colophon { - margin-right: flex-gutter(2); - width: flex-grid(6,12); + //margin-right: flex-gutter(2); + width: flex-grid(8,12); float: left; .nav-colophon { @@ -71,7 +71,7 @@ p { float: left; - width: 460px; + width: flex-grid(9,12); margin-left: $baseline; padding-left: $baseline; font-size: em(13); @@ -91,7 +91,7 @@ text-align: right; li { - margin-right: ($baseline/10); + //margin-right: ($baseline/10); display: inline-block; &:last-child { From ef6c365ca0c67905772fe1dc9d2f37e1147c22e7 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 28 May 2013 13:36:47 -0400 Subject: [PATCH 24/49] lms/mktg - revises paragraph in colophon portion of footer to match footer's flex-grid value --- lms/static/sass/shared/_footer.scss | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss index b7ff7973ec..100977639c 100644 --- a/lms/static/sass/shared/_footer.scss +++ b/lms/static/sass/shared/_footer.scss @@ -71,7 +71,7 @@ p { float: left; - width: flex-grid(9,12); + width: flex-grid(6,8); margin-left: $baseline; padding-left: $baseline; font-size: em(13); @@ -154,9 +154,5 @@ .colophon-about img { margin-top: ($baseline*1.5); } - - .colophon-about p { - width: 360px; - } } } From 25305dc8f7f2d76e3b18b77991c4955f74f3dae1 Mon Sep 17 00:00:00 2001 From: Brian Talbot Date: Tue, 28 May 2013 13:44:16 -0400 Subject: [PATCH 25/49] lms/mktg - removes margins from social icons and reinstates flex-gutter margin to colophon area of footer --- lms/static/sass/shared/_footer.scss | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lms/static/sass/shared/_footer.scss b/lms/static/sass/shared/_footer.scss index 100977639c..864cf57d03 100644 --- a/lms/static/sass/shared/_footer.scss +++ b/lms/static/sass/shared/_footer.scss @@ -33,7 +33,7 @@ // colophon .colophon { - //margin-right: flex-gutter(2); + margin-right: flex-gutter(); width: flex-grid(8,12); float: left; @@ -91,7 +91,6 @@ text-align: right; li { - //margin-right: ($baseline/10); display: inline-block; &:last-child { From 186da0d5fe6f2931522a626ea618d438c6e2bb3e Mon Sep 17 00:00:00 2001 From: Giulio Gratta Date: Tue, 28 May 2013 10:49:58 -0700 Subject: [PATCH 26/49] fix user button/dropdown spacing/scrolling issue --- lms/static/sass/shared/_header.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/lms/static/sass/shared/_header.scss b/lms/static/sass/shared/_header.scss index 6987b35c84..0608a8faf4 100644 --- a/lms/static/sass/shared/_header.scss +++ b/lms/static/sass/shared/_header.scss @@ -14,7 +14,6 @@ header.global { padding: 18px 10px 0px; max-width: grid-width(12); min-width: 760px; - width: flex-grid(12); } h1.logo { From d38f2661535ad66fc947a0a3da252e6a6a1f69a3 Mon Sep 17 00:00:00 2001 From: James Tauber Date: Tue, 28 May 2013 15:14:47 -0400 Subject: [PATCH 27/49] updated mitxmako README to clarify license and origin --- common/djangoapps/mitxmako/README | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/common/djangoapps/mitxmako/README b/common/djangoapps/mitxmako/README index ab04df2cf7..9896d78747 100644 --- a/common/djangoapps/mitxmako/README +++ b/common/djangoapps/mitxmako/README @@ -1,3 +1,11 @@ +The code in this directory is based on: + + django-mako Copyright (c) 2008 Mikeal Rogers + +and is redistributed here with modifications under the same Apache 2.0 license +as the orginal. + + ================================================================================ django-mako ================================================================================ From 54fb46cee4ca0de0f29c5e63109c88c6f99a9338 Mon Sep 17 00:00:00 2001 From: Brian Wilson Date: Tue, 28 May 2013 15:30:25 -0400 Subject: [PATCH 28/49] make debug_request available only when DEBUG is enabled, so that production doesn't have this security risk. --- lms/urls.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/lms/urls.py b/lms/urls.py index d1bee076fc..98c36df9b0 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -22,10 +22,6 @@ urlpatterns = ('', # nopep8 url(r'^admin_dashboard$', 'dashboard.views.dashboard'), - # Adding to allow debugging issues when prod is mysteriously different from staging - # (specifically missing get parameters in certain cases) - url(r'^debug_request$', 'util.views.debug_request'), - url(r'^change_email$', 'student.views.change_email_request', name="change_email"), url(r'^email_confirm/(?P[^/]*)$', 'student.views.confirm_email_change'), url(r'^change_name$', 'student.views.change_name_request', name="change_name"), @@ -334,6 +330,13 @@ if settings.DEBUG or settings.MITX_FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'): ## Jasmine and admin urlpatterns += (url(r'^admin/', include(admin.site.urls)),) +if settings.DEBUG: + # Originally added to allow debugging issues when prod is + # mysteriously different from staging (specifically missing get + # parameters in certain cases), but removing from prod because + # it's a security risk. + urlpatterns += (url(r'^debug_request$', 'util.views.debug_request'),) + if settings.MITX_FEATURES.get('AUTH_USE_OPENID'): urlpatterns += ( url(r'^openid/login/$', 'django_openid_auth.views.login_begin', name='openid-login'), From c976fee2d938d91af088ddaecefa8ce6e4d17afe Mon Sep 17 00:00:00 2001 From: Jane Manning Date: Tue, 28 May 2013 14:09:58 -0700 Subject: [PATCH 29/49] Copy changes to make dup email or public-username messages more informative --- common/djangoapps/student/views.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/djangoapps/student/views.py b/common/djangoapps/student/views.py index 8059026e12..463ad33316 100644 --- a/common/djangoapps/student/views.py +++ b/common/djangoapps/student/views.py @@ -527,12 +527,12 @@ def _do_create_account(post_vars): js = {'success': False} # Figure out the cause of the integrity error if len(User.objects.filter(username=post_vars['username'])) > 0: - js['value'] = "An account with this username already exists." + js['value'] = "An account with the Public Username '" + post_vars['username'] + "' already exists." js['field'] = 'username' return HttpResponse(json.dumps(js)) if len(User.objects.filter(email=post_vars['email'])) > 0: - js['value'] = "An account with this e-mail already exists." + js['value'] = "An account with the Email '" + post_vars['email'] + "' already exists." js['field'] = 'email' return HttpResponse(json.dumps(js)) From ce9acf9fb22204584745b7187ed3821f62351821 Mon Sep 17 00:00:00 2001 From: Ned Batchelder Date: Tue, 28 May 2013 21:45:46 -0400 Subject: [PATCH 30/49] Use latest CodeJail to prevent bytecode writing. --- requirements/edx/github.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/edx/github.txt b/requirements/edx/github.txt index f280d66557..092ec997b7 100644 --- a/requirements/edx/github.txt +++ b/requirements/edx/github.txt @@ -9,4 +9,4 @@ # Our libraries: -e git+https://github.com/edx/XBlock.git@2144a25d#egg=XBlock --e git+https://github.com/edx/codejail.git@72cf791#egg=codejail +-e git+https://github.com/edx/codejail.git@874361f#egg=codejail From 4c97d16edc6aca1cc34ac08f880273e0f0b4a382 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Tue, 28 May 2013 23:40:16 -0400 Subject: [PATCH 31/49] add hack to assets.rake to unblock the release train --- rakefiles/assets.rake | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/rakefiles/assets.rake b/rakefiles/assets.rake index c0757b712b..68127a317f 100644 --- a/rakefiles/assets.rake +++ b/rakefiles/assets.rake @@ -10,11 +10,14 @@ end # the ENV_TOKENS to the templating context. def preprocess_with_mako(filename) # simple command-line invocation of Mako engine + # cdodge: the .gsub() are used to translate true->True and false->False to make the generated + # python actually valid python. This is just a short term hack to unblock the release train + # until a real fix can be made by people who know this better mako = "from mako.template import Template;" + "print Template(filename=\"#{filename}\")" + # Total hack. It works because a Python dict literal has # the same format as a JSON object. - ".render(env=#{ENV_TOKENS.to_json});" + ".render(env=#{ENV_TOKENS.to_json.gsub("true","True").gsub("false","False")});" # strip off the .mako extension output_filename = filename.chomp(File.extname(filename)) From 25dae57226447cfb9efbebca30945ab50e37a653 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 29 May 2013 11:16:33 -0400 Subject: [PATCH 32/49] add the new category which can act as a category --- common/lib/xmodule/xmodule/modulestore/mongo.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/lib/xmodule/xmodule/modulestore/mongo.py b/common/lib/xmodule/xmodule/modulestore/mongo.py index 24df17b15b..be01328733 100644 --- a/common/lib/xmodule/xmodule/modulestore/mongo.py +++ b/common/lib/xmodule/xmodule/modulestore/mongo.py @@ -268,7 +268,7 @@ class MongoModuleStore(ModuleStoreBase): query = {'_id.org': location.org, '_id.course': location.course, '_id.category': {'$in': ['course', 'chapter', 'sequential', 'vertical', - 'wrapper', 'problemset', 'conditional']} + 'wrapper', 'problemset', 'conditional', 'randomize']} } # we just want the Location, children, and inheritable metadata record_filter = {'_id': 1, 'definition.children': 1} From eb4dd76328aa48deed9a4de09f5fce35e8093fc9 Mon Sep 17 00:00:00 2001 From: Chris Dodge Date: Wed, 29 May 2013 12:02:41 -0400 Subject: [PATCH 33/49] update POST-back URL to the new one - post Drupal --- lms/templates/university_profile/edge.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lms/templates/university_profile/edge.html b/lms/templates/university_profile/edge.html index a3e115ddd8..9e6adfe3d8 100644 --- a/lms/templates/university_profile/edge.html +++ b/lms/templates/university_profile/edge.html @@ -9,7 +9,7 @@