Commit 5478525a by jmclaus

Added JSInput template to Studio and a sop attribute to JSInput. Only if it's…

Added JSInput template to Studio and a sop attribute to JSInput. Only if it's set to false, as in JSInput's template, will JSChannel be used to bypass it. In all other cases (attribute not present or set to something else), SOP is enforced. Compatibility with jsVGL is therefore maintained. Multiple JSInput are supported in a vertical. Also, save button now functions. [BLD-523, BLD-556, BLD-568]
parent c0c895cb
......@@ -456,11 +456,9 @@ 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
Inputtype for general javascript inputs. Intended to be used with
customresponse.
Loads in a sandboxed iframe to help prevent css and js conflicts between
Loads in a sandboxed iframe to help prevent css and js conflicts between
frame and top-level window.
iframe sandbox whitelist:
......@@ -478,7 +476,8 @@ class JSInput(InputTypeBase):
height="500"
width="400"/>
See the documentation in the /doc/public folder for more information.
See the documentation in docs/data/source/course_data_formats/jsinput.rst
for more information.
"""
template = "jsinput.html"
......@@ -498,12 +497,16 @@ class JSInput(InputTypeBase):
Attribute('set_statefn', None), # Function to call iframe to
# set state
Attribute('width', "400"), # iframe width
Attribute('height', "300") # iframe height
Attribute('height', "300"), # iframe height
Attribute('sop', None) # SOP will be relaxed only if this
# attribute is set to false.
]
def _extra_context(self):
context = {
'applet_loader': '{static_url}js/capa/src/jsinput.js'.format(
'jschannel_loader': '{static_url}js/capa/src/jschannel.js'.format(
static_url=self.system.STATIC_URL),
'jsinput_loader': '{static_url}js/capa/src/jsinput.js'.format(
static_url=self.system.STATIC_URL),
'saved_state': self.value
}
......
......@@ -9,10 +9,14 @@
% if set_statefn:
data-setstate="${set_statefn}"
% endif
% if sop:
data-sop="${sop}"
% endif
data-processed="false"
>
<div class="script_placeholder" data-src="${applet_loader}"/>
<div class="script_placeholder" data-src="${jschannel_loader}"/>
<div class="script_placeholder" data-src="${jsinput_loader}"/>
% if status == 'unsubmitted':
<div class="unanswered" id="status_${id}">
% elif status == 'correct':
......
......@@ -151,7 +151,7 @@ class @Problem
# 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
# check_save_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.
......@@ -159,18 +159,23 @@ class @Problem
# 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: =>
check_save_waitfor: (callback) =>
for inp in @inputs
if ($(inp).is("input[waitfor]"))
try
$(inp).data("waitfor")()
@refreshAnswers()
$(inp).data("waitfor")(() =>
@refreshAnswers()
callback()
)
catch e
if e.name == "Waitfor Exception"
alert e.message
else
alert "Could not grade your answer. The submission was aborted."
throw e
return true
else
return false
###
......@@ -254,7 +259,10 @@ class @Problem
$.ajaxWithPrefix("#{@url}/problem_check", settings)
check: =>
@check_waitfor()
if not @check_save_waitfor(@check_internal)
@check_internal()
check_internal: =>
Logger.log 'problem_check', @answers
# Segment.io
......@@ -334,6 +342,10 @@ class @Problem
@el.find('.capa_alert').css(opacity: 0).animate(opacity: 1, 700)
save: =>
if not @check_save_waitfor(@save_internal)
@save_internal()
save_internal: =>
Logger.log 'problem_save', @answers
$.postWithPrefix "#{@url}/problem_save", @answers, (response) =>
saveMessage = response.msg
......
---
metadata:
display_name: Custom Javascript Display and Grading
markdown: !!null
data: |
<problem display_name="webGLDemo">
<script type="loncapa/python">
import json
def vglcfn(e, ans):
'''
par is a dictionary containing two keys, "answer" and "state"
The value of answer is the JSON string returned by getGrade
The value of state is the JSON string returned by getState
'''
par = json.loads(ans)
# We can use either the value of the answer key to grade
answer = json.loads(par["answer"])
return answer["cylinder"] and not answer["cube"]
'''
# Or we could use the value of the state key
state = json.loads(par["state"])
selectedObjects = state["selectedObjects"]
return selectedObjects["cylinder"] and not selectedObjects["cube"]
'''
</script>
<p>
The shapes below can be selected (yellow) or unselected (cyan).
Clicking on them repeatedly will cycle through these two states.
</p>
<p>
If the cone is selected (and not the cube), a correct answer will be
generated after pressing "Check". Clicking on either "Check" or "Save"
will register the current state.
</p>
<customresponse cfn="vglcfn">
<jsinput gradefn="WebGLDemo.getGrade"
get_statefn="WebGLDemo.getState"
set_statefn="WebGLDemo.setState"
width="400"
height="400"
html_file="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html"
sop="false"/>
</customresponse>
</problem>
\ No newline at end of file
<section
id="inputtype_1"
data="getGrade"
data-stored="{&quot;answer&quot;:&quot;{\&quot;cylinder\&quot;:true,\&quot;cube\&quot;:true}&quot;,&quot;state&quot;:&quot;{\&quot;selectedObjects\&quot;:{\&quot;cylinder\&quot;:true,\&quot;cube\&quot;:true}}&quot;}"
data-getstate="getState"
data-setstate="setState"
data-processed="false"
data-sop="false"
class="jsinput">
<div class="script_placeholder"/>
<iframe
name="iframe_1"
sandbox="allow-scripts
allow-popups
allow-same-origin
allow-forms
allow-pointer-lock"
height="500"
width="500"
src="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html">
</iframe>
<input type="hidden" name="input_1" id="input_1" waitfor value="{&quot;answer&quot;:&quot;{\&quot;cylinder\&quot;:true,\&quot;cube\&quot;:true}&quot;,&quot;state&quot;:&quot;{\&quot;selectedObjects\&quot;:{\&quot;cylinder\&quot;:true,\&quot;cube\&quot;:true}}&quot;}">
</div>
</section>
<section
id="inputtype_2"
data="getGrade"
data-stored="{&quot;answer&quot;:&quot;{\&quot;cylinder\&quot;:true,\&quot;cube\&quot;:true}&quot;,&quot;state&quot;:&quot;{\&quot;selectedObjects\&quot;:{\&quot;cylinder\&quot;:true,\&quot;cube\&quot;:true}}&quot;}"
data-getstate="getState"
data-setstate="setState"
data-processed="false"
data-sop="false"
class="jsinput">
<div class="script_placeholder"/>
<iframe
name="iframe_2"
sandbox="allow-scripts
allow-popups
allow-same-origin
allow-forms
allow-pointer-lock"
height="500"
width="500"
src="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html">
</iframe>
<input type="hidden" name="input_2" id="input_1" waitfor value="{&quot;answer&quot;:&quot;{\&quot;cylinder\&quot;:true,\&quot;cube\&quot;:true}&quot;,&quot;state&quot;:&quot;{\&quot;selectedObjects\&quot;:{\&quot;cylinder\&quot;:true,\&quot;cube\&quot;:true}}&quot;}">
</div>
</section>
\ No newline at end of file
xdescribe("A jsinput has:", function () {
describe("JSInput", function() {
beforeEach(function () {
$('#fixture').remove();
$.ajax({
async: false,
url: 'mainfixture.html',
success: function(data) {
$('body').append($(data));
}
});
loadFixtures('js/capa/fixtures/jsinput.html');
});
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();
});
it('sets all data-processed attributes to true on first load', function() {
var sections = $(document).find('section[id="inputtype_"]');
JSInput.walkDOM();
sections.each(function(index, section) {
expect(section.attr('data-processed')).toEqual('true');
});
});
it('sets the data-processed attribute to true on subsequent load', function() {
var section1 = $(document).find('section[id="inputtype_1"]'),
section2 = $(document).find('section[id="inputtype_2"]');
section1.attr('data-processed', false);
JSInput.walkDOM();
expect(section1.attr('data-processed')).toEqual('true');
expect(section2.attr('data-processed')).toEqual('true');
});
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);
}
it('sets the waitfor attribute to its update function', function() {
var inputFields = $(document).find('input[id="input_"]');
JSInput.walkDOM();
inputFields.each(function(index, inputField) {
expect(inputField.data('waitfor')).toBeDefined();
});
});
})
});
<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>
......@@ -42,11 +42,13 @@ lib_paths:
src_paths:
- coffee/src
- js/src
- js/capa/src
# Paths to spec (test) JavaScript files
spec_paths:
- coffee/spec
- js/spec
- js/capa/spec
# Regular expressions used to exclude *.js files from
# appearing in the test runner page.
......@@ -72,4 +74,5 @@ spec_paths:
# plus the path to the file (relative to this YAML file)
fixture_paths:
- js/fixtures
- js/capa/fixtures
##############################################################################
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
......
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