Commit f8adfc62 by Diana Huang

Merge pull request #1602 from MITx/fix/will/capa_xmodule_unit_tests

Fix/will/capa xmodule unit tests
parents cde4cdf8 c7d80a91
...@@ -136,7 +136,7 @@ class CapaModule(XModule): ...@@ -136,7 +136,7 @@ class CapaModule(XModule):
self.close_date = self.display_due_date self.close_date = self.display_due_date
max_attempts = self.metadata.get('attempts', None) max_attempts = self.metadata.get('attempts', None)
if max_attempts: if max_attempts is not None:
self.max_attempts = int(max_attempts) self.max_attempts = int(max_attempts)
else: else:
self.max_attempts = None self.max_attempts = None
...@@ -247,13 +247,87 @@ class CapaModule(XModule): ...@@ -247,13 +247,87 @@ class CapaModule(XModule):
'progress': Progress.to_js_status_str(self.get_progress()) 'progress': Progress.to_js_status_str(self.get_progress())
}) })
def get_problem_html(self, encapsulate=True): def check_button_name(self):
'''Return html for the problem. Adds check, reset, save buttons """
as necessary based on the problem config and state.''' 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
try: return "Final Check" if final_check else "Check"
html = self.lcp.get_html()
except Exception, err: 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.
"""
is_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 is_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:
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 is_survey_question) or needs_reset:
return False
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) log.exception(err)
# TODO (vshnayder): another switch on DEBUG. # TODO (vshnayder): another switch on DEBUG.
...@@ -265,9 +339,10 @@ class CapaModule(XModule): ...@@ -265,9 +339,10 @@ class CapaModule(XModule):
msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '&lt;') msg += '<p>Error:</p><p><pre>%s</pre></p>' % str(err).replace('<', '&lt;')
msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;') msg += '<p><pre>%s</pre></p>' % traceback.format_exc().replace('<', '&lt;')
html = msg html = msg
else:
# We're in non-debug mode, and possibly even in production. We want # We're in non-debug mode, and possibly even in production. We want
# to avoid bricking of problem as much as possible # to avoid bricking of problem as much as possible
else:
# Presumably, student submission has corrupted LoncapaProblem HTML. # Presumably, student submission has corrupted LoncapaProblem HTML.
# First, pull down all student answers # First, pull down all student answers
...@@ -308,62 +383,40 @@ class CapaModule(XModule): ...@@ -308,62 +383,40 @@ class CapaModule(XModule):
log.exception(err) log.exception(err)
raise raise
content = {'name': self.display_name, return html
'html': html,
'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 def get_problem_html(self, encapsulate=True):
if self.max_attempts is None or self.attempts < self.max_attempts - 1: '''Return html for the problem. Adds check, reset, save buttons
check_button = "Check" as necessary based on the problem config and state.'''
else:
# Will be final check so let user know that
check_button = "Final Check"
reset_button = True try:
save_button = True html = self.lcp.get_html()
# If we're after deadline, or user has exhausted attempts, # If we cannot construct the problem HTML,
# question is read-only. # then generate an error message instead.
if self.closed(): except Exception, err:
check_button = False html = self.handle_problem_html_error(err)
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 # The convention is to pass the name of the check button
# more submissions. # if we want to show a check button, and False otherwise
if self.lcp.done and self.rerandomize == "always": # 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 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 content = {'name': self.display_name,
if not self.lcp.done: 'html': html,
reset_button = False 'weight': self.descriptor.weight,
}
# 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, context = {'problem': content,
'id': self.id, 'id': self.id,
'check_button': check_button, 'check_button': check_button,
'reset_button': reset_button, 'reset_button': self.should_show_reset_button(),
'save_button': save_button, 'save_button': self.should_show_save_button(),
'answer_available': self.answer_available(), 'answer_available': self.answer_available(),
'ajax_url': self.system.ajax_url, 'ajax_url': self.system.ajax_url,
'attempts_used': self.attempts, 'attempts_used': self.attempts,
...@@ -419,7 +472,7 @@ class CapaModule(XModule): ...@@ -419,7 +472,7 @@ class CapaModule(XModule):
def closed(self): def closed(self):
''' Is the student still allowed to submit answers? ''' ''' 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 return True
if self.is_past_due(): if self.is_past_due():
return True return True
...@@ -528,21 +581,61 @@ class CapaModule(XModule): ...@@ -528,21 +581,61 @@ class CapaModule(XModule):
def make_dict_of_responses(get): def make_dict_of_responses(get):
'''Make dictionary of student responses (aka "answers") '''Make dictionary of student responses (aka "answers")
get is POST dictionary. 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() answers = dict()
for key in get: for key in get:
# e.g. input_resistor_1 ==> resistor_1 # e.g. input_resistor_1 ==> resistor_1
_, _, name = key.partition('_') _, _, name = key.partition('_')
# 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:
# This allows for answers which require more than one value for # This allows for answers which require more than one value for
# the same form input (e.g. checkbox inputs). The convention is that # the same form input (e.g. checkbox inputs). The convention is that
# if the name ends with '[]' (which looks like an array), then the # if the name ends with '[]' (which looks like an array), then the
# answer will be an array. # answer will be an array.
if not name.endswith('[]'): is_list_key = name.endswith('[]')
answers[name] = get[key] 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: else:
name = name[:-2] val = get[key]
answers[name] = get.getlist(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 return answers
...@@ -550,7 +643,7 @@ class CapaModule(XModule): ...@@ -550,7 +643,7 @@ class CapaModule(XModule):
''' Checks whether answers to a problem are correct, and ''' Checks whether answers to a problem are correct, and
returns a map of correct/incorrect answers: returns a map of correct/incorrect answers:
{'success' : bool, {'success' : 'correct' | 'incorrect' | AJAX alert msg string,
'contents' : html} 'contents' : html}
''' '''
event_info = dict() event_info = dict()
...@@ -663,7 +756,12 @@ class CapaModule(XModule): ...@@ -663,7 +756,12 @@ class CapaModule(XModule):
''' Changes problem state to unfinished -- removes student answers, ''' Changes problem state to unfinished -- removes student answers,
and causes problem to rerender itself. 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 = dict()
event_info['old_state'] = self.lcp.get_state() event_info['old_state'] = self.lcp.get_state()
...@@ -687,6 +785,7 @@ class CapaModule(XModule): ...@@ -687,6 +785,7 @@ class CapaModule(XModule):
# in next line) # in next line)
self.lcp.seed = None self.lcp.seed = None
self.lcp = LoncapaProblem(self.definition['data'], self.lcp = LoncapaProblem(self.definition['data'],
self.location.html_id(), self.lcp.get_state(), self.location.html_id(), self.lcp.get_state(),
system=self.system) system=self.system)
...@@ -694,7 +793,8 @@ class CapaModule(XModule): ...@@ -694,7 +793,8 @@ class CapaModule(XModule):
event_info['new_state'] = self.lcp.get_state() event_info['new_state'] = self.lcp.get_state()
self.system.track_function('reset_problem', event_info) 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): class CapaDescriptor(RawDescriptor):
......
...@@ -28,13 +28,27 @@ open_ended_grading_interface = { ...@@ -28,13 +28,27 @@ open_ended_grading_interface = {
'grading_controller' : 'grading_controller' 'grading_controller' : 'grading_controller'
} }
test_system = ModuleSystem(
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', ajax_url='courses/course_id/modx/a_location',
track_function=Mock(), track_function=Mock(),
get_module=Mock(), get_module=Mock(),
# "render" to just the context...
render_template=lambda template, context: str(context), render_template=lambda template, context: str(context),
replace_urls=Mock(), replace_urls=lambda html: str(html),
user=Mock(is_staff=False), user=Mock(is_staff=False),
filestore=Mock(), filestore=Mock(),
debug=True, debug=True,
...@@ -42,7 +56,7 @@ test_system = ModuleSystem( ...@@ -42,7 +56,7 @@ test_system = ModuleSystem(
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student', anonymous_student_id='student',
open_ended_grading_interface= open_ended_grading_interface open_ended_grading_interface= open_ended_grading_interface
) )
class ModelsTest(unittest.TestCase): class ModelsTest(unittest.TestCase):
......
import datetime import datetime
import json import json
from mock import Mock from mock import Mock, MagicMock, patch
from pprint import pprint from pprint import pprint
import unittest import unittest
import random
import xmodule
import capa
from xmodule.capa_module import CapaModule from xmodule.capa_module import CapaModule
from xmodule.modulestore import Location from xmodule.modulestore import Location
from lxml import etree from lxml import etree
...@@ -34,6 +37,18 @@ class CapaFactory(object): ...@@ -34,6 +37,18 @@ class CapaFactory(object):
return CapaFactory.num return CapaFactory.num
@staticmethod @staticmethod
def input_key():
""" Return the input key to use when passing GET parameters """
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")
@staticmethod
def create(graceperiod=None, def create(graceperiod=None,
due=None, due=None,
max_attempts=None, max_attempts=None,
...@@ -59,11 +74,10 @@ class CapaFactory(object): ...@@ -59,11 +74,10 @@ class CapaFactory(object):
module. module.
attempts: also added to instance state. Will be converted to an int. 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, } definition = {'data': CapaFactory.sample_problem_xml, }
location = Location(["i4x", "edX", "capa_test", "problem", location = Location(["i4x", "edX", "capa_test", "problem",
"SampleProblem{0}".format(CapaFactory.next_num())]) "SampleProblem%d" % CapaFactory.next_num()])
metadata = {} metadata = {}
if graceperiod is not None: if graceperiod is not None:
metadata['graceperiod'] = graceperiod metadata['graceperiod'] = graceperiod
...@@ -89,19 +103,12 @@ class CapaFactory(object): ...@@ -89,19 +103,12 @@ class CapaFactory(object):
# since everything else is a string. # since everything else is a string.
instance_state_dict['attempts'] = int(attempts) 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: if len(instance_state_dict) > 0:
instance_state = json.dumps(instance_state_dict) instance_state = json.dumps(instance_state_dict)
else: else:
instance_state = None instance_state = None
module = CapaModule(test_system, location, module = CapaModule(test_system(), location,
definition, descriptor, definition, descriptor,
instance_state, None, metadata=metadata) instance_state, None, metadata=metadata)
...@@ -282,3 +289,572 @@ class CapaModuleTest(unittest.TestCase): ...@@ -282,3 +289,572 @@ class CapaModuleTest(unittest.TestCase):
due=self.yesterday_str, due=self.yesterday_str,
graceperiod=self.two_day_delta_str) graceperiod=self.two_day_delta_str)
self.assertTrue(still_in_grace.answer_available()) 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
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)
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()
# 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' }
result = module.check_problem(get_request_dict)
# 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)
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)
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 = "<div>Test HTML</div>"
# 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'], "<div>Test HTML</div>")
# 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'])
def test_save_problem(self):
module = CapaFactory.create()
# 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):
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):
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):
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'])
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())
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
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
# Patch the capa problem's HTML rendering
with patch('capa.capa_problem.LoncapaProblem.get_html') as mock_html:
mock_html.return_value = "<div>Test Problem HTML</div>"
# Render the problem HTML
html = module.get_problem_html(encapsulate=False)
# Also render the problem encapsulated in a <div>
html_encapsulated = module.get_problem_html(encapsulate=True)
# Expect that we get the rendered template back
self.assertEqual(html, "<div>Test Template HTML</div>")
# Check the rendering context
render_args,_ = module.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'], "<div>Test Problem HTML</div>")
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
module.system.render_template = Mock(return_value="<div>Test Template HTML</div>")
# 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,_ = module.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)
...@@ -54,7 +54,8 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -54,7 +54,8 @@ class OpenEndedChildTest(unittest.TestCase):
descriptor = Mock() descriptor = Mock()
def setUp(self): 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) self.definition, self.descriptor, self.static_data, self.metadata)
...@@ -69,7 +70,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -69,7 +70,7 @@ class OpenEndedChildTest(unittest.TestCase):
def test_latest_post_assessment_empty(self): 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, "") self.assertEqual(answer, "")
...@@ -106,7 +107,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -106,7 +107,7 @@ class OpenEndedChildTest(unittest.TestCase):
post_assessment = "Post assessment" post_assessment = "Post assessment"
self.openendedchild.record_latest_post_assessment(post_assessment) self.openendedchild.record_latest_post_assessment(post_assessment)
self.assertEqual(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): def test_get_score(self):
new_answer = "New Answer" new_answer = "New Answer"
...@@ -125,7 +126,7 @@ class OpenEndedChildTest(unittest.TestCase): ...@@ -125,7 +126,7 @@ class OpenEndedChildTest(unittest.TestCase):
def test_reset(self): def test_reset(self):
self.openendedchild.reset(test_system) self.openendedchild.reset(self.test_system)
state = json.loads(self.openendedchild.get_instance_state()) state = json.loads(self.openendedchild.get_instance_state())
self.assertEqual(state['state'], OpenEndedChild.INITIAL) self.assertEqual(state['state'], OpenEndedChild.INITIAL)
...@@ -182,11 +183,13 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -182,11 +183,13 @@ class OpenEndedModuleTest(unittest.TestCase):
descriptor = Mock() descriptor = Mock()
def setUp(self): 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 = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message") 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.test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 1}
self.openendedmodule = OpenEndedModule(test_system, self.location, self.openendedmodule = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata) self.definition, self.descriptor, self.static_data, self.metadata)
def test_message_post(self): def test_message_post(self):
...@@ -195,7 +198,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -195,7 +198,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'grader_id': '1', 'grader_id': '1',
'score': 3} 'score': 3}
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) 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} 'submission_time': qtime}
contents = { contents = {
'feedback': get['feedback'], 'feedback': get['feedback'],
...@@ -205,7 +208,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -205,7 +208,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'student_info': json.dumps(student_info) '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']) self.assertTrue(result['success'])
# make sure it's actually sending something we want to the queue # 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) self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
...@@ -216,7 +219,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -216,7 +219,7 @@ class OpenEndedModuleTest(unittest.TestCase):
def test_send_to_grader(self): def test_send_to_grader(self):
submission = "This is a student submission" submission = "This is a student submission"
qtime = datetime.strftime(datetime.now(), xqueue_interface.dateformat) 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} 'submission_time': qtime}
contents = self.openendedmodule.payload.copy() contents = self.openendedmodule.payload.copy()
contents.update({ contents.update({
...@@ -224,7 +227,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -224,7 +227,7 @@ class OpenEndedModuleTest(unittest.TestCase):
'student_response': submission, 'student_response': submission,
'max_score': self.max_score '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.assertTrue(result)
self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY) self.mock_xqueue.send_to_queue.assert_called_with(body=json.dumps(contents), header=ANY)
...@@ -238,7 +241,7 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -238,7 +241,7 @@ class OpenEndedModuleTest(unittest.TestCase):
} }
get = {'queuekey': "abcd", get = {'queuekey': "abcd",
'xqueue_body': score_msg} 'xqueue_body': score_msg}
self.openendedmodule.update_score(get, test_system) self.openendedmodule.update_score(get, self.test_system)
def update_score_single(self): def update_score_single(self):
self.openendedmodule.new_history_entry("New Entry") self.openendedmodule.new_history_entry("New Entry")
...@@ -261,11 +264,11 @@ class OpenEndedModuleTest(unittest.TestCase): ...@@ -261,11 +264,11 @@ class OpenEndedModuleTest(unittest.TestCase):
} }
get = {'queuekey': "abcd", get = {'queuekey': "abcd",
'xqueue_body': json.dumps(score_msg)} '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): def test_latest_post_assessment(self):
self.update_score_single() self.update_score_single()
assessment = self.openendedmodule.latest_post_assessment(test_system) assessment = self.openendedmodule.latest_post_assessment(self.test_system)
self.assertFalse(assessment == '') self.assertFalse(assessment == '')
# check for errors # check for errors
self.assertFalse('errors' in assessment) self.assertFalse('errors' in assessment)
...@@ -336,7 +339,13 @@ class CombinedOpenEndedModuleTest(unittest.TestCase): ...@@ -336,7 +339,13 @@ class CombinedOpenEndedModuleTest(unittest.TestCase):
descriptor = Mock() descriptor = Mock()
def setUp(self): 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): def test_get_tag_name(self):
name = self.combinedoe.get_tag_name("<t>Tag</t>") name = self.combinedoe.get_tag_name("<t>Tag</t>")
......
...@@ -56,6 +56,9 @@ class ConditionalModuleTest(unittest.TestCase): ...@@ -56,6 +56,9 @@ class ConditionalModuleTest(unittest.TestCase):
'''Get a dummy system''' '''Get a dummy system'''
return DummySystem(load_error_modules) return DummySystem(load_error_modules)
def setUp(self):
self.test_system = test_system()
def get_course(self, name): def get_course(self, name):
"""Get a test course by directory name. If there's more than one, error.""" """Get a test course by directory name. If there's more than one, error."""
print "Importing {0}".format(name) print "Importing {0}".format(name)
...@@ -85,14 +88,14 @@ class ConditionalModuleTest(unittest.TestCase): ...@@ -85,14 +88,14 @@ class ConditionalModuleTest(unittest.TestCase):
location = descriptor.location location = descriptor.location
instance_state = instance_states.get(location.category, None) instance_state = instance_states.get(location.category, None)
print "inner_get_module, location=%s, inst_state=%s" % (location, instance_state) 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"]) location = Location(["i4x", "edX", "cond_test", "conditional", "condone"])
def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None): def replace_urls(text, staticfiles_prefix=None, replace_prefix='/static/', course_namespace=None):
return text return text
test_system.replace_urls = replace_urls self.test_system.replace_urls = replace_urls
test_system.get_module = inner_get_module self.test_system.get_module = inner_get_module
module = inner_get_module(location) module = inner_get_module(location)
print "module: ", module print "module: ", module
......
...@@ -53,13 +53,13 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -53,13 +53,13 @@ class SelfAssessmentTest(unittest.TestCase):
'skip_basic_checks' : False, 'skip_basic_checks' : False,
} }
self.module = SelfAssessmentModule(test_system, self.location, self.module = SelfAssessmentModule(test_system(), self.location,
self.definition, self.descriptor, self.definition, self.descriptor,
static_data, static_data,
state, metadata=self.metadata) state, metadata=self.metadata)
def test_get_html(self): 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) self.assertTrue("This is sample prompt text" in html)
def test_self_assessment_flow(self): def test_self_assessment_flow(self):
...@@ -82,10 +82,11 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -82,10 +82,11 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(self.module.get_score()['score'], 0) 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.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) self.assertEqual(self.module.state, self.module.DONE)
...@@ -94,7 +95,8 @@ class SelfAssessmentTest(unittest.TestCase): ...@@ -94,7 +95,8 @@ class SelfAssessmentTest(unittest.TestCase):
self.assertEqual(self.module.state, self.module.INITIAL) self.assertEqual(self.module.state, self.module.INITIAL)
# if we now assess as right, skip the REQUEST_HINT state # 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' 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) self.assertEqual(self.module.state, self.module.DONE)
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment