Commit 5ba583ed by Your Name

Merge branch 'master' of github.com:MITx/mitx

parents 6d9e5ef6 8c1b7a71
......@@ -25,11 +25,14 @@ CMS.Views.ValidatingView = Backbone.View.extend({
for (var field in error) {
var ele = this.$el.find('#' + this.fieldToSelectorMap[field]);
this._cacheValidationErrors.push(ele);
if ($(ele).is('div')) {
var inputElements = 'input, textarea';
if ($(ele).is(inputElements)) {
$(ele).addClass('error');
}
else {
// put error on the contained inputs
$(ele).find('input, textarea').addClass('error');
$(ele).find(inputElements).addClass('error');
}
else $(ele).addClass('error');
$(ele).parent().append(this.errorTemplate({message : error[field]}));
}
},
......
......@@ -16,7 +16,6 @@ This is used by capa_module.
from __future__ import division
from datetime import datetime
import json
import logging
import math
import numpy
......@@ -32,8 +31,6 @@ from xml.sax.saxutils import unescape
from copy import deepcopy
import chem
import chem.chemcalc
import chem.chemtools
import chem.miller
import verifiers
import verifiers.draganddrop
......@@ -70,9 +67,6 @@ global_context = {'random': random,
'scipy': scipy,
'calc': calc,
'eia': eia,
'chemcalc': chem.chemcalc,
'chemtools': chem.chemtools,
'miller': chem.miller,
'draganddrop': verifiers.draganddrop}
# These should be removed from HTML output, including all subelements
......@@ -97,8 +91,13 @@ class LoncapaProblem(object):
- problem_text (string): xml defining the problem
- id (string): identifier for this problem; often a filename (no spaces)
- state (dict): student state
- seed (int): random number generator seed (int)
- seed (int): random number generator seed (int)
- state (dict): containing the following keys:
- 'seed' - (int) random number generator seed
- 'student_answers' - (dict) maps input id to the stored answer for that input
- 'correct_map' (CorrectMap) a map of each input to their 'correctness'
- 'done' - (bool) indicates whether or not this problem is considered done
- 'input_state' - (dict) maps input_id to a dictionary that holds the state for that input
- system (ModuleSystem): ModuleSystem instance which provides OS,
rendering, and user context
......@@ -110,21 +109,23 @@ class LoncapaProblem(object):
self.system = system
if self.system is None:
raise Exception()
self.seed = seed
if state:
if 'seed' in state:
self.seed = state['seed']
if 'student_answers' in state:
self.student_answers = state['student_answers']
if 'correct_map' in state:
self.correct_map.set_dict(state['correct_map'])
if 'done' in state:
self.done = state['done']
# TODO: Does this deplete the Linux entropy pool? Is this fast enough?
if not self.seed:
self.seed = struct.unpack('i', os.urandom(4))[0]
state = state if state else {}
# Set seed according to the following priority:
# 1. Contained in problem's state
# 2. Passed into capa_problem via constructor
# 3. Assign from the OS's random number generator
self.seed = state.get('seed', seed)
if self.seed is None:
self.seed = struct.unpack('i', os.urandom(4))
self.student_answers = state.get('student_answers', {})
if 'correct_map' in state:
self.correct_map.set_dict(state['correct_map'])
self.done = state.get('done', False)
self.input_state = state.get('input_state', {})
# Convert startouttext and endouttext to proper <text></text>
problem_text = re.sub("startouttext\s*/", "text", problem_text)
......@@ -188,6 +189,7 @@ class LoncapaProblem(object):
return {'seed': self.seed,
'student_answers': self.student_answers,
'correct_map': self.correct_map.get_dict(),
'input_state': self.input_state,
'done': self.done}
def get_max_score(self):
......@@ -237,6 +239,20 @@ class LoncapaProblem(object):
self.correct_map.set_dict(cmap.get_dict())
return cmap
def ungraded_response(self, xqueue_msg, queuekey):
'''
Handle any responses from the xqueue that do not contain grades
Will try to pass the queue message to all inputtypes that can handle ungraded responses
Does not return any value
'''
# check against each inputtype
for the_input in self.inputs.values():
# if the input type has an ungraded function, pass in the values
if hasattr(the_input, 'ungraded_response'):
the_input.ungraded_response(xqueue_msg, queuekey)
def is_queued(self):
'''
Returns True if any part of the problem has been submitted to an external queue
......@@ -351,7 +367,7 @@ class LoncapaProblem(object):
dispatch = get['dispatch']
return self.inputs[input_id].handle_ajax(dispatch, get)
else:
log.warning("Could not find matching input for id: %s" % problem_id)
log.warning("Could not find matching input for id: %s" % input_id)
return {}
......@@ -527,11 +543,15 @@ class LoncapaProblem(object):
value = ""
if self.student_answers and problemid in self.student_answers:
value = self.student_answers[problemid]
if input_id not in self.input_state:
self.input_state[input_id] = {}
# do the rendering
state = {'value': value,
'status': status,
'id': input_id,
'input_state': self.input_state[input_id],
'feedback': {'message': msg,
'hint': hint,
'hintmode': hintmode, }}
......
<section id="textbox_${id}" class="textbox">
<textarea rows="${rows}" cols="${cols}" name="input_${id}" id="input_${id}"
% if hidden:
style="display:none;"
% endif
>${value|h}</textarea>
<div class="grader-status">
% if status == 'unsubmitted':
<span class="unanswered" style="display:inline-block;" id="status_${id}">Unanswered</span>
% elif status == 'correct':
<span class="correct" id="status_${id}">Correct</span>
% elif status == 'incorrect':
<span class="incorrect" id="status_${id}">Incorrect</span>
% elif status == 'queued':
<span class="processing" id="status_${id}">Queued</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif
% if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif
<p class="debug">${status}</p>
</div>
<span id="answer_${id}"></span>
<div class="external-grader-message">
${msg|n}
</div>
<div class="external-grader-message">
${queue_msg|n}
</div>
<div class="plot-button">
<input type="button" class="save" name="plot-button" id="plot_${id}" value="Plot" />
</div>
<script>
// Note: We need to make the area follow the CodeMirror for this to work.
$(function(){
var cm = CodeMirror.fromTextArea(document.getElementById("input_${id}"), {
% if linenumbers == 'true':
lineNumbers: true,
% endif
mode: "matlab",
matchBrackets: true,
lineWrapping: true,
indentUnit: "${tabsize}",
tabSize: "${tabsize}",
indentWithTabs: false,
extraKeys: {
"Tab": function(cm) {
cm.replaceSelection("${' '*tabsize}", "end");
}
},
smartIndent: false
});
$("#textbox_${id}").find('.CodeMirror-scroll').height(${int(13.5*eval(rows))});
var gentle_alert = function (parent_elt, msg) {
if($(parent_elt).find('.capa_alert').length) {
$(parent_elt).find('.capa_alert').remove();
}
var alert_elem = "<div>" + msg + "</div>";
alert_elem = $(alert_elem).addClass('capa_alert');
$(parent_elt).find('.action').after(alert_elem);
$(parent_elt).find('.capa_alert').css({opacity: 0}).animate({opacity: 1}, 700);
}
// hook up the plot button
var plot = function(event) {
var problem_elt = $(event.target).closest('.problems-wrapper');
url = $(event.target).closest('.problems-wrapper').data('url');
input_id = "${id}";
// save the codemirror text to the textarea
cm.save();
var input = $("#input_${id}");
// pull out the coded text
submission = input.val();
answer = input.serialize();
// setup callback for after we send information to plot
var plot_callback = function(response) {
if(response.success) {
window.location.reload();
}
else {
gentle_alert(problem_elt, msg);
}
}
var save_callback = function(response) {
if(response.success) {
// send information to the problem's plot functionality
Problem.inputAjax(url, input_id, 'plot',
{'submission': submission}, plot_callback);
}
else {
gentle_alert(problem_elt, msg);
}
}
// save the answer
$.postWithPrefix(url + '/problem_save', answer, save_callback);
}
$('#plot_${id}').click(plot);
});
</script>
</section>
......@@ -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 '<div>{0}</div>'.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'
)
......@@ -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 = """<matlabinput id="prob_1_2"
rows="{r}" cols="{c}"
tabsize="{tabsize}" mode="{m}"
linenumbers="{ln}">
<plot_payload>
{payload}
</plot_payload>
</matlabinput>""".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):
'''
......
......@@ -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.
......
......@@ -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
......
......@@ -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)
......
......@@ -89,7 +89,7 @@ metadata:
{"short_description": "Download the Studio Documentation",
"long_description": "Download the searchable Studio reference documentation in PDF form.",
"is_checked": false,
"action_url": "http://help.edge.edx.org/help/assets/8ccd409f979c8639dd463e126eb840dc67f09098/Getting_Started_with_Studio.pdf",
"action_url": "http://files.edx.org/Getting_Started_with_Studio.pdf",
"action_text": "Download Documentation",
"action_external": true}]
},
......
......@@ -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)
......
......@@ -125,7 +125,7 @@ class IsNewCourseTestCase(unittest.TestCase):
descriptor = self.get_dummy_course(start='2013-01-15T12:00')
assert(descriptor.is_newish is True)
descriptor = self.get_dummy_course(start='2013-03-00T12:00')
descriptor = self.get_dummy_course(start='2013-03-01T12:00')
assert(descriptor.is_newish is True)
descriptor = self.get_dummy_course(start='2012-10-15T12:00')
......
......@@ -12,6 +12,8 @@
* }
*/
/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsme is running inside edX platform.
body, table td, select {
font-family: Arial Unicode MS, Arial, sans-serif;
font-size: small;
......@@ -31,6 +33,7 @@ body {
a, a:visited, a:hover {
color: #0000AA;
}
*/
/**
* The reference theme can be used to determine when this style sheet has
......
......@@ -12,6 +12,8 @@
* }
*/
/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsme is running inside edX platform.
body, table td, select {
font-family: Arial Unicode MS, Arial, sans-serif;
font-size: small;
......@@ -31,6 +33,7 @@ body {
a, a:visited, a:hover {
color: #0000AA;
}
*/
/**
* The reference theme can be used to determine when this style sheet has
......
......@@ -26,6 +26,8 @@
zoom: 1;
}
/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsme is running inside edX platform.
body {
font-family: arial,sans-serif;
}
......@@ -45,6 +47,7 @@ a:visited {
a:active {
color:#ff0000;
}
*/
/*** Button ***/
......
......@@ -26,6 +26,8 @@
zoom: 1;
}
/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsme is running inside edX platform.
body {
font-family: arial,sans-serif;
}
......@@ -45,6 +47,7 @@ a:visited {
a:active {
color:#ff0000;
}
*/
/*** Button ***/
......
......@@ -12,6 +12,8 @@
* }
*/
/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsmolcalc is running inside edX platform.
body, table td, select, button {
font-family: Arial Unicode MS, Arial, sans-serif;
font-size: small;
......@@ -41,6 +43,7 @@ a:hover {
select {
background: white;
}
*/
/**
* The reference theme can be used to determine when this style sheet has
......
......@@ -12,6 +12,8 @@
* }
*/
/* Commented out the following that was messing up the CSS in sequential and had no use anyway when jsmolcalc is running inside edX platform.
body, table td, select, button {
font-family: Arial Unicode MS, Arial, sans-serif;
font-size: small;
......@@ -41,6 +43,7 @@ a:hover {
select {
background: white;
}
*/
/**
* The reference theme can be used to determine when this style sheet has
......
......@@ -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
}
......
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