From 082216395c57d3f6c89e97178c4e5fa1ffe44fa8 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Mon, 4 Mar 2013 12:38:11 -0500
Subject: [PATCH 01/13] Added unit test for conversion of GET parameters for
capa module. Added error checking for possible error cases
---
common/lib/xmodule/xmodule/capa_module.py | 35 ++++++++++---
.../xmodule/xmodule/tests/test_capa_module.py | 50 +++++++++++++++++++
2 files changed, 77 insertions(+), 8 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index ade5cc6ef6..05a12f1761 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -534,15 +534,34 @@ class CapaModule(XModule):
# e.g. input_resistor_1 ==> resistor_1
_, _, name = key.partition('_')
- # This allows for answers which require more than one value for
- # the same form input (e.g. checkbox inputs). The convention is that
- # if the name ends with '[]' (which looks like an array), then the
- # answer will be an array.
- if not name.endswith('[]'):
- answers[name] = get[key]
+ # If key has no underscores, then partition
+ # will return (key, '', '')
+ # We detect this and raise an error
+ if name is '':
+ raise ValueError("%s must contain at least one underscore" % str(key))
+
else:
- name = name[:-2]
- answers[name] = get.getlist(key)
+ # This allows for answers which require more than one value for
+ # the same form input (e.g. checkbox inputs). The convention is that
+ # if the name ends with '[]' (which looks like an array), then the
+ # answer will be an array.
+ is_list_key = name.endswith('[]')
+ name = name[:-2] if is_list_key else name
+
+ if is_list_key:
+ if type(get[key]) is list:
+ val = get[key]
+ else:
+ val = [get[key]]
+ else:
+ val = get[key]
+
+ # If the name already exists, then we don't want
+ # to override it. Raise an error instead
+ if name in answers:
+ raise ValueError("Key %s already exists in answers dict" % str(name))
+ else:
+ answers[name] = val
return answers
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index e84267c1e7..7b565ff2da 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -282,3 +282,53 @@ class CapaModuleTest(unittest.TestCase):
due=self.yesterday_str,
graceperiod=self.two_day_delta_str)
self.assertTrue(still_in_grace.answer_available())
+
+
+ def test_parse_get_params(self):
+
+ # Valid GET param dict
+ valid_get_dict = {'input_1': 'test',
+ 'input_1_2': 'test',
+ 'input_1_2_3': 'test',
+ 'input_[]_3': 'test',
+ 'input_4': None,
+ 'input_5': [],
+ 'input_6': 5}
+
+ result = CapaModule.make_dict_of_responses(valid_get_dict)
+
+ # Expect that we get a dict with "input" stripped from key names
+ # and that we get the same values back
+ for key in result.keys():
+ original_key = "input_" + key
+ self.assertTrue(original_key in valid_get_dict,
+ "Output dict should have key %s" % original_key)
+ self.assertEqual(valid_get_dict[original_key], result[key])
+
+
+ # Valid GET param dict with list keys
+ valid_get_dict = {'input_2[]': ['test1', 'test2']}
+ result = CapaModule.make_dict_of_responses(valid_get_dict)
+ self.assertTrue('2' in result)
+ self.assertEqual(valid_get_dict['input_2[]'], result['2'])
+
+ # If we use [] at the end of a key name, we should always
+ # get a list, even if there's just one value
+ valid_get_dict = {'input_1[]': 'test'}
+ result = CapaModule.make_dict_of_responses(valid_get_dict)
+ self.assertEqual(result['1'], ['test'])
+
+
+ # If we have no underscores in the name, then the key is invalid
+ invalid_get_dict = {'input': 'test'}
+ with self.assertRaises(ValueError):
+ result = CapaModule.make_dict_of_responses(invalid_get_dict)
+
+
+ # Two equivalent names (one list, one non-list)
+ # One of the values would overwrite the other, so detect this
+ # and raise an exception
+ invalid_get_dict = {'input_1[]': 'test 1',
+ 'input_1': 'test 2' }
+ with self.assertRaises(ValueError):
+ result = CapaModule.make_dict_of_responses(invalid_get_dict)
From 1cc895078067e00f1b194528b0c660f1ffd43051 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Mon, 4 Mar 2013 12:45:13 -0500
Subject: [PATCH 02/13] Added comments for make_dict_of_responses() in capa
module
---
common/lib/xmodule/xmodule/capa_module.py | 21 +++++++++++++++++++++
1 file changed, 21 insertions(+)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 05a12f1761..d9ada0038e 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -528,6 +528,27 @@ class CapaModule(XModule):
def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers")
get is POST dictionary.
+
+ The *get* dict has keys of the form 'x_y', which are mapped
+ to key 'y' in the returned dict. For example,
+ 'input_1_2_3' would be mapped to '1_2_3' in the returned dict.
+
+ Some inputs always expect a list in the returned dict
+ (e.g. checkbox inputs). The convention is that
+ keys in the *get* dict that end with '[]' will always
+ have list values in the returned dict.
+ For example, if the *get* dict contains {'input_1[]': 'test' }
+ then the output dict would contain {'1': ['test'] }
+ (the value is a list).
+
+ Raises an exception if:
+
+ A key in the *get* dictionary does not contain >= 1 underscores
+ (e.g. "input" is invalid; "input_1" is valid)
+
+ Two keys end up with the same name in the returned dict.
+ (e.g. 'input_1' and 'input_1[]', which both get mapped
+ to 'input_1' in the returned dict)
'''
answers = dict()
for key in get:
From d14890685179460236fbdde5b040c677252de244 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Mon, 4 Mar 2013 16:06:14 -0500
Subject: [PATCH 03/13] Added unit tests for capa module check_problem() method
---
.../xmodule/xmodule/tests/test_capa_module.py | 147 ++++++++++++++++--
1 file changed, 137 insertions(+), 10 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 7b565ff2da..c569309af9 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -1,9 +1,11 @@
import datetime
import json
-from mock import Mock
+from mock import Mock, patch
from pprint import pprint
import unittest
+import xmodule
+import capa
from xmodule.capa_module import CapaModule
from xmodule.modulestore import Location
from lxml import etree
@@ -33,6 +35,14 @@ class CapaFactory(object):
CapaFactory.num += 1
return CapaFactory.num
+ @staticmethod
+ def input_key():
+ """ Return the input key to use when passing GET parameters """
+ return ("input_" +
+ "-".join(['i4x', 'edX', 'capa_test', 'problem',
+ 'SampleProblem%d' % CapaFactory.num]) +
+ "_2_1")
+
@staticmethod
def create(graceperiod=None,
due=None,
@@ -59,11 +69,10 @@ class CapaFactory(object):
module.
attempts: also added to instance state. Will be converted to an int.
- correct: if True, the problem will be initialized to be answered correctly.
"""
definition = {'data': CapaFactory.sample_problem_xml, }
location = Location(["i4x", "edX", "capa_test", "problem",
- "SampleProblem{0}".format(CapaFactory.next_num())])
+ "SampleProblem%d" % CapaFactory.next_num()])
metadata = {}
if graceperiod is not None:
metadata['graceperiod'] = graceperiod
@@ -89,13 +98,6 @@ class CapaFactory(object):
# since everything else is a string.
instance_state_dict['attempts'] = int(attempts)
- if correct:
- # TODO: make this actually set an answer of 3.14, and mark it correct
- #instance_state_dict['student_answers'] = {}
- #instance_state_dict['correct_map'] = {}
- pass
-
-
if len(instance_state_dict) > 0:
instance_state = json.dumps(instance_state_dict)
else:
@@ -332,3 +334,128 @@ class CapaModuleTest(unittest.TestCase):
'input_1': 'test 2' }
with self.assertRaises(ValueError):
result = CapaModule.make_dict_of_responses(invalid_get_dict)
+
+ def test_check_problem_correct(self):
+
+ module = CapaFactory.create(attempts=1)
+
+ # Simulate that all answers are marked correct, no matter
+ # what the input is, by patching CorrectMap.is_correct()
+ with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
+ mock_is_correct.return_value = True
+
+ # Check the problem
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.check_problem(get_request_dict)
+
+ # Expect that the problem is marked correct
+ self.assertEqual(result['success'], 'correct')
+
+ # Expect that the number of attempts is incremented by 1
+ self.assertEqual(module.attempts, 2)
+
+
+ def test_check_problem_incorrect(self):
+
+ module = CapaFactory.create(attempts=0)
+
+ # Simulate marking the input incorrect
+ with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
+ mock_is_correct.return_value = False
+
+ # Check the problem
+ get_request_dict = { CapaFactory.input_key(): '0' }
+ result = module.check_problem(get_request_dict)
+
+ # Expect that the problem is marked correct
+ self.assertEqual(result['success'], 'incorrect')
+
+ # Expect that the number of attempts is incremented by 1
+ self.assertEqual(module.attempts, 1)
+
+
+ def test_check_problem_closed(self):
+ module = CapaFactory.create(attempts=3)
+
+ # Problem closed -- cannot submit
+ # Simulate that CapaModule.closed() always returns True
+ with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
+ mock_closed.return_value = True
+ with self.assertRaises(xmodule.exceptions.NotFoundError):
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ module.check_problem(get_request_dict)
+
+ # Expect that number of attempts NOT incremented
+ self.assertEqual(module.attempts, 3)
+
+
+ def test_check_problem_resubmitted_with_randomize(self):
+ # Randomize turned on
+ module = CapaFactory.create(rerandomize='always', attempts=0)
+
+ # Simulate that the problem is completed
+ module.lcp.done = True
+
+ # Expect that we cannot submit
+ with self.assertRaises(xmodule.exceptions.NotFoundError):
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ module.check_problem(get_request_dict)
+
+ # Expect that number of attempts NOT incremented
+ self.assertEqual(module.attempts, 0)
+
+
+ def test_check_problem_resubmitted_no_randomize(self):
+ # Randomize turned off
+ module = CapaFactory.create(rerandomize='never', attempts=0)
+
+ # Simulate that the problem is completed
+ module.lcp.done = True
+
+ # Expect that we can submit successfully
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.check_problem(get_request_dict)
+
+ self.assertEqual(result['success'], 'correct')
+
+ # Expect that number of attempts IS incremented
+ self.assertEqual(module.attempts, 1)
+
+
+ def test_check_problem_queued(self):
+ module = CapaFactory.create(attempts=1)
+
+ # Simulate that the problem is queued
+ with patch('capa.capa_problem.LoncapaProblem.is_queued') \
+ as mock_is_queued,\
+ patch('capa.capa_problem.LoncapaProblem.get_recentmost_queuetime') \
+ as mock_get_queuetime:
+
+ mock_is_queued.return_value = True
+ mock_get_queuetime.return_value = datetime.datetime.now()
+
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.check_problem(get_request_dict)
+
+ # Expect an AJAX alert message in 'success'
+ self.assertTrue('You must wait' in result['success'])
+
+ # Expect that the number of attempts is NOT incremented
+ self.assertEqual(module.attempts, 1)
+
+
+ def test_check_problem_student_input_error(self):
+ module = CapaFactory.create(attempts=1)
+
+ # Simulate a student input exception
+ with patch('capa.capa_problem.LoncapaProblem.grade_answers') as mock_grade:
+ mock_grade.side_effect = capa.responsetypes.StudentInputError('test error')
+
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.check_problem(get_request_dict)
+
+ # Expect an AJAX alert message in 'success'
+ self.assertTrue('test error' in result['success'])
+
+ # Expect that the number of attempts is NOT incremented
+ self.assertEqual(module.attempts, 1)
From 9743f6bea9ce4cdcf859d5f0c8a735e3a287080c Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Mon, 4 Mar 2013 16:16:38 -0500
Subject: [PATCH 04/13] Fixed indentation in capa module. Added test of HTML
output for successful check to capa module unit tests
---
common/lib/xmodule/xmodule/capa_module.py | 6 +++---
common/lib/xmodule/xmodule/tests/test_capa_module.py | 8 +++++++-
2 files changed, 10 insertions(+), 4 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index d9ada0038e..cf1d786b72 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -649,11 +649,11 @@ class CapaModule(XModule):
# 'success' will always be incorrect
event_info['correct_map'] = correct_map.get_dict()
event_info['success'] = success
- event_info['attempts'] = self.attempts
+ event_info['attempts'] = self.attempts
self.system.track_function('save_problem_check', event_info)
- if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
- self.system.psychometrics_handler(self.get_instance_state())
+ if hasattr(self.system, 'psychometrics_handler'): # update PsychometricsData using callback
+ self.system.psychometrics_handler(self.get_instance_state())
# render problem into HTML
html = self.get_problem_html(encapsulate=False)
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index c569309af9..453365f73b 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -341,8 +341,11 @@ class CapaModuleTest(unittest.TestCase):
# Simulate that all answers are marked correct, no matter
# what the input is, by patching CorrectMap.is_correct()
- with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct:
+ # Also simulate rendering the HTML
+ with patch('capa.correctmap.CorrectMap.is_correct') as mock_is_correct,\
+ patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
mock_is_correct.return_value = True
+ mock_html.return_value = "Test HTML"
# Check the problem
get_request_dict = { CapaFactory.input_key(): '3.14' }
@@ -351,6 +354,9 @@ class CapaModuleTest(unittest.TestCase):
# Expect that the problem is marked correct
self.assertEqual(result['success'], 'correct')
+ # Expect that we get the (mocked) HTML
+ self.assertEqual(result['contents'], 'Test HTML')
+
# Expect that the number of attempts is incremented by 1
self.assertEqual(module.attempts, 2)
From 21af0493fdda90e389955a8c939290c5b2b175b5 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Tue, 5 Mar 2013 11:21:28 -0500
Subject: [PATCH 05/13] Added unit tests for capa_module reset_problem()
---
common/lib/xmodule/xmodule/capa_module.py | 10 +++-
.../xmodule/xmodule/tests/test_capa_module.py | 59 ++++++++++++++++++-
2 files changed, 66 insertions(+), 3 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index cf1d786b72..22f09b4591 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -703,7 +703,12 @@ class CapaModule(XModule):
''' Changes problem state to unfinished -- removes student answers,
and causes problem to rerender itself.
- Returns problem html as { 'html' : html-string }.
+ Returns a dictionary of the form:
+ {'success': True/False,
+ 'html': Problem HTML string }
+
+ If an error occurs, the dictionary will also have an
+ 'error' key containing an error message.
'''
event_info = dict()
event_info['old_state'] = self.lcp.get_state()
@@ -734,7 +739,8 @@ class CapaModule(XModule):
event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info)
- return {'html': self.get_problem_html(encapsulate=False)}
+ return { 'success': True,
+ 'html': self.get_problem_html(encapsulate=False)}
class CapaDescriptor(RawDescriptor):
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 453365f73b..afe4a73725 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -1,6 +1,6 @@
import datetime
import json
-from mock import Mock, patch
+from mock import Mock, MagicMock, patch
from pprint import pprint
import unittest
@@ -465,3 +465,60 @@ class CapaModuleTest(unittest.TestCase):
# Expect that the number of attempts is NOT incremented
self.assertEqual(module.attempts, 1)
+
+
+ def test_reset_problem(self):
+ module = CapaFactory.create()
+
+ # Mock the module's capa problem
+ # to simulate that the problem is done
+ mock_problem = MagicMock(capa.capa_problem.LoncapaProblem)
+ mock_problem.done = True
+ module.lcp = mock_problem
+
+ # Stub out HTML rendering
+ with patch('xmodule.capa_module.CapaModule.get_problem_html') as mock_html:
+ mock_html.return_value = "Test HTML
"
+
+ # Reset the problem
+ get_request_dict = {}
+ result = module.reset_problem(get_request_dict)
+
+ # Expect that the request was successful
+ self.assertTrue('success' in result and result['success'])
+
+ # Expect that the problem HTML is retrieved
+ self.assertTrue('html' in result)
+ self.assertEqual(result['html'], "Test HTML
")
+
+ # Expect that the problem was reset
+ mock_problem.do_reset.assert_called_once_with()
+
+
+ def test_reset_problem_closed(self):
+ module = CapaFactory.create()
+
+ # Simulate that the problem is closed
+ with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
+ mock_closed.return_value = True
+
+ # Try to reset the problem
+ get_request_dict = {}
+ result = module.reset_problem(get_request_dict)
+
+ # Expect that the problem was NOT reset
+ self.assertTrue('success' in result and not result['success'])
+
+
+ def test_reset_problem_not_done(self):
+ module = CapaFactory.create()
+
+ # Simulate that the problem is NOT done
+ module.lcp.done = False
+
+ # Try to reset the problem
+ get_request_dict = {}
+ result = module.reset_problem(get_request_dict)
+
+ # Expect that the problem was NOT reset
+ self.assertTrue('success' in result and not result['success'])
From 49784009d538a686e8b47455b5e4c83d7b95ae9e Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Tue, 5 Mar 2013 11:23:14 -0500
Subject: [PATCH 06/13] Fixed incorrect doc string
---
common/lib/xmodule/xmodule/capa_module.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 22f09b4591..8e2f8825de 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -590,7 +590,7 @@ class CapaModule(XModule):
''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers:
- {'success' : bool,
+ {'success' : 'correct' | 'incorrect' | AJAX alert msg string,
'contents' : html}
'''
event_info = dict()
From 6ec3b94d0c0df050257fffc9deab4783206777ef Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Tue, 5 Mar 2013 11:47:41 -0500
Subject: [PATCH 07/13] Added test for capa module closed() method; modified
closed() to handle edge cases
---
common/lib/xmodule/xmodule/capa_module.py | 2 +-
.../xmodule/xmodule/tests/test_capa_module.py | 46 +++++++++++++++++++
2 files changed, 47 insertions(+), 1 deletion(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 8e2f8825de..5e0e2e9ac4 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -419,7 +419,7 @@ class CapaModule(XModule):
def closed(self):
''' Is the student still allowed to submit answers? '''
- if self.attempts == self.max_attempts:
+ if self.max_attempts is not None and self.attempts >= self.max_attempts:
return True
if self.is_past_due():
return True
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index afe4a73725..803b91d139 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -286,6 +286,34 @@ class CapaModuleTest(unittest.TestCase):
self.assertTrue(still_in_grace.answer_available())
+ def test_closed(self):
+
+ # Attempts < Max attempts --> NOT closed
+ module = CapaFactory.create(max_attempts="1", attempts="0")
+ self.assertFalse(module.closed())
+
+ # Attempts < Max attempts --> NOT closed
+ module = CapaFactory.create(max_attempts="2", attempts="1")
+ self.assertFalse(module.closed())
+
+ # Attempts = Max attempts --> closed
+ module = CapaFactory.create(max_attempts="1", attempts="1")
+ self.assertTrue(module.closed())
+
+ # Attempts > Max attempts --> closed
+ module = CapaFactory.create(max_attempts="1", attempts="2")
+ self.assertTrue(module.closed())
+
+ # Max attempts = 0 --> closed
+ module = CapaFactory.create(max_attempts="0", attempts="2")
+ self.assertTrue(module.closed())
+
+ # Past due --> closed
+ module = CapaFactory.create(max_attempts="1", attempts="0",
+ due=self.yesterday_str)
+ self.assertTrue(module.closed())
+
+
def test_parse_get_params(self):
# Valid GET param dict
@@ -522,3 +550,21 @@ class CapaModuleTest(unittest.TestCase):
# Expect that the problem was NOT reset
self.assertTrue('success' in result and not result['success'])
+
+
+ def test_save_problem(self):
+ module = CapaFactory.create()
+
+ # Simulate
+
+
+ def test_save_problem_closed(self):
+ self.fail()
+
+
+ def test_save_problem_submitted_with_randomize(self):
+ self.fail()
+
+
+ def test_save_problem_submitted_no_randomize(self):
+ self.fail()
From 42043d2ccc4d0600b1610d4e3a195d7b6ba7e145 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Tue, 5 Mar 2013 12:01:04 -0500
Subject: [PATCH 08/13] Added tests for capa module save_problem()
---
.../xmodule/xmodule/tests/test_capa_module.py | 62 +++++++++++++++++--
1 file changed, 56 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 803b91d139..2469431217 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -38,8 +38,12 @@ class CapaFactory(object):
@staticmethod
def input_key():
""" Return the input key to use when passing GET parameters """
- return ("input_" +
- "-".join(['i4x', 'edX', 'capa_test', 'problem',
+ return ("input_" + CapaFactory.answer_key())
+
+ @staticmethod
+ def answer_key():
+ """ Return the key stored in the capa problem answer dict """
+ return ("-".join(['i4x', 'edX', 'capa_test', 'problem',
'SampleProblem%d' % CapaFactory.num]) +
"_2_1")
@@ -555,16 +559,62 @@ class CapaModuleTest(unittest.TestCase):
def test_save_problem(self):
module = CapaFactory.create()
- # Simulate
+ # Simulate that the problem is not done (not attempted or reset)
+ module.lcp.done = False
+
+ # Save the problem
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.save_problem(get_request_dict)
+
+ # Expect that answers are saved to the problem
+ expected_answers = { CapaFactory.answer_key(): '3.14' }
+ self.assertEqual(module.lcp.student_answers, expected_answers)
+
+ # Expect that the result is success
+ self.assertTrue('success' in result and result['success'])
def test_save_problem_closed(self):
- self.fail()
+ module = CapaFactory.create()
+
+ # Simulate that the problem is NOT done (not attempted or reset)
+ module.lcp.done = False
+
+ # Simulate that the problem is closed
+ with patch('xmodule.capa_module.CapaModule.closed') as mock_closed:
+ mock_closed.return_value = True
+
+ # Try to save the problem
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.save_problem(get_request_dict)
+
+ # Expect that the result is failure
+ self.assertTrue('success' in result and not result['success'])
def test_save_problem_submitted_with_randomize(self):
- self.fail()
+ module = CapaFactory.create(rerandomize='always')
+
+ # Simulate that the problem is completed
+ module.lcp.done = True
+
+ # Try to save
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.save_problem(get_request_dict)
+
+ # Expect that we cannot save
+ self.assertTrue('success' in result and not result['success'])
def test_save_problem_submitted_no_randomize(self):
- self.fail()
+ module = CapaFactory.create(rerandomize='never')
+
+ # Simulate that the problem is completed
+ module.lcp.done = True
+
+ # Try to save
+ get_request_dict = { CapaFactory.input_key(): '3.14' }
+ result = module.save_problem(get_request_dict)
+
+ # Expect that we succeed
+ self.assertTrue('success' in result and result['success'])
From 608552429f5c5f456e3bd104cc43a6943c80061b Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Tue, 5 Mar 2013 15:35:14 -0500
Subject: [PATCH 09/13] Refactored capa module HTML rendering, isolating logic
for determining which buttons to show
---
common/lib/xmodule/xmodule/capa_module.py | 130 ++++++++------
.../xmodule/xmodule/tests/test_capa_module.py | 162 ++++++++++++++++++
2 files changed, 242 insertions(+), 50 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 5e0e2e9ac4..19bddd1a19 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -136,7 +136,7 @@ class CapaModule(XModule):
self.close_date = self.display_due_date
max_attempts = self.metadata.get('attempts', None)
- if max_attempts:
+ if max_attempts is not None:
self.max_attempts = int(max_attempts)
else:
self.max_attempts = None
@@ -247,6 +247,78 @@ class CapaModule(XModule):
'progress': Progress.to_js_status_str(self.get_progress())
})
+ def check_button_name(self):
+ """
+ Determine the name for the "check" button.
+ Usually it is just "Check", but if this is the student's
+ final attempt, change the name to "Final Check"
+ """
+ if self.max_attempts is not None:
+ final_check = (self.attempts >= self.max_attempts - 1)
+ else:
+ final_check = False
+
+ return "Final Check" if final_check else "Check"
+
+ def should_show_check_button(self):
+ """
+ Return True/False to indicate whether to show the "Check" button.
+ """
+ submitted_without_reset = (self.is_completed() and self.rerandomize == "always")
+
+ # If the problem is closed (past due / too many attempts)
+ # then we do NOT show the "check" button
+ # Also, do not show the "check" button if we're waiting
+ # for the user to reset a randomized problem
+ if self.closed() or submitted_without_reset:
+ return False
+ else:
+ return True
+
+ def should_show_reset_button(self):
+ """
+ Return True/False to indicate whether to show the "Reset" button.
+ """
+ survey_question = (self.max_attempts == 0)
+
+ if self.rerandomize in ["always", "onreset"]:
+
+ # If the problem is closed (and not a survey question with max_attempts==0),
+ # then do NOT show the reset button.
+ # If the problem hasn't been submitted yet, then do NOT show
+ # the reset button.
+ if (self.closed() and not survey_question) or not self.is_completed():
+ return False
+ else:
+ return True
+
+ # Only randomized problems need a "reset" button
+ else:
+ return False
+
+ def should_show_save_button(self):
+ """
+ Return True/False to indicate whether to show the "Save" button.
+ """
+
+ # If the user has forced the save button to display,
+ # then show it as long as the problem is not closed
+ # (past due / too many attempts)
+ if self.force_save_button == "true":
+ return not self.closed()
+ else:
+ survey_question = (self.max_attempts == 0)
+ needs_reset = self.is_completed() and self.rerandomize == "always"
+
+ # If the problem is closed (and not a survey question with max_attempts==0),
+ # then do NOT show the reset button
+ # If we're waiting for the user to reset a randomized problem
+ # then do NOT show the reset button
+ if (self.closed() and not survey_question) or needs_reset:
+ return False
+ else:
+ return True
+
def get_problem_html(self, encapsulate=True):
'''Return html for the problem. Adds check, reset, save buttons
as necessary based on the problem config and state.'''
@@ -313,57 +385,14 @@ class CapaModule(XModule):
'weight': self.descriptor.weight,
}
- # We using strings as truthy values, because the terminology of the
- # check button is context-specific.
-
- # Put a "Check" button if unlimited attempts or still some left
- if self.max_attempts is None or self.attempts < self.max_attempts - 1:
- check_button = "Check"
- else:
- # Will be final check so let user know that
- check_button = "Final Check"
-
- reset_button = True
- save_button = True
-
- # If we're after deadline, or user has exhausted attempts,
- # question is read-only.
- if self.closed():
- check_button = False
- reset_button = False
- save_button = False
-
- # If attempts=0 then show just check and reset buttons; this is for survey questions using capa
- if self.max_attempts==0:
- check_button = False
- reset_button = True
- save_button = True
-
- # User submitted a problem, and hasn't reset. We don't want
- # more submissions.
- if self.lcp.done and self.rerandomize == "always":
- check_button = False
- save_button = False
-
- # Only show the reset button if pressing it will show different values
- if self.rerandomize not in ["always", "onreset"]:
- reset_button = False
-
- # User hasn't submitted an answer yet -- we don't want resets
- if not self.lcp.done:
- reset_button = False
-
- # We may not need a "save" button if infinite number of attempts and
- # non-randomized. The problem author can force it. It's a bit weird for
- # randomization to control this; should perhaps be cleaned up.
- if (self.force_save_button == "false") and (self.max_attempts is None and self.rerandomize != "always"):
- save_button = False
-
context = {'problem': content,
'id': self.id,
- 'check_button': check_button,
- 'reset_button': reset_button,
- 'save_button': save_button,
+
+ # Pass in the name of the check button or False
+ # if we do not need a check button
+ 'check_button': self.check_button_name() if self.should_show_check_button() else False,
+ 'reset_button': self.should_show_reset_button(),
+ 'save_button': self.should_show_save_button(),
'answer_available': self.answer_available(),
'ajax_url': self.system.ajax_url,
'attempts_used': self.attempts,
@@ -731,6 +760,7 @@ class CapaModule(XModule):
# reset random number generator seed (note the self.lcp.get_state()
# in next line)
self.lcp.seed = None
+
self.lcp = LoncapaProblem(self.definition['data'],
self.location.html_id(), self.lcp.get_state(),
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 2469431217..1ac9c2e644 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -3,6 +3,7 @@ import json
from mock import Mock, MagicMock, patch
from pprint import pprint
import unittest
+import random
import xmodule
import capa
@@ -618,3 +619,164 @@ class CapaModuleTest(unittest.TestCase):
# Expect that we succeed
self.assertTrue('success' in result and result['success'])
+
+ def test_check_button_name(self):
+
+ # If last attempt, button name changes to "Final Check"
+ # Just in case, we also check what happens if we have
+ # more attempts than allowed.
+ attempts = random.randint(1, 10)
+ module = CapaFactory.create(attempts=attempts-1, max_attempts=attempts)
+ self.assertEqual(module.check_button_name(), "Final Check")
+
+ module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
+ self.assertEqual(module.check_button_name(), "Final Check")
+
+ module = CapaFactory.create(attempts=attempts + 1, max_attempts=attempts)
+ self.assertEqual(module.check_button_name(), "Final Check")
+
+ # Otherwise, button name is "Check"
+ module = CapaFactory.create(attempts=attempts-2, max_attempts=attempts)
+ self.assertEqual(module.check_button_name(), "Check")
+
+ module = CapaFactory.create(attempts=attempts-3, max_attempts=attempts)
+ self.assertEqual(module.check_button_name(), "Check")
+
+ # If no limit on attempts, then always show "Check"
+ module = CapaFactory.create(attempts=attempts-3)
+ self.assertEqual(module.check_button_name(), "Check")
+
+ module = CapaFactory.create(attempts=0)
+ self.assertEqual(module.check_button_name(), "Check")
+
+ def test_should_show_check_button(self):
+
+ attempts = random.randint(1,10)
+
+ # If we're after the deadline, do NOT show check button
+ module = CapaFactory.create(due=self.yesterday_str)
+ self.assertFalse(module.should_show_check_button())
+
+ # If user is out of attempts, do NOT show the check button
+ module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
+ self.assertFalse(module.should_show_check_button())
+
+ # If survey question (max_attempts = 0), do NOT show the check button
+ module = CapaFactory.create(max_attempts=0)
+ self.assertFalse(module.should_show_check_button())
+
+ # If user submitted a problem but hasn't reset,
+ # do NOT show the check button
+ # Note: we can only reset when rerandomize="always"
+ module = CapaFactory.create(rerandomize="always")
+ module.lcp.done = True
+ self.assertFalse(module.should_show_check_button())
+
+ # Otherwise, DO show the check button
+ module = CapaFactory.create()
+ self.assertTrue(module.should_show_check_button())
+
+ # If the user has submitted the problem
+ # and we do NOT have a reset button, then we can show the check button
+ # Setting rerandomize to "never" ensures that the reset button
+ # is not shown
+ module = CapaFactory.create(rerandomize="never")
+ module.lcp.done = True
+ self.assertTrue(module.should_show_check_button())
+
+
+ def test_should_show_reset_button(self):
+
+ attempts = random.randint(1,10)
+
+ # If we're after the deadline, do NOT show the reset button
+ module = CapaFactory.create(due=self.yesterday_str)
+ module.lcp.done = True
+ self.assertFalse(module.should_show_reset_button())
+
+ # If the user is out of attempts, do NOT show the reset button
+ module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
+ module.lcp.done = True
+ self.assertFalse(module.should_show_reset_button())
+
+ # If we're NOT randomizing, then do NOT show the reset button
+ module = CapaFactory.create(rerandomize="never")
+ module.lcp.done = True
+ self.assertFalse(module.should_show_reset_button())
+
+ # If the user hasn't submitted an answer yet,
+ # then do NOT show the reset button
+ module = CapaFactory.create()
+ module.lcp.done = False
+ self.assertFalse(module.should_show_reset_button())
+
+ # Otherwise, DO show the reset button
+ module = CapaFactory.create()
+ module.lcp.done = True
+ self.assertTrue(module.should_show_reset_button())
+
+ # If survey question for capa (max_attempts = 0),
+ # DO show the reset button
+ module = CapaFactory.create(max_attempts=0)
+ module.lcp.done = True
+ self.assertTrue(module.should_show_reset_button())
+
+
+ def test_should_show_save_button(self):
+
+ attempts = random.randint(1,10)
+
+ # If we're after the deadline, do NOT show the save button
+ module = CapaFactory.create(due=self.yesterday_str)
+ module.lcp.done = True
+ self.assertFalse(module.should_show_save_button())
+
+ # If the user is out of attempts, do NOT show the save button
+ module = CapaFactory.create(attempts=attempts, max_attempts=attempts)
+ module.lcp.done = True
+ self.assertFalse(module.should_show_save_button())
+
+ # If user submitted a problem but hasn't reset, do NOT show the save button
+ module = CapaFactory.create(rerandomize="always")
+ module.lcp.done = True
+ self.assertFalse(module.should_show_save_button())
+
+ # Otherwise, DO show the save button
+ module = CapaFactory.create()
+ module.lcp.done = False
+ self.assertTrue(module.should_show_save_button())
+
+ # If we're not randomizing, then we can re-save
+ module = CapaFactory.create(rerandomize="never")
+ module.lcp.done = True
+ self.assertTrue(module.should_show_save_button())
+
+ # If survey question for capa (max_attempts = 0),
+ # DO show the save button
+ module = CapaFactory.create(max_attempts=0)
+ module.lcp.done = False
+ self.assertTrue(module.should_show_save_button())
+
+ def test_should_show_save_button_force_save_button(self):
+ # If we're after the deadline, do NOT show the save button
+ # even though we're forcing a save
+ module = CapaFactory.create(due=self.yesterday_str,
+ force_save_button="true")
+ module.lcp.done = True
+ self.assertFalse(module.should_show_save_button())
+
+ # If the user is out of attempts, do NOT show the save button
+ attempts = random.randint(1,10)
+ module = CapaFactory.create(attempts=attempts,
+ max_attempts=attempts,
+ force_save_button="true")
+ module.lcp.done = True
+ self.assertFalse(module.should_show_save_button())
+
+ # Otherwise, if we force the save button,
+ # then show it even if we would ordinarily
+ # require a reset first
+ module = CapaFactory.create(force_save_button="true",
+ rerandomize="always")
+ module.lcp.done = True
+ self.assertTrue(module.should_show_save_button())
From 40d7e8addf0bbd3b12e14c853ad0fa0ae818fa25 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Tue, 5 Mar 2013 15:43:47 -0500
Subject: [PATCH 10/13] Moved problem HTML error handling to its own function
---
common/lib/xmodule/xmodule/capa_module.py | 122 ++++++++++++----------
1 file changed, 69 insertions(+), 53 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index 19bddd1a19..e6d26bf88e 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -319,66 +319,82 @@ class CapaModule(XModule):
else:
return True
+ def handle_problem_html_error(self, err):
+ """
+ Change our problem to a dummy problem containing
+ a warning message to display to users.
+
+ Returns the HTML to show to users
+
+ *err* is the Exception encountered while rendering the problem HTML.
+ """
+ log.exception(err)
+
+ # TODO (vshnayder): another switch on DEBUG.
+ if self.system.DEBUG:
+ msg = (
+ '[courseware.capa.capa_module] '
+ 'Failed to generate HTML for problem %s' %
+ (self.location.url()))
+ msg += 'Error:
%s
' % str(err).replace('<', '<')
+ msg += '%s
' % traceback.format_exc().replace('<', '<')
+ html = msg
+
+ # We're in non-debug mode, and possibly even in production. We want
+ # to avoid bricking of problem as much as possible
+ else:
+
+ # Presumably, student submission has corrupted LoncapaProblem HTML.
+ # First, pull down all student answers
+ student_answers = self.lcp.student_answers
+ answer_ids = student_answers.keys()
+
+ # Some inputtypes, such as dynamath, have additional "hidden" state that
+ # is not exposed to the student. Keep those hidden
+ # TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
+ hidden_state_keywords = ['dynamath']
+ for answer_id in answer_ids:
+ for hidden_state_keyword in hidden_state_keywords:
+ if answer_id.find(hidden_state_keyword) >= 0:
+ student_answers.pop(answer_id)
+
+ # Next, generate a fresh LoncapaProblem
+ self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
+ state=None, # Tabula rasa
+ seed=self.seed, system=self.system)
+
+ # Prepend a scary warning to the student
+ warning = ''\
+ '
Warning: The problem has been reset to its initial state!
'\
+ 'The problem\'s state was corrupted by an invalid submission. ' \
+ 'The submission consisted of:'\
+ '
'
+ for student_answer in student_answers.values():
+ if student_answer != '':
+ warning += '- ' + cgi.escape(student_answer) + '
'
+ warning += '
'\
+ 'If this error persists, please contact the course staff.'\
+ '
'
+
+ html = warning
+ try:
+ html += self.lcp.get_html()
+ except Exception, err: # Couldn't do it. Give up
+ log.exception(err)
+ raise
+
+ return html
+
+
def get_problem_html(self, encapsulate=True):
'''Return html for the problem. Adds check, reset, save buttons
as necessary based on the problem config and state.'''
try:
html = self.lcp.get_html()
+
except Exception, err:
- log.exception(err)
-
- # TODO (vshnayder): another switch on DEBUG.
- if self.system.DEBUG:
- msg = (
- '[courseware.capa.capa_module] '
- 'Failed to generate HTML for problem %s' %
- (self.location.url()))
- msg += 'Error:
%s
' % str(err).replace('<', '<')
- msg += '%s
' % traceback.format_exc().replace('<', '<')
- html = msg
- else:
- # We're in non-debug mode, and possibly even in production. We want
- # to avoid bricking of problem as much as possible
-
- # Presumably, student submission has corrupted LoncapaProblem HTML.
- # First, pull down all student answers
- student_answers = self.lcp.student_answers
- answer_ids = student_answers.keys()
-
- # Some inputtypes, such as dynamath, have additional "hidden" state that
- # is not exposed to the student. Keep those hidden
- # TODO: Use regex, e.g. 'dynamath' is suffix at end of answer_id
- hidden_state_keywords = ['dynamath']
- for answer_id in answer_ids:
- for hidden_state_keyword in hidden_state_keywords:
- if answer_id.find(hidden_state_keyword) >= 0:
- student_answers.pop(answer_id)
-
- # Next, generate a fresh LoncapaProblem
- self.lcp = LoncapaProblem(self.definition['data'], self.location.html_id(),
- state=None, # Tabula rasa
- seed=self.seed, system=self.system)
-
- # Prepend a scary warning to the student
- warning = ''\
- '
Warning: The problem has been reset to its initial state!
'\
- 'The problem\'s state was corrupted by an invalid submission. ' \
- 'The submission consisted of:'\
- '
'
- for student_answer in student_answers.values():
- if student_answer != '':
- warning += '- ' + cgi.escape(student_answer) + '
'
- warning += '
'\
- 'If this error persists, please contact the course staff.'\
- '
'
-
- html = warning
- try:
- html += self.lcp.get_html()
- except Exception, err: # Couldn't do it. Give up
- log.exception(err)
- raise
+ return self.handle_problem_html_error(err)
content = {'name': self.display_name,
'html': html,
From 73281a43c97406715d7bf4b51d6cc5c452aa7bb2 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Tue, 5 Mar 2013 17:02:12 -0500
Subject: [PATCH 11/13] Added tests for get_problem_html, including error
conditions
---
common/lib/xmodule/xmodule/capa_module.py | 18 +++-
common/lib/xmodule/xmodule/tests/__init__.py | 2 +-
.../xmodule/xmodule/tests/test_capa_module.py | 92 +++++++++++++++++++
3 files changed, 106 insertions(+), 6 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index e6d26bf88e..b42b7e4ccc 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -393,8 +393,19 @@ class CapaModule(XModule):
try:
html = self.lcp.get_html()
+ # If we cannot construct the problem HTML,
+ # then generate an error message instead.
except Exception, err:
- return self.handle_problem_html_error(err)
+ html = self.handle_problem_html_error(err)
+
+
+ # The convention is to pass the name of the check button
+ # if we want to show a check button, and False otherwise
+ # This works because non-empty strings evaluate to True
+ if self.should_show_check_button():
+ check_button = self.check_button_name()
+ else:
+ check_button = False
content = {'name': self.display_name,
'html': html,
@@ -403,10 +414,7 @@ class CapaModule(XModule):
context = {'problem': content,
'id': self.id,
-
- # Pass in the name of the check button or False
- # if we do not need a check button
- 'check_button': self.check_button_name() if self.should_show_check_button() else False,
+ 'check_button': check_button,
'reset_button': self.should_show_reset_button(),
'save_button': self.should_show_save_button(),
'answer_available': self.answer_available(),
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index 9474717cb2..220f122e7a 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -34,7 +34,7 @@ test_system = ModuleSystem(
get_module=Mock(),
# "render" to just the context...
render_template=lambda template, context: str(context),
- replace_urls=Mock(),
+ replace_urls=lambda html: str(html),
user=Mock(is_staff=False),
filestore=Mock(),
debug=True,
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index 1ac9c2e644..a42143da82 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -780,3 +780,95 @@ class CapaModuleTest(unittest.TestCase):
rerandomize="always")
module.lcp.done = True
self.assertTrue(module.should_show_save_button())
+
+ def test_get_problem_html(self):
+ module = CapaFactory.create()
+
+ # We've tested the show/hide button logic in other tests,
+ # so here we hard-wire the values
+ show_check_button = bool(random.randint(0,1) % 2)
+ show_reset_button = bool(random.randint(0,1) % 2)
+ show_save_button = bool(random.randint(0,1) % 2)
+
+ module.should_show_check_button = Mock(return_value=show_check_button)
+ module.should_show_reset_button = Mock(return_value=show_reset_button)
+ module.should_show_save_button = Mock(return_value=show_save_button)
+
+ # Mock the system rendering function (reset when we're done)
+ old_render_func = test_system.render_template
+ test_system.render_template = Mock(return_value="Test Template HTML
")
+
+ def cleanup_func():
+ test_system.render_template = old_render_func
+
+ self.addCleanup(cleanup_func)
+
+ # Patch the capa problem's HTML rendering
+ with patch('capa.capa_problem.LoncapaProblem.get_html') as mock_html:
+ mock_html.return_value = "Test Problem HTML
"
+
+ # Render the problem HTML
+ html = module.get_problem_html(encapsulate=False)
+
+ # Also render the problem encapsulated in a
+ html_encapsulated = module.get_problem_html(encapsulate=True)
+
+ # Expect that we get the rendered template back
+ self.assertEqual(html, "
Test Template HTML
")
+
+ # Check the rendering context
+ render_args,_ = test_system.render_template.call_args
+ self.assertEqual(len(render_args), 2)
+
+ template_name = render_args[0]
+ self.assertEqual(template_name, "problem.html")
+
+ context = render_args[1]
+ self.assertEqual(context['problem']['html'], "
Test Problem HTML
")
+ self.assertEqual(bool(context['check_button']), show_check_button)
+ self.assertEqual(bool(context['reset_button']), show_reset_button)
+ self.assertEqual(bool(context['save_button']), show_save_button)
+
+ # Assert that the encapsulated html contains the original html
+ self.assertTrue(html in html_encapsulated)
+
+
+ def test_get_problem_html_error(self):
+ """
+ In production, when an error occurs with the problem HTML
+ rendering, a "dummy" problem is created with an error
+ message to display to the user.
+ """
+ module = CapaFactory.create()
+
+ # Save the original problem so we can compare it later
+ original_problem = module.lcp
+
+ # Simulate throwing an exception when the capa problem
+ # is asked to render itself as HTML
+ module.lcp.get_html = Mock(side_effect=Exception("Test"))
+
+ # Stub out the test_system rendering function temporarily
+ old_render_func = test_system.render_template
+ test_system.render_template = Mock(return_value="
Test Template HTML
")
+
+ # Turn off DEBUG temporarily
+ old_debug = test_system.DEBUG
+ test_system.DEBUG = False
+
+ def cleanup_func():
+ test_system.render_template = old_render_func
+ test_system.DEBUG = old_debug
+
+ self.addCleanup(cleanup_func)
+
+ # Try to render the module with DEBUG turned off
+ html = module.get_problem_html()
+
+ # Check the rendering context
+ render_args,_ = test_system.render_template.call_args
+ context = render_args[1]
+ self.assertTrue("error" in context['problem']['html'])
+
+ # Expect that the module has created a new dummy problem with the error
+ self.assertNotEqual(original_problem, module.lcp)
From 43d8574e920568f35f6847da9173a3ed4ec615d1 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Tue, 5 Mar 2013 17:26:15 -0500
Subject: [PATCH 12/13] Refactored use of test_system in xmodule unit tests so
that a new one is created for each test
---
common/lib/xmodule/xmodule/tests/__init__.py | 44 ++++++++++++-------
.../xmodule/xmodule/tests/test_capa_module.py | 32 ++++----------
.../xmodule/tests/test_combined_open_ended.py | 39 +++++++++-------
.../xmodule/xmodule/tests/test_conditional.py | 9 ++--
.../xmodule/tests/test_self_assessment.py | 14 +++---
5 files changed, 76 insertions(+), 62 deletions(-)
diff --git a/common/lib/xmodule/xmodule/tests/__init__.py b/common/lib/xmodule/xmodule/tests/__init__.py
index 220f122e7a..43c2bbe24d 100644
--- a/common/lib/xmodule/xmodule/tests/__init__.py
+++ b/common/lib/xmodule/xmodule/tests/__init__.py
@@ -28,21 +28,35 @@ open_ended_grading_interface = {
'grading_controller' : 'grading_controller'
}
-test_system = ModuleSystem(
- ajax_url='courses/course_id/modx/a_location',
- track_function=Mock(),
- get_module=Mock(),
- # "render" to just the context...
- render_template=lambda template, context: str(context),
- replace_urls=lambda html: str(html),
- user=Mock(is_staff=False),
- filestore=Mock(),
- debug=True,
- xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
- node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
- anonymous_student_id='student',
- open_ended_grading_interface= open_ended_grading_interface
-)
+
+def test_system():
+ """
+ Construct a test ModuleSystem instance.
+
+ By default, the render_template() method simply returns
+ the context it is passed as a string.
+ You can override this behavior by monkey patching:
+
+ system = test_system()
+ system.render_template = my_render_func
+
+ where my_render_func is a function of the form
+ my_render_func(template, context)
+ """
+ return ModuleSystem(
+ ajax_url='courses/course_id/modx/a_location',
+ track_function=Mock(),
+ get_module=Mock(),
+ render_template=lambda template, context: str(context),
+ replace_urls=lambda html: str(html),
+ user=Mock(is_staff=False),
+ filestore=Mock(),
+ debug=True,
+ xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
+ node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
+ anonymous_student_id='student',
+ open_ended_grading_interface= open_ended_grading_interface
+ )
class ModelsTest(unittest.TestCase):
diff --git a/common/lib/xmodule/xmodule/tests/test_capa_module.py b/common/lib/xmodule/xmodule/tests/test_capa_module.py
index a42143da82..0e64e740fd 100644
--- a/common/lib/xmodule/xmodule/tests/test_capa_module.py
+++ b/common/lib/xmodule/xmodule/tests/test_capa_module.py
@@ -108,7 +108,7 @@ class CapaFactory(object):
else:
instance_state = None
- module = CapaModule(test_system, location,
+ module = CapaModule(test_system(), location,
definition, descriptor,
instance_state, None, metadata=metadata)
@@ -794,14 +794,8 @@ class CapaModuleTest(unittest.TestCase):
module.should_show_reset_button = Mock(return_value=show_reset_button)
module.should_show_save_button = Mock(return_value=show_save_button)
- # Mock the system rendering function (reset when we're done)
- old_render_func = test_system.render_template
- test_system.render_template = Mock(return_value="Test Template HTML
")
-
- def cleanup_func():
- test_system.render_template = old_render_func
-
- self.addCleanup(cleanup_func)
+ # Mock the system rendering function
+ module.system.render_template = Mock(return_value="Test Template HTML
")
# Patch the capa problem's HTML rendering
with patch('capa.capa_problem.LoncapaProblem.get_html') as mock_html:
@@ -817,7 +811,7 @@ class CapaModuleTest(unittest.TestCase):
self.assertEqual(html, "Test Template HTML
")
# Check the rendering context
- render_args,_ = test_system.render_template.call_args
+ render_args,_ = module.system.render_template.call_args
self.assertEqual(len(render_args), 2)
template_name = render_args[0]
@@ -848,25 +842,17 @@ class CapaModuleTest(unittest.TestCase):
# is asked to render itself as HTML
module.lcp.get_html = Mock(side_effect=Exception("Test"))
- # Stub out the test_system rendering function temporarily
- old_render_func = test_system.render_template
- test_system.render_template = Mock(return_value="Test Template HTML
")
+ # Stub out the test_system rendering function
+ module.system.render_template = Mock(return_value="Test Template HTML
")
- # Turn off DEBUG temporarily
- old_debug = test_system.DEBUG
- test_system.DEBUG = False
-
- def cleanup_func():
- test_system.render_template = old_render_func
- test_system.DEBUG = old_debug
-
- self.addCleanup(cleanup_func)
+ # Turn off DEBUG
+ module.system.DEBUG = False
# Try to render the module with DEBUG turned off
html = module.get_problem_html()
# Check the rendering context
- render_args,_ = test_system.render_template.call_args
+ render_args,_ = module.system.render_template.call_args
context = render_args[1]
self.assertTrue("error" in context['problem']['html'])
diff --git a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
index 5f6496f823..a524ac2fd9 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -54,7 +54,8 @@ class OpenEndedChildTest(unittest.TestCase):
descriptor = Mock()
def setUp(self):
- self.openendedchild = OpenEndedChild(test_system, self.location,
+ self.test_system = test_system()
+ self.openendedchild = OpenEndedChild(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
@@ -69,7 +70,7 @@ class OpenEndedChildTest(unittest.TestCase):
def test_latest_post_assessment_empty(self):
- answer = self.openendedchild.latest_post_assessment(test_system)
+ answer = self.openendedchild.latest_post_assessment(self.test_system)
self.assertEqual(answer, "")
@@ -106,7 +107,7 @@ class OpenEndedChildTest(unittest.TestCase):
post_assessment = "Post assessment"
self.openendedchild.record_latest_post_assessment(post_assessment)
self.assertEqual(post_assessment,
- self.openendedchild.latest_post_assessment(test_system))
+ self.openendedchild.latest_post_assessment(self.test_system))
def test_get_score(self):
new_answer = "New Answer"
@@ -125,7 +126,7 @@ class OpenEndedChildTest(unittest.TestCase):
def test_reset(self):
- self.openendedchild.reset(test_system)
+ self.openendedchild.reset(self.test_system)
state = json.loads(self.openendedchild.get_instance_state())
self.assertEqual(state['state'], OpenEndedChild.INITIAL)
@@ -182,11 +183,13 @@ class OpenEndedModuleTest(unittest.TestCase):
descriptor = Mock()
def setUp(self):
- test_system.location = self.location
+ self.test_system = test_system()
+
+ self.test_system.location = self.location
self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
- test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
- self.openendedmodule = OpenEndedModule(test_system, self.location,
+ self.test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
+ self.openendedmodule = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
def test_message_post(self):
@@ -195,7 +198,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'grader_id': '1',
'score': 3}
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
- student_info = {'anonymous_student_id': test_system.anonymous_student_id,
+ student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
'submission_time': qtime}
contents = {
'feedback': get['feedback'],
@@ -205,7 +208,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'student_info': json.dumps(student_info)
}
- result = self.openendedmodule.message_post(get, test_system)
+ result = self.openendedmodule.message_post(get, self.test_system)
self.assertTrue(result['success'])
# make sure it's actually sending something we want to the queue
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
@@ -216,7 +219,7 @@ class OpenEndedModuleTest(unittest.TestCase):
def test_send_to_grader(self):
submission = "This is a student submission"
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat)
- student_info = {'anonymous_student_id': test_system.anonymous_student_id,
+ student_info = {'anonymous_student_id': self.test_system.anonymous_student_id,
'submission_time': qtime}
contents = self.openendedmodule.payload.copy()
contents.update({
@@ -224,7 +227,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'student_response': submission,
'max_score': self.max_score
})
- result = self.openendedmodule.send_to_grader(submission, test_system)
+ result = self.openendedmodule.send_to_grader(submission, self.test_system)
self.assertTrue(result)
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
@@ -238,7 +241,7 @@ class OpenEndedModuleTest(unittest.TestCase):
}
get = {'queuekey': "abcd",
'xqueue_body': score_msg}
- self.openendedmodule.update_score(get, test_system)
+ self.openendedmodule.update_score(get, self.test_system)
def update_score_single(self):
self.openendedmodule.new_history_entry("New Entry")
@@ -261,11 +264,11 @@ class OpenEndedModuleTest(unittest.TestCase):
}
get = {'queuekey': "abcd",
'xqueue_body': json.dumps(score_msg)}
- self.openendedmodule.update_score(get, test_system)
+ self.openendedmodule.update_score(get, self.test_system)
def test_latest_post_assessment(self):
self.update_score_single()
- assessment = self.openendedmodule.latest_post_assessment(test_system)
+ assessment = self.openendedmodule.latest_post_assessment(self.test_system)
self.assertFalse(assessment == '')
# check for errors
self.assertFalse('errors' in assessment)
@@ -336,7 +339,13 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
descriptor = Mock()
def setUp(self):
- self.combinedoe = CombinedOpenEndedV1Module(test_system, self.location, self.definition, self.descriptor, static_data = self.static_data, metadata=self.metadata)
+ self.test_system = test_system()
+ self.combinedoe = CombinedOpenEndedV1Module(self.test_system,
+ self.location,
+ self.definition,
+ self.descriptor,
+ static_data = self.static_data,
+ metadata=self.metadata)
def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("Tag")
diff --git a/common/lib/xmodule/xmodule/tests/test_conditional.py b/common/lib/xmodule/xmodule/tests/test_conditional.py
index 361a6ea785..16bd222b9e 100644
--- a/common/lib/xmodule/xmodule/tests/test_conditional.py
+++ b/common/lib/xmodule/xmodule/tests/test_conditional.py
@@ -56,6 +56,9 @@ class ConditionalModuleTest(unittest.TestCase):
'''Get a dummy system'''
return DummySystem(load_error_modules)
+ def setUp(self):
+ self.test_system = test_system()
+
def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error."""
print "Importing {0}".format(name)
@@ -85,14 +88,14 @@ class ConditionalModuleTest(unittest.TestCase):
location = descriptor.location
instance_state = instance_states.get(location.category, None)
print "inner_get_module, location=%s, inst_state=%s" % (location, instance_state)
- return descriptor.xmodule_constructor(test_system)(instance_state, shared_state)
+ return descriptor.xmodule_constructor(self.test_system)(instance_state, shared_state)
location = Location(["i4x", "edX", "cond_test", "conditional", "condone"])
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
return text
- test_system.replace_urls = replace_urls
- test_system.get_module = inner_get_module
+ self.test_system.replace_urls = replace_urls
+ self.test_system.get_module = inner_get_module
module = inner_get_module(location)
print "module: ", module
diff --git a/common/lib/xmodule/xmodule/tests/test_self_assessment.py b/common/lib/xmodule/xmodule/tests/test_self_assessment.py
index b9c3076b7c..362b73df67 100644
--- a/common/lib/xmodule/xmodule/tests/test_self_assessment.py
+++ b/common/lib/xmodule/xmodule/tests/test_self_assessment.py
@@ -53,13 +53,13 @@ class SelfAssessmentTest(unittest.TestCase):
'skip_basic_checks' : False,
}
- self.module = SelfAssessmentModule(test_system, self.location,
+ self.module = SelfAssessmentModule(test_system(), self.location,
self.definition, self.descriptor,
static_data,
state, metadata=self.metadata)
def test_get_html(self):
- html = self.module.get_html(test_system)
+ html = self.module.get_html(self.module.system)
self.assertTrue("This is sample prompt text" in html)
def test_self_assessment_flow(self):
@@ -82,10 +82,11 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(self.module.get_score()['score'], 0)
- self.module.save_answer({'student_answer': "I am an answer"}, test_system)
+ self.module.save_answer({'student_answer': "I am an answer"},
+ self.module.system)
self.assertEqual(self.module.state, self.module.ASSESSING)
- self.module.save_assessment(mock_query_dict, test_system)
+ self.module.save_assessment(mock_query_dict, self.module.system)
self.assertEqual(self.module.state, self.module.DONE)
@@ -94,7 +95,8 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(self.module.state, self.module.INITIAL)
# if we now assess as right, skip the REQUEST_HINT state
- self.module.save_answer({'student_answer': 'answer 4'}, test_system)
+ self.module.save_answer({'student_answer': 'answer 4'},
+ self.module.system)
responses['assessment'] = '1'
- self.module.save_assessment(mock_query_dict, test_system)
+ self.module.save_assessment(mock_query_dict, self.module.system)
self.assertEqual(self.module.state, self.module.DONE)
From c7d80a91eeff6875ba48096fa4cb92b06a3ebe40 Mon Sep 17 00:00:00 2001
From: Will Daly
Date: Tue, 5 Mar 2013 17:30:38 -0500
Subject: [PATCH 13/13] Changed name of survey_question to is_survey_question
for clarity
---
common/lib/xmodule/xmodule/capa_module.py | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index b42b7e4ccc..9ae6583c50 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -279,7 +279,7 @@ class CapaModule(XModule):
"""
Return True/False to indicate whether to show the "Reset" button.
"""
- survey_question = (self.max_attempts == 0)
+ is_survey_question = (self.max_attempts == 0)
if self.rerandomize in ["always", "onreset"]:
@@ -287,7 +287,7 @@ class CapaModule(XModule):
# then do NOT show the reset button.
# If the problem hasn't been submitted yet, then do NOT show
# the reset button.
- if (self.closed() and not survey_question) or not self.is_completed():
+ if (self.closed() and not is_survey_question) or not self.is_completed():
return False
else:
return True
@@ -307,14 +307,14 @@ class CapaModule(XModule):
if self.force_save_button == "true":
return not self.closed()
else:
- survey_question = (self.max_attempts == 0)
+ is_survey_question = (self.max_attempts == 0)
needs_reset = self.is_completed() and self.rerandomize == "always"
# If the problem is closed (and not a survey question with max_attempts==0),
# then do NOT show the reset button
# If we're waiting for the user to reset a randomized problem
# then do NOT show the reset button
- if (self.closed() and not survey_question) or needs_reset:
+ if (self.closed() and not is_survey_question) or needs_reset:
return False
else:
return True