response_msg_div.set("class", "response_message")
@@ -384,20 +394,20 @@ class JavascriptResponse(LoncapaResponse):
# until we decide on exactly how to solve this issue. For now, files are
# manually being compiled to DATA_DIR/js/compiled.
- #latestTimestamp = 0
- #basepath = self.system.filestore.root_path + '/js/'
- #for filename in (self.display_dependencies + [self.display]):
+ # latestTimestamp = 0
+ # basepath = self.system.filestore.root_path + '/js/'
+ # for filename in (self.display_dependencies + [self.display]):
# filepath = basepath + filename
# timestamp = os.stat(filepath).st_mtime
# if timestamp > latestTimestamp:
# latestTimestamp = timestamp
#
- #h = hashlib.md5()
- #h.update(self.answer_id + str(self.display_dependencies))
- #compiled_filename = 'compiled/' + h.hexdigest() + '.js'
- #compiled_filepath = basepath + compiled_filename
+ # h = hashlib.md5()
+ # h.update(self.answer_id + str(self.display_dependencies))
+ # compiled_filename = 'compiled/' + h.hexdigest() + '.js'
+ # compiled_filepath = basepath + compiled_filename
- #if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp:
+ # if not os.path.exists(compiled_filepath) or os.stat(compiled_filepath).st_mtime < latestTimestamp:
# outfile = open(compiled_filepath, 'w')
# for filename in (self.display_dependencies + [self.display]):
# filepath = basepath + filename
@@ -419,7 +429,7 @@ class JavascriptResponse(LoncapaResponse):
id=self.xml.get('id'))[0]
self.display_xml = self.xml.xpath('//*[@id=$id]//display',
- id=self.xml.get('id'))[0]
+ id=self.xml.get('id'))[0]
self.xml.remove(self.generator_xml)
self.xml.remove(self.grader_xml)
@@ -430,17 +440,20 @@ class JavascriptResponse(LoncapaResponse):
self.display = self.display_xml.get("src")
if self.generator_xml.get("dependencies"):
- self.generator_dependencies = self.generator_xml.get("dependencies").split()
+ self.generator_dependencies = self.generator_xml.get(
+ "dependencies").split()
else:
self.generator_dependencies = []
if self.grader_xml.get("dependencies"):
- self.grader_dependencies = self.grader_xml.get("dependencies").split()
+ self.grader_dependencies = self.grader_xml.get(
+ "dependencies").split()
else:
self.grader_dependencies = []
if self.display_xml.get("dependencies"):
- self.display_dependencies = self.display_xml.get("dependencies").split()
+ self.display_dependencies = self.display_xml.get(
+ "dependencies").split()
else:
self.display_dependencies = []
@@ -461,10 +474,10 @@ class JavascriptResponse(LoncapaResponse):
return subprocess.check_output(subprocess_args, env=self.get_node_env())
-
def generate_problem_state(self):
- generator_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_generator.js'
+ generator_file = os.path.dirname(os.path.normpath(
+ __file__)) + '/javascript_problem_generator.js'
output = self.call_node([generator_file,
self.generator,
json.dumps(self.generator_dependencies),
@@ -478,17 +491,18 @@ class JavascriptResponse(LoncapaResponse):
params = {}
for param in self.xml.xpath('//*[@id=$id]//responseparam',
- id=self.xml.get('id')):
+ id=self.xml.get('id')):
raw_param = param.get("value")
- params[param.get("name")] = json.loads(contextualize_text(raw_param, self.context))
+ params[param.get("name")] = json.loads(
+ contextualize_text(raw_param, self.context))
return params
def prepare_inputfield(self):
for inputfield in self.xml.xpath('//*[@id=$id]//javascriptinput',
- id=self.xml.get('id')):
+ id=self.xml.get('id')):
escapedict = {'"': '"'}
@@ -501,7 +515,7 @@ class JavascriptResponse(LoncapaResponse):
escapedict)
inputfield.set("problem_state", encoded_problem_state)
- inputfield.set("display_file", self.display_filename)
+ inputfield.set("display_file", self.display_filename)
inputfield.set("display_class", self.display_class)
def get_score(self, student_answers):
@@ -519,7 +533,8 @@ class JavascriptResponse(LoncapaResponse):
if submission is None or submission == '':
submission = json.dumps(None)
- grader_file = os.path.dirname(os.path.normpath(__file__)) + '/javascript_problem_grader.js'
+ grader_file = os.path.dirname(os.path.normpath(
+ __file__)) + '/javascript_problem_grader.js'
outputs = self.call_node([grader_file,
self.grader,
json.dumps(self.grader_dependencies),
@@ -528,8 +543,8 @@ class JavascriptResponse(LoncapaResponse):
json.dumps(self.params)]).split('\n')
all_correct = json.loads(outputs[0].strip())
- evaluation = outputs[1].strip()
- solution = outputs[2].strip()
+ evaluation = outputs[1].strip()
+ solution = outputs[2].strip()
return (all_correct, evaluation, solution)
def get_answers(self):
@@ -539,9 +554,7 @@ class JavascriptResponse(LoncapaResponse):
return {self.answer_id: self.solution}
-
#-----------------------------------------------------------------------------
-
class ChoiceResponse(LoncapaResponse):
"""
This response type is used when the student chooses from a discrete set of
@@ -599,9 +612,10 @@ class ChoiceResponse(LoncapaResponse):
self.assign_choice_names()
correct_xml = self.xml.xpath('//*[@id=$id]//choice[@correct="true"]',
- id=self.xml.get('id'))
+ id=self.xml.get('id'))
- self.correct_choices = set([choice.get('name') for choice in correct_xml])
+ self.correct_choices = set([choice.get(
+ 'name') for choice in correct_xml])
def assign_choice_names(self):
'''
@@ -654,7 +668,8 @@ class MultipleChoiceResponse(LoncapaResponse):
allowed_inputfields = ['choicegroup']
def setup_response(self):
- # call secondary setup for MultipleChoice questions, to set name attributes
+ # call secondary setup for MultipleChoice questions, to set name
+ # attributes
self.mc_setup_response()
# define correct choices (after calling secondary setup)
@@ -692,7 +707,7 @@ class MultipleChoiceResponse(LoncapaResponse):
# log.debug('%s: student_answers=%s, correct_choices=%s' % (
# unicode(self), student_answers, self.correct_choices))
if (self.answer_id in student_answers
- and student_answers[self.answer_id] in self.correct_choices):
+ and student_answers[self.answer_id] in self.correct_choices):
return CorrectMap(self.answer_id, 'correct')
else:
return CorrectMap(self.answer_id, 'incorrect')
@@ -760,7 +775,8 @@ class OptionResponse(LoncapaResponse):
return cmap
def get_answers(self):
- amap = dict([(af.get('id'), contextualize_text(af.get('correct'), self.context)) for af in self.answer_fields])
+ amap = dict([(af.get('id'), contextualize_text(af.get(
+ 'correct'), self.context)) for af in self.answer_fields])
# log.debug('%s: expected answers=%s' % (unicode(self),amap))
return amap
@@ -780,8 +796,9 @@ class NumericalResponse(LoncapaResponse):
context = self.context
self.correct_answer = contextualize_text(xml.get('answer'), context)
try:
- self.tolerance_xml = xml.xpath('//*[@id=$id]//responseparam[@type="tolerance"]/@default',
- id=xml.get('id'))[0]
+ self.tolerance_xml = xml.xpath(
+ '//*[@id=$id]//responseparam[@type="tolerance"]/@default',
+ id=xml.get('id'))[0]
self.tolerance = contextualize_text(self.tolerance_xml, context)
except Exception:
self.tolerance = '0'
@@ -798,17 +815,21 @@ class NumericalResponse(LoncapaResponse):
try:
correct_ans = complex(self.correct_answer)
except ValueError:
- log.debug("Content error--answer '{0}' is not a valid complex number".format(self.correct_answer))
- raise StudentInputError("There was a problem with the staff answer to this problem")
+ log.debug("Content error--answer '{0}' is not a valid complex number".format(
+ self.correct_answer))
+ raise StudentInputError(
+ "There was a problem with the staff answer to this problem")
try:
- correct = compare_with_tolerance(evaluator(dict(), dict(), student_answer),
- correct_ans, self.tolerance)
+ correct = compare_with_tolerance(
+ evaluator(dict(), dict(), student_answer),
+ correct_ans, self.tolerance)
# We should catch this explicitly.
# I think this is just pyparsing.ParseException, calc.UndefinedVariable:
# But we'd need to confirm
except:
- # Use the traceback-preserving version of re-raising with a different type
+ # Use the traceback-preserving version of re-raising with a
+ # different type
import sys
type, value, traceback = sys.exc_info()
@@ -837,7 +858,8 @@ class StringResponse(LoncapaResponse):
max_inputfields = 1
def setup_response(self):
- self.correct_answer = contextualize_text(self.xml.get('answer'), self.context).strip()
+ self.correct_answer = contextualize_text(
+ self.xml.get('answer'), self.context).strip()
def get_score(self, student_answers):
'''Grade a string response '''
@@ -846,7 +868,8 @@ class StringResponse(LoncapaResponse):
return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect')
def check_string(self, expected, given):
- if self.xml.get('type') == 'ci': return given.lower() == expected.lower()
+ if self.xml.get('type') == 'ci':
+ return given.lower() == expected.lower()
return given == expected
def check_hint_condition(self, hxml_set, student_answers):
@@ -854,8 +877,10 @@ class StringResponse(LoncapaResponse):
hints_to_show = []
for hxml in hxml_set:
name = hxml.get('name')
- correct_answer = contextualize_text(hxml.get('answer'), self.context).strip()
- if self.check_string(correct_answer, given): hints_to_show.append(name)
+ correct_answer = contextualize_text(
+ hxml.get('answer'), self.context).strip()
+ if self.check_string(correct_answer, given):
+ hints_to_show.append(name)
log.debug('hints_to_show = %s' % hints_to_show)
return hints_to_show
@@ -889,7 +914,7 @@ class CustomResponse(LoncapaResponse):
correct[0] ='incorrect'
"""},
- {'snippet': """
+
diff --git a/common/lib/capa/capa/tests/__init__.py b/common/lib/capa/capa/tests/__init__.py
index 89cb5a5ee9..72d82c683b 100644
--- a/common/lib/capa/capa/tests/__init__.py
+++ b/common/lib/capa/capa/tests/__init__.py
@@ -2,7 +2,7 @@ import fs
import fs.osfs
import os
-from mock import Mock
+from mock import Mock, MagicMock
import xml.sax.saxutils as saxutils
@@ -16,6 +16,11 @@ def tst_render_template(template, context):
"""
return '
{0}
'.format(saxutils.escape(repr(context)))
+def calledback_url(dispatch = 'score_update'):
+ return dispatch
+
+xqueue_interface = MagicMock()
+xqueue_interface.send_to_queue.return_value = (0, 'Success!')
test_system = Mock(
ajax_url='courses/course_id/modx/a_location',
@@ -26,7 +31,7 @@ test_system = Mock(
user=Mock(),
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
debug=True,
- xqueue={'interface': None, 'callback_url': '/', 'default_queuename': 'testqueue', 'waittime': 10},
+ xqueue={'interface': xqueue_interface, 'construct_callback': calledback_url, 'default_queuename': 'testqueue', 'waittime': 10},
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
anonymous_student_id='student'
)
diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py
index 360fd9f2f6..250cedd549 100644
--- a/common/lib/capa/capa/tests/test_inputtypes.py
+++ b/common/lib/capa/capa/tests/test_inputtypes.py
@@ -23,6 +23,7 @@ import xml.sax.saxutils as saxutils
from . import test_system
from capa import inputtypes
+from mock import ANY
# just a handy shortcut
lookup_tag = inputtypes.registry.get_class_for_tag
@@ -300,6 +301,98 @@ class CodeInputTest(unittest.TestCase):
self.assertEqual(context, expected)
+class MatlabTest(unittest.TestCase):
+ '''
+ Test Matlab input types
+ '''
+ def setUp(self):
+ self.rows = '10'
+ self.cols = '80'
+ self.tabsize = '4'
+ self.mode = ""
+ self.payload = "payload"
+ self.linenumbers = 'true'
+ self.xml = """
+
+ {payload}
+
+ """.format(r = self.rows,
+ c = self.cols,
+ tabsize = self.tabsize,
+ m = self.mode,
+ payload = self.payload,
+ ln = self.linenumbers)
+ elt = etree.fromstring(self.xml)
+ state = {'value': 'print "good evening"',
+ 'status': 'incomplete',
+ 'feedback': {'message': '3'}, }
+
+ self.input_class = lookup_tag('matlabinput')
+ self.the_input = self.input_class(test_system, elt, state)
+
+
+ def test_rendering(self):
+ context = self.the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'value': 'print "good evening"',
+ 'status': 'queued',
+ 'msg': self.input_class.submitted_msg,
+ 'mode': self.mode,
+ 'rows': self.rows,
+ 'cols': self.cols,
+ 'queue_msg': '',
+ 'linenumbers': 'true',
+ 'hidden': '',
+ 'tabsize': int(self.tabsize),
+ 'queue_len': '3',
+ }
+
+ self.assertEqual(context, expected)
+
+
+ def test_rendering_with_state(self):
+ state = {'value': 'print "good evening"',
+ 'status': 'incomplete',
+ 'input_state': {'queue_msg': 'message'},
+ 'feedback': {'message': '3'}, }
+ elt = etree.fromstring(self.xml)
+
+ input_class = lookup_tag('matlabinput')
+ the_input = self.input_class(test_system, elt, state)
+ context = the_input._get_render_context()
+
+ expected = {'id': 'prob_1_2',
+ 'value': 'print "good evening"',
+ 'status': 'queued',
+ 'msg': self.input_class.submitted_msg,
+ 'mode': self.mode,
+ 'rows': self.rows,
+ 'cols': self.cols,
+ 'queue_msg': 'message',
+ 'linenumbers': 'true',
+ 'hidden': '',
+ 'tabsize': int(self.tabsize),
+ 'queue_len': '3',
+ }
+
+ self.assertEqual(context, expected)
+
+ def test_plot_data(self):
+ get = {'submission': 'x = 1234;'}
+ response = self.the_input.handle_ajax("plot", get)
+
+ test_system.xqueue['interface'].send_to_queue.assert_called_with(header=ANY, body=ANY)
+
+ self.assertTrue(response['success'])
+ self.assertTrue(self.the_input.input_state['queuekey'] is not None)
+ self.assertEqual(self.the_input.input_state['queuestate'], 'queued')
+
+
+
class SchematicTest(unittest.TestCase):
'''
diff --git a/common/lib/xmodule/xmodule/capa_module.py b/common/lib/xmodule/xmodule/capa_module.py
index e66b1d3495..da8b5b4f96 100644
--- a/common/lib/xmodule/xmodule/capa_module.py
+++ b/common/lib/xmodule/xmodule/capa_module.py
@@ -93,6 +93,7 @@ class CapaFields(object):
rerandomize = Randomization(help="When to rerandomize the problem", default="always", scope=Scope.settings)
data = String(help="XML data for the problem", scope=Scope.content)
correct_map = Object(help="Dictionary with the correctness of current student answers", scope=Scope.student_state, default={})
+ input_state = Object(help="Dictionary for maintaining the state of inputtypes", scope=Scope.student_state, default={})
student_answers = Object(help="Dictionary with the current student responses", scope=Scope.student_state)
done = Boolean(help="Whether the student has answered the problem", scope=Scope.student_state)
display_name = String(help="Display name for this module", scope=Scope.settings)
@@ -188,6 +189,7 @@ class CapaModule(CapaFields, XModule):
'done': self.done,
'correct_map': self.correct_map,
'student_answers': self.student_answers,
+ 'input_state': self.input_state,
'seed': self.seed,
}
@@ -195,6 +197,7 @@ class CapaModule(CapaFields, XModule):
lcp_state = self.lcp.get_state()
self.done = lcp_state['done']
self.correct_map = lcp_state['correct_map']
+ self.input_state = lcp_state['input_state']
self.student_answers = lcp_state['student_answers']
self.seed = lcp_state['seed']
@@ -443,7 +446,8 @@ class CapaModule(CapaFields, XModule):
'problem_save': self.save_problem,
'problem_show': self.get_answer,
'score_update': self.update_score,
- 'input_ajax': self.lcp.handle_input_ajax
+ 'input_ajax': self.handle_input_ajax,
+ 'ungraded_response': self.handle_ungraded_response
}
if dispatch not in handlers:
@@ -537,6 +541,43 @@ class CapaModule(CapaFields, XModule):
return dict() # No AJAX return is needed
+ def handle_ungraded_response(self, get):
+ '''
+ Delivers a response from the XQueue to the capa problem
+
+ The score of the problem will not be updated
+
+ Args:
+ - get (dict) must contain keys:
+ queuekey - a key specific to this response
+ xqueue_body - the body of the response
+ Returns:
+ empty dictionary
+
+ No ajax return is needed, so an empty dict is returned
+ '''
+ queuekey = get['queuekey']
+ score_msg = get['xqueue_body']
+ # pass along the xqueue message to the problem
+ self.lcp.ungraded_response(score_msg, queuekey)
+ self.set_state_from_lcp()
+ return dict()
+
+ def handle_input_ajax(self, get):
+ '''
+ Handle ajax calls meant for a particular input in the problem
+
+ Args:
+ - get (dict) - data that should be passed to the input
+ Returns:
+ - dict containing the response from the input
+ '''
+ response = self.lcp.handle_input_ajax(get)
+ # save any state changes that may occur
+ self.set_state_from_lcp()
+ return response
+
+
def get_answer(self, get):
'''
For the "show answer" button.
diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
index 158c2b98d0..70704ab247 100644
--- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee
+++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee
@@ -41,6 +41,11 @@ class @Problem
@el.attr progress: response.progress_status
@el.trigger('progressChanged')
+ forceUpdate: (response) =>
+ @el.attr progress: response.progress_status
+ @el.trigger('progressChanged')
+
+
queueing: =>
@queued_items = @$(".xqueue")
@num_queued_items = @queued_items.length
@@ -71,6 +76,7 @@ class @Problem
@num_queued_items = @new_queued_items.length
if @num_queued_items == 0
+ @forceUpdate response
delete window.queuePollerID
else
# TODO: Some logic to dynamically adjust polling rate based on queuelen
diff --git a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
index 1f84d2ab8c..8373700837 100644
--- a/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
+++ b/common/lib/xmodule/xmodule/open_ended_grading_classes/open_ended_module.py
@@ -174,7 +174,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
str(len(self.child_history)))
xheader = xqueue_interface.make_xheader(
- lms_callback_url=system.xqueue['callback_url'],
+ lms_callback_url=system.xqueue['construct_callback'](),
lms_key=queuekey,
queue_name=self.message_queue_name
)
@@ -224,7 +224,7 @@ class OpenEndedModule(openendedchild.OpenEndedChild):
anonymous_student_id +
str(len(self.child_history)))
- xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['callback_url'],
+ xheader = xqueue_interface.make_xheader(lms_callback_url=system.xqueue['construct_callback'](),
lms_key=queuekey,
queue_name=self.queue_name)
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 09c86baf27..55c31ded58 100644
--- a/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
+++ b/common/lib/xmodule/xmodule/tests/test_combined_open_ended.py
@@ -183,7 +183,10 @@ class OpenEndedModuleTest(unittest.TestCase):
self.test_system.location = self.location
self.mock_xqueue = MagicMock()
self.mock_xqueue.send_to_queue.return_value = (None, "Message")
- self.test_system.xqueue = {'interface': self.mock_xqueue, 'callback_url': '/', 'default_queuename': 'testqueue',
+ def constructed_callback(dispatch="score_update"):
+ return dispatch
+
+ self.test_system.xqueue = {'interface': self.mock_xqueue, 'construct_callback': constructed_callback, 'default_queuename': 'testqueue',
'waittime': 1}
self.openendedmodule = OpenEndedModule(self.test_system, self.location,
self.definition, self.descriptor, self.static_data, self.metadata)
diff --git a/lms/djangoapps/courseware/module_render.py b/lms/djangoapps/courseware/module_render.py
index 08df7bfb8c..973940d784 100644
--- a/lms/djangoapps/courseware/module_render.py
+++ b/lms/djangoapps/courseware/module_render.py
@@ -181,12 +181,21 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
host=request.get_host(),
proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
)
- xqueue_callback_url += reverse('xqueue_callback',
- kwargs=dict(course_id=course_id,
- userid=str(user.id),
- id=descriptor.location.url(),
- dispatch='score_update'),
- )
+
+ def make_xqueue_callback(dispatch='score_update'):
+ # Fully qualified callback URL for external queueing system
+ xqueue_callback_url = '{proto}://{host}'.format(
+ host=request.get_host(),
+ proto=request.META.get('HTTP_X_FORWARDED_PROTO', 'https' if request.is_secure() else 'http')
+ )
+
+ xqueue_callback_url += reverse('xqueue_callback',
+ kwargs=dict(course_id=course_id,
+ userid=str(user.id),
+ id=descriptor.location.url(),
+ dispatch=dispatch),
+ )
+ return xqueue_callback_url
# Default queuename is course-specific and is derived from the course that
# contains the current module.
@@ -194,7 +203,7 @@ def get_module_for_descriptor(user, request, descriptor, model_data_cache, cours
xqueue_default_queuename = descriptor.location.org + '-' + descriptor.location.course
xqueue = {'interface': xqueue_interface,
- 'callback_url': xqueue_callback_url,
+ 'construct_callback': make_xqueue_callback,
'default_queuename': xqueue_default_queuename.replace(' ', '_'),
'waittime': settings.XQUEUE_WAITTIME_BETWEEN_REQUESTS
}