Commit 156b43ec by cahrens

Allow passing through title to iFrame.

Also changes section to div.

TNL-6044
parent d2b6c9eb
...@@ -592,8 +592,12 @@ class JSInput(InputTypeBase): ...@@ -592,8 +592,12 @@ class JSInput(InputTypeBase):
# set state # set state
Attribute('width', "400"), # iframe width 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 # Title for the iframe, which should be supplied by the author of the problem. Not translated
# attribute is set to false. # because we are in a class method and therefore do not have access to capa_system.i18n.
# Note that the default "display name" for the problem is also not translated.
Attribute('title', "Problem Remote Content"),
# SOP will be relaxed only if this attribute is set to false.
Attribute('sop', None)
] ]
def _extra_context(self): def _extra_context(self):
......
<%page expression_filter="h"/>
<%! from openedx.core.djangolib.markup import HTML %> <%! from openedx.core.djangolib.markup import HTML %>
<section id="inputtype_${id}" class="jsinput" <div id="inputtype_${id}" class="jsinput"
data="${gradefn}" data="${gradefn}"
% if saved_state: % if saved_state:
data-stored="${saved_state|x}" data-stored="${saved_state}"
% endif % endif
% if initial_state: % if initial_state:
data-initial-state="${initial_state|x}" data-initial-state="${initial_state}"
% endif % endif
% if get_statefn: % if get_statefn:
data-getstate="${get_statefn}" data-getstate="${get_statefn}"
...@@ -33,10 +34,11 @@ ...@@ -33,10 +34,11 @@
src="${html_file}" src="${html_file}"
height="${height}" height="${height}"
width="${width}" width="${width}"
title="${title}"
/> />
<input type="hidden" name="input_${id}" id="input_${id}" <input type="hidden" name="input_${id}" id="input_${id}"
waitfor="" waitfor=""
value="${value|h}"/> value="${value}"/>
<br/> <br/>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
...@@ -54,4 +56,4 @@ ...@@ -54,4 +56,4 @@
% if msg: % if msg:
<span class="message" tabindex="-1">${HTML(msg)}</span> <span class="message" tabindex="-1">${HTML(msg)}</span>
% endif % endif
</section> </div>
...@@ -608,6 +608,18 @@ class ImageResponseXMLFactory(ResponseXMLFactory): ...@@ -608,6 +608,18 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
return input_element return input_element
class JSInputXMLFactory(CustomResponseXMLFactory):
"""
Factory for producing <jsinput> XML.
Note that this factory currently does not create a functioning problem.
It will only create an empty iframe.
"""
def create_input_element(self, **kwargs):
""" Create the <jsinput> element """
return etree.Element("jsinput")
class MultipleChoiceResponseXMLFactory(ResponseXMLFactory): class MultipleChoiceResponseXMLFactory(ResponseXMLFactory):
""" Factory for producing <multiplechoiceresponse> XML """ """ Factory for producing <multiplechoiceresponse> XML """
......
...@@ -162,6 +162,89 @@ class ChoiceGroupTest(unittest.TestCase): ...@@ -162,6 +162,89 @@ class ChoiceGroupTest(unittest.TestCase):
self.check_group('checkboxgroup', 'checkbox', '[]') self.check_group('checkboxgroup', 'checkbox', '[]')
class JSInputTest(unittest.TestCase):
"""
Test context variables passed into the jsinput template.
"""
def test_rendering_default_values(self):
"""
Tests the default values passed through to render.
"""
xml_str = '<jsinput id="prob_1_2"/>'
expected = {
'html_file': None,
'gradefn': "gradefn",
'get_statefn': None,
'set_statefn': None,
'initial_state': None,
'width': "400",
'height': "300",
'title': "Problem Remote Content",
'sop': None
}
self._render_context_test(xml_str, expected)
def test_rendering_provided_values(self):
"""
Tests that values provided by course authors are passed through to render.
"""
xml_str = """
<jsinput id="prob_1_2"
gradefn="WebGLDemo.getGrade" get_statefn="WebGLDemo.getState" set_statefn="WebGLDemo.setState"
initial_state='{"selectedObjects":{"cube":true,"cylinder":false}}'
width="1000" height="1200"
html_file="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html"
sop="false" title="Awesome and fun!"
/>
"""
expected = {
'html_file': "https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html",
'gradefn': "WebGLDemo.getGrade",
'get_statefn': "WebGLDemo.getState",
'set_statefn': "WebGLDemo.setState",
'initial_state': '{"selectedObjects":{"cube":true,"cylinder":false}}',
'width': "1000",
'height': "1200",
'title': "Awesome and fun!",
'sop': 'false'
}
self._render_context_test(xml_str, expected)
def _render_context_test(self, xml_str, expected_context):
"""
Helper method for testing context based on the provided XML string.
"""
element = etree.fromstring(xml_str)
state = {
'value': 103,
'response_data': RESPONSE_DATA
}
the_input = lookup_tag('jsinput')(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access
full_expected_context = {
'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2',
'status': inputtypes.Status('unanswered'),
'describedby_html': DESCRIBEDBY.format(status_id='prob_1_2'),
'msg': "",
'params': None,
'jschannel_loader': '/dummy-static/js/capa/src/jschannel.js',
'jsinput_loader': '/dummy-static/js/capa/src/jsinput.js',
'saved_state': 103,
'response_data': RESPONSE_DATA,
'value': 103
}
full_expected_context.update(expected_context)
self.assertEqual(full_expected_context, context)
class TextLineTest(unittest.TestCase): class TextLineTest(unittest.TestCase):
''' '''
Check that textline inputs work, with and without math. Check that textline inputs work, with and without math.
......
...@@ -66,6 +66,7 @@ data: | ...@@ -66,6 +66,7 @@ data: |
width="400" width="400"
height="400" height="400"
html_file="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html" html_file="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html"
title="Spinning Cone and Cube"
sop="false"/> sop="false"/>
</customresponse> </customresponse>
</problem> </problem>
describe('JSInput', function() { describe('JSInput', function() {
var sections; var $jsinputContainers;
var inputFields; var $inputFields;
beforeEach(function() { beforeEach(function() {
loadFixtures('js/capa/fixtures/jsinput.html'); loadFixtures('js/capa/fixtures/jsinput.html');
sections = $('section[id^="inputtype_"]'); $jsinputContainers = $('.jsinput');
inputFields = $('input[id^="input_"]'); $inputFields = $('input[id^="input_"]');
JSInput.walkDOM(); JSInput.walkDOM();
}); });
it('sets all data-processed attributes to true on first load', function() { it('sets all data-processed attributes to true on first load', function() {
sections.each(function(index, item) { $jsinputContainers.each(function(index, item) {
expect(item).toHaveData('processed', true); expect(item).toHaveData('processed', true);
}); });
}); });
it('sets the waitfor attribute to its update function', function() { it('sets the waitfor attribute to its update function', function() {
inputFields.each(function(index, item) { $inputFields.each(function(index, item) {
expect(item).toHaveAttr('waitfor'); expect(item).toHaveAttr('waitfor');
}); });
}); });
it('tests the correct number of sections', function() { it('tests the correct number of jsinput instances', function() {
expect(sections.length).toEqual(2); expect($jsinputContainers.length).toEqual(2);
expect(sections.length).toEqual(inputFields.length); expect($jsinputContainers.length).toEqual($inputFields.length);
}); });
}); });
...@@ -39,26 +39,26 @@ var JSInput = (function($, undefined) { ...@@ -39,26 +39,26 @@ var JSInput = (function($, undefined) {
/* Private methods */ /* Private methods */
var section = $(elem).parent().find('section[class="jsinput"]'), var jsinputContainer = $(elem).parent().find('.jsinput'),
sectionAttr = function(e) { return $(section).attr(e); }, jsinputAttr = function(e) { return $(jsinputContainer).attr(e); },
iframe = $(elem).find('iframe[name^="iframe_"]').get(0), iframe = $(elem).find('iframe[name^="iframe_"]').get(0),
cWindow = iframe.contentWindow, cWindow = iframe.contentWindow,
path = iframe.src.substring(0, iframe.src.lastIndexOf('/') + 1), path = iframe.src.substring(0, iframe.src.lastIndexOf('/') + 1),
// Get the hidden input field to pass to customresponse // Get the hidden input field to pass to customresponse
inputField = $(elem).parent().find('input[id^="input_"]'), inputField = $(elem).parent().find('input[id^="input_"]'),
// Get the grade function name // Get the grade function name
gradeFn = sectionAttr('data'), gradeFn = jsinputAttr('data'),
// Get state getter // Get state getter
stateGetter = sectionAttr('data-getstate'), stateGetter = jsinputAttr('data-getstate'),
// Get state setter // Get state setter
stateSetter = sectionAttr('data-setstate'), stateSetter = jsinputAttr('data-setstate'),
// Get stored state // Get stored state
storedState = sectionAttr('data-stored'), storedState = jsinputAttr('data-stored'),
// Get initial state // Get initial state
initialState = sectionAttr('data-initial-state'), initialState = jsinputAttr('data-initial-state'),
// Bypass single-origin policy only if this attribute is "false" // Bypass single-origin policy only if this attribute is "false"
// In that case, use JSChannel to do so. // In that case, use JSChannel to do so.
sop = sectionAttr('data-sop'), sop = jsinputAttr('data-sop'),
channel; channel;
sop = (sop !== 'false'); sop = (sop !== 'false');
...@@ -189,14 +189,14 @@ var JSInput = (function($, undefined) { ...@@ -189,14 +189,14 @@ var JSInput = (function($, undefined) {
} }
function walkDOM() { function walkDOM() {
var allSections = $('section.jsinput'); var $jsinputContainers = $('.jsinput');
// When a JSInput problem loads, its data-processed attribute is false, // When a JSInput problem loads, its data-processed attribute is false,
// so the jsconstructor will be called for it. // so the jsconstructor will be called for it.
// The constructor will not be called again on subsequent reruns of // The constructor will not be called again on subsequent reruns of
// this file by other JSInput. Only if it is reloaded, either with the // this file by other JSInput. Only if it is reloaded, either with the
// rest of the page or when it is submitted, will this constructor be // rest of the page or when it is submitted, will this constructor be
// called again. // called again.
allSections.each(function(index, value) { $jsinputContainers.each(function(index, value) {
var dataProcessed = ($(value).attr('data-processed') === 'true'); var dataProcessed = ($(value).attr('data-processed') === 'true');
if (!dataProcessed) { if (!dataProcessed) {
jsinputConstructor(value); jsinputConstructor(value);
......
...@@ -19,6 +19,7 @@ from capa.tests.response_xml_factory import ( ...@@ -19,6 +19,7 @@ from capa.tests.response_xml_factory import (
CustomResponseXMLFactory, CustomResponseXMLFactory,
FormulaResponseXMLFactory, FormulaResponseXMLFactory,
ImageResponseXMLFactory, ImageResponseXMLFactory,
JSInputXMLFactory,
MultipleChoiceResponseXMLFactory, MultipleChoiceResponseXMLFactory,
NumericalResponseXMLFactory, NumericalResponseXMLFactory,
OptionResponseXMLFactory, OptionResponseXMLFactory,
...@@ -132,7 +133,29 @@ class ProblemTypeTestBase(ProblemsTest, EventsTestMixin): ...@@ -132,7 +133,29 @@ class ProblemTypeTestBase(ProblemsTest, EventsTestMixin):
raise NotImplementedError() raise NotImplementedError()
class ProblemTypeTestMixin(object): class ProblemTypeA11yTestMixin(object):
"""
Shared a11y tests for all problem types.
"""
@attr('a11y')
def test_problem_type_a11y(self):
"""
Run accessibility audit for the problem type.
"""
self.problem_page.wait_for(
lambda: self.problem_page.problem_name == self.problem_name,
"Make sure the correct problem is on the page"
)
# Set the scope to the problem container
self.problem_page.a11y_audit.config.set_scope(
include=['div#seq_content'])
# Run the accessibility audit.
self.problem_page.a11y_audit.check_for_accessibility_errors()
class ProblemTypeTestMixin(ProblemTypeA11yTestMixin):
""" """
Test cases shared amongst problem types. Test cases shared amongst problem types.
""" """
...@@ -357,23 +380,6 @@ class ProblemTypeTestMixin(object): ...@@ -357,23 +380,6 @@ class ProblemTypeTestMixin(object):
self.problem_page.click_submit() self.problem_page.click_submit()
self.problem_page.wait_partial_notification() self.problem_page.wait_partial_notification()
@attr('a11y')
def test_problem_type_a11y(self):
"""
Run accessibility audit for the problem type.
"""
self.problem_page.wait_for(
lambda: self.problem_page.problem_name == self.problem_name,
"Make sure the correct problem is on the page"
)
# Set the scope to the problem container
self.problem_page.a11y_audit.config.set_scope(
include=['div#seq_content'])
# Run the accessibility audit.
self.problem_page.a11y_audit.check_for_accessibility_errors()
class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class AnnotationProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
""" """
...@@ -801,6 +807,29 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -801,6 +807,29 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
self.problem_page.fill_answer(second_addend, input_num=1) self.problem_page.fill_answer(second_addend, input_num=1)
class JSInputTypeTest(ProblemTypeTestBase, ProblemTypeA11yTestMixin):
"""
TestCase Class for jsinput (custom JavaScript) problem type.
Right now the only test point that is executed is the a11y test.
This is because the factory simply creates an empty iframe.
"""
problem_name = 'JSINPUT PROBLEM'
problem_type = 'customresponse'
factory = JSInputXMLFactory()
factory_kwargs = {
'question_text': 'IFrame shows below (but has no content)'
}
def answer_problem(self, correctness):
"""
Problem is not set up to work (displays an empty iframe), but this method must
be extended because the parent class has marked it as abstract.
"""
raise NotImplementedError()
class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): class CodeProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
""" """
TestCase Class for Code Problem Type TestCase Class for Code Problem Type
......
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