Merge pull request #14072 from edx/christina/jsinput_title
Allow passing through title to iFrame.
This commit is contained in:
@@ -592,8 +592,12 @@ class JSInput(InputTypeBase):
|
||||
# set state
|
||||
Attribute('width', "400"), # iframe width
|
||||
Attribute('height', "300"), # iframe height
|
||||
Attribute('sop', None) # SOP will be relaxed only if this
|
||||
# attribute is set to false.
|
||||
# Title for the iframe, which should be supplied by the author of the problem. Not translated
|
||||
# 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):
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<%page expression_filter="h"/>
|
||||
<%! from openedx.core.djangolib.markup import HTML %>
|
||||
<section id="inputtype_${id}" class="jsinput"
|
||||
<div id="inputtype_${id}" class="jsinput"
|
||||
data="${gradefn}"
|
||||
% if saved_state:
|
||||
data-stored="${saved_state|x}"
|
||||
data-stored="${saved_state}"
|
||||
% endif
|
||||
% if initial_state:
|
||||
data-initial-state="${initial_state|x}"
|
||||
data-initial-state="${initial_state}"
|
||||
% endif
|
||||
% if get_statefn:
|
||||
data-getstate="${get_statefn}"
|
||||
@@ -33,10 +34,11 @@
|
||||
src="${html_file}"
|
||||
height="${height}"
|
||||
width="${width}"
|
||||
title="${title}"
|
||||
/>
|
||||
<input type="hidden" name="input_${id}" id="input_${id}"
|
||||
waitfor=""
|
||||
value="${value|h}"/>
|
||||
value="${value}"/>
|
||||
|
||||
<br/>
|
||||
<p id="answer_${id}" class="answer"></p>
|
||||
@@ -54,4 +56,4 @@
|
||||
% if msg:
|
||||
<span class="message" tabindex="-1">${HTML(msg)}</span>
|
||||
% endif
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -608,6 +608,18 @@ class ImageResponseXMLFactory(ResponseXMLFactory):
|
||||
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):
|
||||
""" Factory for producing <multiplechoiceresponse> XML """
|
||||
|
||||
|
||||
@@ -162,6 +162,89 @@ class ChoiceGroupTest(unittest.TestCase):
|
||||
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):
|
||||
'''
|
||||
Check that textline inputs work, with and without math.
|
||||
|
||||
@@ -66,6 +66,7 @@ data: |
|
||||
width="400"
|
||||
height="400"
|
||||
html_file="https://studio.edx.org/c4x/edX/DemoX/asset/webGLDemo.html"
|
||||
title="Spinning Cone and Cube"
|
||||
sop="false"/>
|
||||
</customresponse>
|
||||
</problem>
|
||||
|
||||
@@ -1,29 +1,29 @@
|
||||
describe('JSInput', function() {
|
||||
var sections;
|
||||
var inputFields;
|
||||
var $jsinputContainers;
|
||||
var $inputFields;
|
||||
|
||||
beforeEach(function() {
|
||||
loadFixtures('js/capa/fixtures/jsinput.html');
|
||||
sections = $('section[id^="inputtype_"]');
|
||||
inputFields = $('input[id^="input_"]');
|
||||
$jsinputContainers = $('.jsinput');
|
||||
$inputFields = $('input[id^="input_"]');
|
||||
JSInput.walkDOM();
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
it('sets the waitfor attribute to its update function', function() {
|
||||
inputFields.each(function(index, item) {
|
||||
$inputFields.each(function(index, item) {
|
||||
expect(item).toHaveAttr('waitfor');
|
||||
});
|
||||
});
|
||||
|
||||
it('tests the correct number of sections', function() {
|
||||
expect(sections.length).toEqual(2);
|
||||
expect(sections.length).toEqual(inputFields.length);
|
||||
it('tests the correct number of jsinput instances', function() {
|
||||
expect($jsinputContainers.length).toEqual(2);
|
||||
expect($jsinputContainers.length).toEqual($inputFields.length);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -39,26 +39,26 @@ var JSInput = (function($, undefined) {
|
||||
|
||||
/* Private methods */
|
||||
|
||||
var section = $(elem).parent().find('section[class="jsinput"]'),
|
||||
sectionAttr = function(e) { return $(section).attr(e); },
|
||||
var jsinputContainer = $(elem).parent().find('.jsinput'),
|
||||
jsinputAttr = function(e) { return $(jsinputContainer).attr(e); },
|
||||
iframe = $(elem).find('iframe[name^="iframe_"]').get(0),
|
||||
cWindow = iframe.contentWindow,
|
||||
path = iframe.src.substring(0, iframe.src.lastIndexOf('/') + 1),
|
||||
// Get the hidden input field to pass to customresponse
|
||||
inputField = $(elem).parent().find('input[id^="input_"]'),
|
||||
// Get the grade function name
|
||||
gradeFn = sectionAttr('data'),
|
||||
gradeFn = jsinputAttr('data'),
|
||||
// Get state getter
|
||||
stateGetter = sectionAttr('data-getstate'),
|
||||
stateGetter = jsinputAttr('data-getstate'),
|
||||
// Get state setter
|
||||
stateSetter = sectionAttr('data-setstate'),
|
||||
stateSetter = jsinputAttr('data-setstate'),
|
||||
// Get stored state
|
||||
storedState = sectionAttr('data-stored'),
|
||||
storedState = jsinputAttr('data-stored'),
|
||||
// Get initial state
|
||||
initialState = sectionAttr('data-initial-state'),
|
||||
initialState = jsinputAttr('data-initial-state'),
|
||||
// Bypass single-origin policy only if this attribute is "false"
|
||||
// In that case, use JSChannel to do so.
|
||||
sop = sectionAttr('data-sop'),
|
||||
sop = jsinputAttr('data-sop'),
|
||||
channel;
|
||||
|
||||
sop = (sop !== 'false');
|
||||
@@ -189,14 +189,14 @@ var JSInput = (function($, undefined) {
|
||||
}
|
||||
|
||||
function walkDOM() {
|
||||
var allSections = $('section.jsinput');
|
||||
var $jsinputContainers = $('.jsinput');
|
||||
// When a JSInput problem loads, its data-processed attribute is false,
|
||||
// so the jsconstructor will be called for it.
|
||||
// The constructor will not be called again on subsequent reruns of
|
||||
// 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
|
||||
// called again.
|
||||
allSections.each(function(index, value) {
|
||||
$jsinputContainers.each(function(index, value) {
|
||||
var dataProcessed = ($(value).attr('data-processed') === 'true');
|
||||
if (!dataProcessed) {
|
||||
jsinputConstructor(value);
|
||||
|
||||
@@ -19,6 +19,7 @@ from capa.tests.response_xml_factory import (
|
||||
CustomResponseXMLFactory,
|
||||
FormulaResponseXMLFactory,
|
||||
ImageResponseXMLFactory,
|
||||
JSInputXMLFactory,
|
||||
MultipleChoiceResponseXMLFactory,
|
||||
NumericalResponseXMLFactory,
|
||||
OptionResponseXMLFactory,
|
||||
@@ -132,7 +133,29 @@ class ProblemTypeTestBase(ProblemsTest, EventsTestMixin):
|
||||
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.
|
||||
"""
|
||||
@@ -357,23 +380,6 @@ class ProblemTypeTestMixin(object):
|
||||
self.problem_page.click_submit()
|
||||
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):
|
||||
"""
|
||||
@@ -801,6 +807,29 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
|
||||
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):
|
||||
"""
|
||||
TestCase Class for Code Problem Type
|
||||
|
||||
Reference in New Issue
Block a user