From 74bb976ef50cf0767e5f61abd056f3ea3a65b619 Mon Sep 17 00:00:00 2001 From: Julian Arni Date: Mon, 24 Jun 2013 13:32:30 -0400 Subject: [PATCH] Abort submission and alter user if gradefn throws an exception --- .../xmodule/js/src/capa/display.coffee | 31 ++++++-- common/static/js/capa/jsinput.js | 76 +++++++++++-------- doc/public/course_data_formats/jsinput.rst | 5 ++ 3 files changed, 77 insertions(+), 35 deletions(-) diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index 69e4551b6e..fc4c750b52 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -129,6 +129,30 @@ class @Problem if setupMethod? @inputtypeDisplays[id] = setupMethod(inputtype) + # If some function wants to be called before sending the answer to the + # server, give it a chance to do so. + # + # check_waitfor allows the callee to send alerts if the user's input is + # invalid. To do so, the callee must throw an exception named "Waitfor + # Exception". This and any other errors or exceptions that arise from the + # callee are rethrown and abort the submission. + # + # In order to use this feature, add a 'data-waitfor' attribute to the input, + # and specify the function to be called by the check button before sending + # off @answers + check_waitfor: => + for inp in @inputs + if not ($(inp).attr("data-waitfor")?) + try + $(inp).data("waitfor")() + catch e + if e.name == "Waitfor Exception" + alert e.message + else + alert "Could not grade your answer. The submission was aborted." + throw e + @refreshAnswers() + ### # 'check_fd' uses FormData to allow file submissions in the 'problem_check' dispatch, @@ -140,11 +164,7 @@ class @Problem check_fd: => Logger.log 'problem_check', @answers - # If some function wants to be called before sending the answer to the - # server, give it a chance to do so. - if $('input[waitfor]').length != 0 - ($(lcall).data("waitfor").call() for lcall in $('input[waitfor]')) - @refreshAnswers() + # If there are no file inputs in the problem, we can fall back on @check if $('input:file').length == 0 @check() @@ -217,6 +237,7 @@ class @Problem $.ajaxWithPrefix("#{@url}/problem_check", settings) check: => + @check_waitfor() Logger.log 'problem_check', @answers $.postWithPrefix "#{@url}/problem_check", @answers, (response) => switch response.success diff --git a/common/static/js/capa/jsinput.js b/common/static/js/capa/jsinput.js index ff6a8aa68b..5eb1d3e360 100644 --- a/common/static/js/capa/jsinput.js +++ b/common/static/js/capa/jsinput.js @@ -10,16 +10,15 @@ // Use this array to keep track of the elements that have already been // initialized. jsinput.jsinputarr = jsinput.jsinputarr || []; - if (isFirst) { - jsinput.jsinputarr.exists = function (id) { - this.filter(function(e, i, a) { - return e.id = id; - }); - }; - } + jsinput.jsinputarr.exists = function (id) { + this.filter(function(e, i, a) { + return e.id = id; + }); + }; + function jsinputConstructor(spec) { - // Define an class that will be instantiated for each.jsinput element + // Define an class that will be instantiated for each jsinput element // of the DOM // 'that' is the object returned by the constructor. It has a single @@ -35,6 +34,11 @@ return parent.find('input[id^="input_"]'); } + // For the state and grade functions below, use functions instead of + // storing their return values since we might need to call them + // repeatedly, and they might change (e.g., they might not be defined + // when we first try calling them). + // Get the grade function name function getgradefn() { return $(sect).attr("data"); @@ -54,24 +58,24 @@ return $(sect).attr("data-stored"); } + var thisIFrame = $(spec.elem). + find('iframe[name^="iframe_"]'). + get(0); + + var cWindow = thisIFrame.contentWindow; + // Put the return value of gradefn in the hidden inputfield. // If passed an argument, does not call gradefn, and instead directly // updates the inputfield with the passed value. var update = function (answer) { var ans; - ans = $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). // jquery might not be available in the iframe - contentWindow[gradefn](); + ans = cWindow[gradefn](); // setstate presumes getstate, so don't getstate unless setstate is // defined. if (getgetstate() && getsetstate()) { var state, store; - state = $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). - contentWindow[getgetstate()](); + state = cWindow[getgetstate()](); store = { answer: ans, state: state @@ -91,8 +95,6 @@ $(updatebutton).click(update); } - - /* Public methods */ that.update = update; @@ -116,19 +118,30 @@ updateHandler(); bindCheck(); // Check whether application takes in state and there is a saved - // state to give it + // state to give it. If getsetstate is specified but calling it + // fails, wait and try again, since the iframe might still be + // loading. if (getsetstate() && getstoredstate()) { - console.log("Using stored state..."); var sval; if (typeof(getstoredstate()) === "object") { sval = getstoredstate()["state"]; } else { sval = getstoredstate(); } - $(spec.elem). - find('iframe[name^="iframe_"]'). - get(0). - contentWindow[getsetstate()](sval); + function whileloop(n) { + if (n < 10){ + try { + cWindow[getsetstate()](sval); + } catch (err) { + setTimeout(whileloop(n+1), 200); + } + } + else { + console.log("Error: could not set state"); + } + } + whileloop(0); + } } else { // NOT CURRENTLY SUPPORTED @@ -171,11 +184,13 @@ all.each(function() { // Get just the mako variable 'id' from the id attribute newid = $(this).attr("id").replace(/^inputtype_/, ""); - var newJsElem = jsinputConstructor({ - id: newid, - elem: this, - passive: false - }); + if (! jsinput.jsinputarr.exists(newid)){ + var newJsElem = jsinputConstructor({ + id: newid, + elem: this, + passive: false + }); + } }); } @@ -193,5 +208,6 @@ //} //}; - setTimeout(walkDOM, 200); + + setTimeout(walkDOM, 100); })(window.jsinput = window.jsinput || {}) diff --git a/doc/public/course_data_formats/jsinput.rst b/doc/public/course_data_formats/jsinput.rst index 5252a5dd0c..008940e3b7 100644 --- a/doc/public/course_data_formats/jsinput.rst +++ b/doc/public/course_data_formats/jsinput.rst @@ -87,6 +87,11 @@ attributes are also used) be passed as a string to the enclosing response type. In the customresponse example above, this means cfn will be passed this answer as `ans`. +If the `gradefn` function throws an exception when a student attempts to +submit a problem, the submission is aborted, and the student receives a generic +alert. The alert can be customised by making the exception name `Waitfor +Exception`; in that case, the alert message will be the exception message. + **IMPORTANT** : the `gradefn` function should not be at all asynchronous, since this could result in the student's latest answer not being passed correctly. Moreover, the function should also return promptly, since currently the student