Merge pull request #217 from edx/feature/jkarni/jsinput
Feature/jkarni/jsinput
This commit is contained in:
@@ -451,6 +451,68 @@ class JavascriptInput(InputTypeBase):
|
||||
registry.register(JavascriptInput)
|
||||
|
||||
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class JSInput(InputTypeBase):
|
||||
"""
|
||||
DO NOT USE! HAS NOT BEEN TESTED BEYOND 700X PROBLEMS, AND MAY CHANGE IN
|
||||
BACKWARDS-INCOMPATIBLE WAYS.
|
||||
Inputtype for general javascript inputs. Intended to be used with
|
||||
customresponse.
|
||||
Loads in a sandboxed iframe to help prevent css and js conflicts between
|
||||
frame and top-level window.
|
||||
|
||||
iframe sandbox whitelist:
|
||||
- allow-scripts
|
||||
- allow-popups
|
||||
- allow-forms
|
||||
- allow-pointer-lock
|
||||
|
||||
This in turn means that the iframe cannot directly access the top-level
|
||||
window elements.
|
||||
Example:
|
||||
|
||||
<jsinput html_file="/static/test.html"
|
||||
gradefn="grade"
|
||||
height="500"
|
||||
width="400"/>
|
||||
|
||||
See the documentation in the /doc/public folder for more information.
|
||||
"""
|
||||
|
||||
template = "jsinput.html"
|
||||
tags = ['jsinput']
|
||||
|
||||
@classmethod
|
||||
def get_attributes(cls):
|
||||
"""
|
||||
Register the attributes.
|
||||
"""
|
||||
return [Attribute('params', None), # extra iframe params
|
||||
Attribute('html_file', None),
|
||||
Attribute('gradefn', "gradefn"),
|
||||
Attribute('get_statefn', None), # Function to call in iframe
|
||||
# to get current state.
|
||||
Attribute('set_statefn', None), # Function to call iframe to
|
||||
# set state
|
||||
Attribute('width', "400"), # iframe width
|
||||
Attribute('height', "300")] # iframe height
|
||||
|
||||
|
||||
|
||||
def _extra_context(self):
|
||||
context = {
|
||||
'applet_loader': '/static/js/capa/src/jsinput.js',
|
||||
'saved_state': self.value
|
||||
}
|
||||
|
||||
return context
|
||||
|
||||
|
||||
|
||||
registry.register(JSInput)
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
class TextLine(InputTypeBase):
|
||||
|
||||
@@ -935,7 +935,7 @@ class CustomResponse(LoncapaResponse):
|
||||
'chemicalequationinput', 'vsepr_input',
|
||||
'drag_and_drop_input', 'editamoleculeinput',
|
||||
'designprotein2dinput', 'editageneinput',
|
||||
'annotationinput']
|
||||
'annotationinput', 'jsinput']
|
||||
|
||||
def setup_response(self):
|
||||
xml = self.xml
|
||||
|
||||
64
common/lib/capa/capa/templates/jsinput.html
Normal file
64
common/lib/capa/capa/templates/jsinput.html
Normal file
@@ -0,0 +1,64 @@
|
||||
<section id="inputtype_${id}" class="jsinput"
|
||||
data="${gradefn}"
|
||||
% if saved_state:
|
||||
data-stored="${saved_state|x}"
|
||||
% endif
|
||||
% if get_statefn:
|
||||
data-getstate="${get_statefn}"
|
||||
% endif
|
||||
% if set_statefn:
|
||||
data-setstate="${set_statefn}"
|
||||
% endif
|
||||
>
|
||||
|
||||
|
||||
<div class="script_placeholder" data-src="${applet_loader}"/>
|
||||
% if status == 'unsubmitted':
|
||||
<div class="unanswered" id="status_${id}">
|
||||
% elif status == 'correct':
|
||||
<div class="correct" id="status_${id}">
|
||||
% elif status == 'incorrect':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% elif status == 'incomplete':
|
||||
<div class="incorrect" id="status_${id}">
|
||||
% endif
|
||||
|
||||
<iframe name="iframe_${id}"
|
||||
id="iframe_${id}"
|
||||
sandbox="allow-scripts allow-popups allow-same-origin allow-forms allow-pointer-lock"
|
||||
seamless="seamless"
|
||||
frameborder="0"
|
||||
src="${html_file}"
|
||||
height="${height}"
|
||||
width="${width}"
|
||||
/>
|
||||
<input type="hidden" name="input_${id}" id="input_${id}"
|
||||
waitfor=""
|
||||
value="${value|h}"/>
|
||||
|
||||
<br/>
|
||||
<p id="answer_${id}" class="answer"></p>
|
||||
|
||||
<p class="status">
|
||||
% if status == 'unsubmitted':
|
||||
unanswered
|
||||
% elif status == 'correct':
|
||||
correct
|
||||
% elif status == 'incorrect':
|
||||
incorrect
|
||||
% elif status == 'incomplete':
|
||||
incomplete
|
||||
% endif
|
||||
</p>
|
||||
<br/> <br/>
|
||||
|
||||
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
|
||||
|
||||
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
|
||||
</div>
|
||||
% endif
|
||||
|
||||
% if msg:
|
||||
<span class="message">${msg|n}</span>
|
||||
% endif
|
||||
</section>
|
||||
@@ -16,8 +16,16 @@ h2 {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
iframe[seamless]{
|
||||
background-color: transparent;
|
||||
border: 0px none transparent;
|
||||
padding: 0px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.inline-error {
|
||||
color: darken($error-red, 10%);
|
||||
color: darken($error-red, 11%);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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 ($(inp).is("input[waitfor]"))
|
||||
try
|
||||
$(inp).data("waitfor")()
|
||||
@refreshAnswers()
|
||||
catch e
|
||||
if e.name == "Waitfor Exception"
|
||||
alert e.message
|
||||
else
|
||||
alert "Could not grade your answer. The submission was aborted."
|
||||
throw e
|
||||
|
||||
|
||||
###
|
||||
# 'check_fd' uses FormData to allow file submissions in the 'problem_check' dispatch,
|
||||
@@ -213,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
|
||||
|
||||
0
common/static/css/capa/jsinput_css.css
Normal file
0
common/static/css/capa/jsinput_css.css
Normal file
70
common/static/js/capa/spec/jsinput/jsinput.js
Normal file
70
common/static/js/capa/spec/jsinput/jsinput.js
Normal file
@@ -0,0 +1,70 @@
|
||||
describe("A jsinput has:", function () {
|
||||
|
||||
beforeEach(function () {
|
||||
$('#fixture').remove();
|
||||
$.ajax({
|
||||
async: false,
|
||||
url: 'mainfixture.html',
|
||||
success: function(data) {
|
||||
$('body').append($(data));
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
|
||||
describe("The jsinput constructor", function(){
|
||||
|
||||
var iframe1 = $(document).find('iframe')[0];
|
||||
|
||||
var testJsElem = jsinputConstructor({
|
||||
id: 1,
|
||||
elem: iframe1,
|
||||
passive: false
|
||||
});
|
||||
|
||||
it("Returns an object", function(){
|
||||
expect(typeof(testJsElem)).toEqual('object');
|
||||
});
|
||||
|
||||
it("Adds the object to the jsinput array", function() {
|
||||
expect(jsinput.exists(1)).toBe(true);
|
||||
});
|
||||
|
||||
describe("The returned object", function() {
|
||||
|
||||
it("Has a public 'update' method", function(){
|
||||
expect(testJsElem.update).toBeDefined();
|
||||
});
|
||||
|
||||
it("Returns an 'update' that is idempotent", function(){
|
||||
var orig = testJsElem.update();
|
||||
for (var i = 0; i++; i < 5) {
|
||||
expect(testJsElem.update()).toEqual(orig);
|
||||
}
|
||||
});
|
||||
|
||||
it("Changes the parent's inputfield", function() {
|
||||
testJsElem.update();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("The walkDOM functions", function() {
|
||||
|
||||
walkDOM();
|
||||
|
||||
it("Creates (at least) one object per iframe", function() {
|
||||
jsinput.arr.length >= 2;
|
||||
});
|
||||
|
||||
it("Does not create multiple objects with the same id", function() {
|
||||
while (jsinput.arr.length > 0) {
|
||||
var elem = jsinput.arr.pop();
|
||||
expect(jsinput.exists(elem.id)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
118
common/static/js/capa/spec/jsinput/mainfixture.html
Normal file
118
common/static/js/capa/spec/jsinput/mainfixture.html
Normal file
@@ -0,0 +1,118 @@
|
||||
<html>
|
||||
<head>
|
||||
<title> JSinput jasmine test </title>
|
||||
</head>
|
||||
<body>
|
||||
<section id="inputtype_1"
|
||||
data="gradefn"
|
||||
data-setstate="setinput"
|
||||
class="jsinput">
|
||||
|
||||
<div class="script_placeholder" />
|
||||
<iframe name="iframe_1"
|
||||
|
||||
sandbox="allow-scripts
|
||||
allow-popups
|
||||
allow-same-origin
|
||||
allow-forms
|
||||
allow-pointer-lock"
|
||||
seamless="seamless"
|
||||
height="500"
|
||||
width="500">
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
JS input test 1
|
||||
</title>
|
||||
<script type="text/javascript">
|
||||
function gradefn () {
|
||||
var ans = document.getElementById("one").value;
|
||||
console.log("I've been called!");
|
||||
return ans
|
||||
}
|
||||
|
||||
function setinput(val) {
|
||||
document.getElementById("one").value(val);
|
||||
return;
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Simple js input test. Defines a js function that returns the value in
|
||||
the input field below when called. </p>
|
||||
|
||||
<form>
|
||||
<input id='one' type="TEXT"/>
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</iframe>
|
||||
<input type="hidden" name="input_1" id="input_1" value="${value|h}"/>
|
||||
|
||||
<br/>
|
||||
<button id="update_1" class="update">Update</button>
|
||||
<p id="answer_1" class="answer"></p>
|
||||
|
||||
<p class="status">
|
||||
</p>
|
||||
<br/> <br/>
|
||||
|
||||
<div class="error_message" ></div>
|
||||
|
||||
</section>
|
||||
<section id="inputtype_2" data="gradefn" class="jsinput">
|
||||
|
||||
<div class="script_placeholder" />
|
||||
<iframe name="iframe_2"
|
||||
sandbox="allow-scripts
|
||||
allow-popups
|
||||
allow-same-origin
|
||||
allow-forms
|
||||
allow-pointer-lock"
|
||||
seamless="seamless"
|
||||
height="500"
|
||||
width="500" >
|
||||
<html>
|
||||
<head>
|
||||
<title>
|
||||
JS input test
|
||||
</title>
|
||||
<script type="text/javascript">
|
||||
function gradefn () {
|
||||
var ans = document.getElementById("one").value;
|
||||
console.log("I've been called!");
|
||||
return ans
|
||||
}
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p>Simple js input test. Defines a js function that returns the value in
|
||||
the input field below when called. </p>
|
||||
|
||||
<form>
|
||||
<input id='two' type="TEXT"/>
|
||||
</form>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
</iframe>
|
||||
<input type="hidden" name="input_2" id="input_2" value="${value|h}"/>
|
||||
|
||||
<br/>
|
||||
<button id="update_2" class="update">Update</button>
|
||||
<p id="answer_2" class="answer"></p>
|
||||
|
||||
<p class="status">
|
||||
</p>
|
||||
<br/> <br/>
|
||||
|
||||
<div class="error_message"></div>
|
||||
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
199
common/static/js/capa/src/jsinput.js
Normal file
199
common/static/js/capa/src/jsinput.js
Normal file
@@ -0,0 +1,199 @@
|
||||
(function (jsinput, undefined) {
|
||||
// Initialize js inputs on current page.
|
||||
// N.B.: No library assumptions about the iframe can be made (including,
|
||||
// most relevantly, jquery). Keep in mind what happens in which context
|
||||
// when modifying this file.
|
||||
|
||||
/* Check whether there is anything to be done */
|
||||
|
||||
// When all the problems are first loaded, we want to make sure the
|
||||
// constructor only runs once for each iframe; but we also want to make
|
||||
// sure that if part of the page is reloaded (e.g., a problem is
|
||||
// submitted), the constructor is called again.
|
||||
|
||||
if (!jsinput) {
|
||||
jsinput = {
|
||||
runs : 1,
|
||||
arr : [],
|
||||
exists : function(id) {
|
||||
jsinput.arr.filter(function(e, i, a) {
|
||||
return e.id = id;
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
jsinput.runs++;
|
||||
|
||||
|
||||
/* Utils */
|
||||
|
||||
|
||||
// Take a string and find the nested object that corresponds to it. E.g.:
|
||||
// deepKey(obj, "an.example") -> obj["an"]["example"]
|
||||
var _deepKey = function(obj, path){
|
||||
for (var i = 0, p=path.split('.'), len = p.length; i < len; i++){
|
||||
obj = obj[p[i]];
|
||||
}
|
||||
return obj;
|
||||
};
|
||||
|
||||
|
||||
/* END Utils */
|
||||
|
||||
|
||||
|
||||
|
||||
function jsinputConstructor(spec) {
|
||||
// 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
|
||||
// public method, "update", which updates the hidden input field.
|
||||
var that = {};
|
||||
|
||||
/* Private methods */
|
||||
|
||||
var sect = $(spec.elem).parent().find('section[class="jsinput"]');
|
||||
var sectAttr = function (e) { return $(sect).attr(e); };
|
||||
var thisIFrame = $(spec.elem).
|
||||
find('iframe[name^="iframe_"]').
|
||||
get(0);
|
||||
var cWindow = thisIFrame.contentWindow;
|
||||
|
||||
// Get the hidden input field to pass to customresponse
|
||||
function _inputField() {
|
||||
var parent = $(spec.elem).parent();
|
||||
return parent.find('input[id^="input_"]');
|
||||
}
|
||||
var inputField = _inputField();
|
||||
|
||||
// Get the grade function name
|
||||
var getGradeFn = sectAttr("data");
|
||||
// Get state getter
|
||||
var getStateGetter = sectAttr("data-getstate");
|
||||
// Get state setter
|
||||
var getStateSetter = sectAttr("data-setstate");
|
||||
// Get stored state
|
||||
var getStoredState = sectAttr("data-stored");
|
||||
|
||||
|
||||
|
||||
// Put the return value of gradeFn in the hidden inputField.
|
||||
var update = function () {
|
||||
var ans;
|
||||
|
||||
ans = _deepKey(cWindow, gradeFn)();
|
||||
// setstate presumes getstate, so don't getstate unless setstate is
|
||||
// defined.
|
||||
if (getStateGetter && getStateSetter) {
|
||||
var state, store;
|
||||
state = unescape(_deepKey(cWindow, getStateGetter)());
|
||||
store = {
|
||||
answer: ans,
|
||||
state: state
|
||||
};
|
||||
inputField.val(JSON.stringify(store));
|
||||
} else {
|
||||
inputField.val(ans);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
/* Public methods */
|
||||
|
||||
that.update = update;
|
||||
|
||||
|
||||
|
||||
/* Initialization */
|
||||
|
||||
jsinput.arr.push(that);
|
||||
|
||||
// Put the update function as the value of the inputField's "waitfor"
|
||||
// attribute so that it is called when the check button is clicked.
|
||||
function bindCheck() {
|
||||
inputField.data('waitfor', that.update);
|
||||
return;
|
||||
}
|
||||
|
||||
var gradeFn = getGradeFn;
|
||||
|
||||
|
||||
bindCheck();
|
||||
|
||||
// Check whether application takes in state and there is a saved
|
||||
// state to give it. If getStateSetter is specified but calling it
|
||||
// fails, wait and try again, since the iframe might still be
|
||||
// loading.
|
||||
if (getStateSetter && getStoredState) {
|
||||
var sval, jsonVal;
|
||||
|
||||
try {
|
||||
jsonVal = JSON.parse(getStoredState);
|
||||
} catch (err) {
|
||||
jsonVal = getStoredState;
|
||||
}
|
||||
|
||||
if (typeof(jsonVal) === "object") {
|
||||
sval = jsonVal["state"];
|
||||
} else {
|
||||
sval = jsonVal;
|
||||
}
|
||||
|
||||
|
||||
// Try calling setstate every 200ms while it throws an exception,
|
||||
// up to five times; give up after that.
|
||||
// (Functions in the iframe may not be ready when we first try
|
||||
// calling it, but might just need more time. Give the functions
|
||||
// more time.)
|
||||
function whileloop(n) {
|
||||
if (n < 5){
|
||||
try {
|
||||
_deepKey(cWindow, getStateSetter)(sval);
|
||||
} catch (err) {
|
||||
setTimeout(whileloop(n+1), 200);
|
||||
}
|
||||
}
|
||||
else {
|
||||
console.debug("Error: could not set state");
|
||||
}
|
||||
}
|
||||
whileloop(0);
|
||||
|
||||
}
|
||||
|
||||
|
||||
return that;
|
||||
}
|
||||
|
||||
|
||||
function walkDOM() {
|
||||
var newid;
|
||||
|
||||
// Find all jsinput elements, and create a jsinput object for each one
|
||||
var all = $(document).find('section[class="jsinput"]');
|
||||
|
||||
all.each(function(index, value) {
|
||||
// Get just the mako variable 'id' from the id attribute
|
||||
newid = $(value).attr("id").replace(/^inputtype_/, "");
|
||||
|
||||
|
||||
if (!jsinput.exists(newid)){
|
||||
var newJsElem = jsinputConstructor({
|
||||
id: newid,
|
||||
elem: value,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// This is ugly, but without a timeout pages with multiple/heavy jsinputs
|
||||
// don't load properly.
|
||||
if ($.isReady) {
|
||||
setTimeout(walkDOM, 300);
|
||||
} else {
|
||||
$(document).ready(setTimeout(walkDOM, 300));
|
||||
}
|
||||
|
||||
})(window.jsinput = window.jsinput || false);
|
||||
151
doc/public/course_data_formats/jsinput.rst
Normal file
151
doc/public/course_data_formats/jsinput.rst
Normal file
@@ -0,0 +1,151 @@
|
||||
##############################################################################
|
||||
JS Input
|
||||
##############################################################################
|
||||
|
||||
**NOTE**
|
||||
*Do not use this feature yet! Its attributes and behaviors may change
|
||||
without any concern for backwards compatibility. Moreover, it has only been
|
||||
tested in a very limited context. If you absolutely must, contact Julian
|
||||
(julian@edx.org). When the feature stabilizes, this note will be removed.*
|
||||
|
||||
This document explains how to write a JSInput input type. JSInput is meant to
|
||||
allow problem authors to easily turn working standalone HTML files into
|
||||
problems that can be integrated into the edX platform. Since it's aim is
|
||||
flexibility, it can be seen as the input and client-side equivalent of
|
||||
CustomResponse.
|
||||
|
||||
A JSInput input creates an iframe into a static HTML page, and passes the
|
||||
return value of author-specified functions to the enclosing response type
|
||||
(generally CustomResponse). JSInput can also stored and retrieve state.
|
||||
|
||||
******************************************************************************
|
||||
Format
|
||||
******************************************************************************
|
||||
|
||||
A jsinput problem looks like this:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<problem>
|
||||
<script type="loncapa/python">
|
||||
def all_true(exp, ans): return ans == "hi"
|
||||
</script>
|
||||
<customresponse cfn="all_true">
|
||||
<jsinput gradefn="gradefn"
|
||||
height="500"
|
||||
get_statefn="getstate"
|
||||
set_statefn="setstate"
|
||||
html_file="/static/jsinput.html"/>
|
||||
</customresponse>
|
||||
</problem>
|
||||
|
||||
The accepted attributes are:
|
||||
|
||||
============== ============== ========= ==========
|
||||
Attribute Name Value Type Required? Default
|
||||
============== ============== ========= ==========
|
||||
html_file Url string Yes None
|
||||
gradefn Function name Yes `gradefn`
|
||||
set_statefn Function name No None
|
||||
get_statefn Function name No None
|
||||
height Integer No `500`
|
||||
width Integer No `400`
|
||||
============== ============== ========= ==========
|
||||
|
||||
******************************************************************************
|
||||
Required Attributes
|
||||
******************************************************************************
|
||||
|
||||
==============================================================================
|
||||
html_file
|
||||
==============================================================================
|
||||
|
||||
The `html_file` attribute specifies what html file the iframe will point to. This
|
||||
should be located in the content directory.
|
||||
|
||||
The iframe is created using the sandbox attribute; while popups, scripts, and
|
||||
pointer locks are allowed, the iframe cannot access its parent's attributes.
|
||||
|
||||
The html file should contain an accesible gradefn function. To check whether
|
||||
the gradefn will be accessible to JSInput, check that, in the console,::
|
||||
"`gradefn"
|
||||
Returns the right thing. When used by JSInput, `gradefn` is called with::
|
||||
`gradefn`.call(`obj`)
|
||||
Where `obj` is the object-part of `gradefn`. For example, if `gradefn` is
|
||||
`myprog.myfn`, JSInput will call `myprog.myfun.call(myprog)`. (This is to
|
||||
ensure "`this`" continues to refer to what `gradefn` expects.)
|
||||
|
||||
Aside from that, more or less anything goes. Note that currently there is no
|
||||
support for inheriting css or javascript from the parent (aside from the
|
||||
Chrome-only `seamless` attribute, which is set to true by default).
|
||||
|
||||
==============================================================================
|
||||
gradefn
|
||||
==============================================================================
|
||||
|
||||
The `gradefn` attribute specifies the name of the function that will be called
|
||||
when a user clicks on the "Check" button, and which should return the student's
|
||||
answer. This answer will (unless both the get_statefn and set_statefn
|
||||
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
|
||||
has no indication that her answer is being calculated/produced.
|
||||
|
||||
******************************************************************************
|
||||
Option Attributes
|
||||
******************************************************************************
|
||||
|
||||
The `height` and `width` attributes are straightforward: they specify the
|
||||
height and width of the iframe. Both are limited by the enclosing DOM elements,
|
||||
so for instance there is an implicit max-width of around 900.
|
||||
|
||||
In the future, JSInput may attempt to make these dimensions match the html
|
||||
file's dimensions (up to the aforementioned limits), but currently it defaults
|
||||
to `500` and `400` for `height` and `width`, respectively.
|
||||
|
||||
==============================================================================
|
||||
set_statefn
|
||||
==============================================================================
|
||||
|
||||
Sometimes a problem author will want information about a student's previous
|
||||
answers ("state") to be saved and reloaded. If the attribute `set_statefn` is
|
||||
used, the function given as its value will be passed the state as a string
|
||||
argument whenever there is a state, and the student returns to a problem. It is
|
||||
the responsibility of the function to then use this state approriately.
|
||||
|
||||
The state that is passed is:
|
||||
|
||||
1. The previous output of `gradefn` (i.e., the previous answer) if
|
||||
`get_statefn` is not defined.
|
||||
2. The previous output of `get_statefn` (see below) otherwise.
|
||||
|
||||
It is the responsibility of the iframe to do proper verification of the
|
||||
argument that it receives via `set_statefn`.
|
||||
|
||||
==============================================================================
|
||||
get_statefn
|
||||
==============================================================================
|
||||
|
||||
Sometimes the state and the answer are quite different. For instance, a problem
|
||||
that involves using a javascript program that allows the student to alter a
|
||||
molecule may grade based on the molecule's hidrophobicity, but from the
|
||||
hidrophobicity it might be incapable of restoring the state. In that case, a
|
||||
*separate* state may be stored and loaded by `set_statefn`. Note that if
|
||||
`get_statefn` is defined, the answer (i.e., what is passed to the enclosing
|
||||
response type) will be a json string with the following format::
|
||||
{
|
||||
answer: `[answer string]`
|
||||
state: `[state string]`
|
||||
}
|
||||
|
||||
It is the responsibility of the enclosing response type to then parse this as
|
||||
json.
|
||||
@@ -29,6 +29,7 @@ Specific Problem Types
|
||||
course_data_formats/word_cloud/word_cloud.rst
|
||||
course_data_formats/custom_response.rst
|
||||
course_data_formats/symbolic_response.rst
|
||||
course_data_formats/jsinput.rst
|
||||
|
||||
|
||||
Internal Data Formats
|
||||
|
||||
Reference in New Issue
Block a user