Commit f91583c2 by muhammad-ammar Committed by muzaffaryousaf

make responsetypes with multiple inputtypes accessible

parent 19cc68c8
...@@ -72,8 +72,6 @@ log = logging.getLogger(__name__) ...@@ -72,8 +72,6 @@ log = logging.getLogger(__name__)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
# main class for this module # main class for this module
DEFAULT_QUESTION_TEXT = "Formatting error: You must explicitly specify the question text."
class LoncapaSystem(object): class LoncapaSystem(object):
""" """
...@@ -765,8 +763,7 @@ class LoncapaProblem(object): ...@@ -765,8 +763,7 @@ class LoncapaProblem(object):
if problemtree.tag in inputtypes.registry.registered_tags(): if problemtree.tag in inputtypes.registry.registered_tags():
# If this is an inputtype subtree, let it render itself. # If this is an inputtype subtree, let it render itself.
response_id = self.problem_id + '_' + problemtree.get('response_id') response_data = self.problem_data[problemid]
response_data = self.problem_data[response_id]
status = 'unsubmitted' status = 'unsubmitted'
msg = '' msg = ''
...@@ -856,16 +853,16 @@ class LoncapaProblem(object): ...@@ -856,16 +853,16 @@ class LoncapaProblem(object):
problem_data = {} problem_data = {}
self.responders = {} self.responders = {}
for response in tree.xpath('//' + "|//".join(responsetypes.registry.registered_tags())): for response in tree.xpath('//' + "|//".join(responsetypes.registry.registered_tags())):
response_id_str = self.problem_id + "_" + str(response_id) responsetype_id = self.problem_id + "_" + str(response_id)
# create and save ID for this response # create and save ID for this response
response.set('id', response_id_str) response.set('id', responsetype_id)
response_id += 1 response_id += 1
answer_id = 1 answer_id = 1
input_tags = inputtypes.registry.registered_tags() input_tags = inputtypes.registry.registered_tags()
inputfields = tree.xpath( inputfields = tree.xpath(
"|".join(['//' + response.tag + '[@id=$id]//' + x for x in input_tags]), "|".join(['//' + response.tag + '[@id=$id]//' + x for x in input_tags]),
id=response_id_str id=responsetype_id
) )
# assign one answer_id for each input type # assign one answer_id for each input type
...@@ -875,10 +872,65 @@ class LoncapaProblem(object): ...@@ -875,10 +872,65 @@ class LoncapaProblem(object):
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id) entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
answer_id = answer_id + 1 answer_id = answer_id + 1
question_id = u'{}_{}'.format(self.problem_id, response_id) self.response_a11y_data(response, inputfields, responsetype_id, problem_data)
label = ''
element_to_be_deleted = None # instantiate capa Response
responsetype_cls = responsetypes.registry.get_class_for_tag(response.tag)
responder = responsetype_cls(response, inputfields, self.context, self.capa_system, self.capa_module)
# save in list in self
self.responders[response] = responder
# get responder answers (do this only once, since there may be a performance cost,
# eg with externalresponse)
self.responder_answers = {}
for response in self.responders.keys():
try:
self.responder_answers[response] = self.responders[response].get_answers()
except:
log.debug('responder %s failed to properly return get_answers()',
self.responders[response]) # FIXME
raise
# <solution>...</solution> may not be associated with any specific response; give
# IDs for those separately
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
solution_id = 1
for solution in tree.findall('.//solution'):
solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id)
solution_id += 1
return problem_data
def response_a11y_data(self, response, inputfields, responsetype_id, problem_data):
"""
Construct data to be used for a11y.
Arguments:
response (object): xml response object
inputfields (list): list of inputfields in a responsetype
responsetype_id (str): responsetype id
problem_data (dict): dict to be filled with response data
"""
element_to_be_deleted = None
label = ''
if len(inputfields) > 1:
response.set('multiple_inputtypes', 'true')
group_label_tag = response.find('label')
group_label_tag_text = ''
if group_label_tag is not None:
group_label_tag.tag = 'p'
group_label_tag.set('id', responsetype_id)
group_label_tag.set('class', 'multi-inputs-group-label')
group_label_tag_text = group_label_tag.text
for inputfield in inputfields:
problem_data[inputfield.get('id')] = {
'group_label': group_label_tag_text,
'label': inputfield.attrib.get('label', ''),
'descriptions': {}
}
else:
# Extract label value from <label> tag or label attribute from inside the responsetype # Extract label value from <label> tag or label attribute from inside the responsetype
responsetype_label_tag = response.find('label') responsetype_label_tag = response.find('label')
if responsetype_label_tag is not None: if responsetype_label_tag is not None:
...@@ -913,56 +965,22 @@ class LoncapaProblem(object): ...@@ -913,56 +965,22 @@ class LoncapaProblem(object):
label = label_tag[0].text label = label_tag[0].text
element_to_be_deleted = label_tag[0] element_to_be_deleted = label_tag[0]
label = label.strip() or DEFAULT_QUESTION_TEXT
# delete label or p element only if responsetype is fully accessible # delete label or p element only if responsetype is fully accessible
if response.tag in ACCESSIBLE_CAPA_RESPONSE_TYPES and element_to_be_deleted is not None: if response.tag in ACCESSIBLE_CAPA_RESPONSE_TYPES and element_to_be_deleted is not None:
element_to_be_deleted.getparent().remove(element_to_be_deleted) element_to_be_deleted.getparent().remove(element_to_be_deleted)
# for non-accessible responsetypes it may be possible that label attribute is not present
# in this case pass an empty label. remember label attribute is only used as value for aria-label
if response.tag not in ACCESSIBLE_CAPA_RESPONSE_TYPES and label == DEFAULT_QUESTION_TEXT:
label = ''
# Extract descriptions and set unique id on each description tag # Extract descriptions and set unique id on each description tag
description_tags = response.findall('description') description_tags = response.findall('description')
description_id = 1 description_id = 1
descriptions = OrderedDict() descriptions = OrderedDict()
for description in description_tags: for description in description_tags:
descriptions[ descriptions[
"description_%s_%i_%i" % (self.problem_id, response_id, description_id) "description_%s_%i" % (responsetype_id, description_id)
] = description.text ] = description.text
response.remove(description) response.remove(description)
description_id += 1 description_id += 1
problem_data[question_id] = { problem_data[inputfields[0].get('id')] = {
'label': label, 'label': label,
'descriptions': descriptions 'descriptions': descriptions
} }
# instantiate capa Response
responsetype_cls = responsetypes.registry.get_class_for_tag(response.tag)
responder = responsetype_cls(response, inputfields, self.context, self.capa_system, self.capa_module)
# save in list in self
self.responders[response] = responder
# get responder answers (do this only once, since there may be a performance cost,
# eg with externalresponse)
self.responder_answers = {}
for response in self.responders.keys():
try:
self.responder_answers[response] = self.responders[response].get_answers()
except:
log.debug('responder %s failed to properly return get_answers()',
self.responders[response]) # FIXME
raise
# <solution>...</solution> may not be associated with any specific response; give
# IDs for those separately
# TODO: We should make the namespaces consistent and unique (e.g. %s_problem_%i).
solution_id = 1
for solution in tree.findall('.//solution'):
solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id)
solution_id += 1
return problem_data
...@@ -263,6 +263,15 @@ class LoncapaResponse(object): ...@@ -263,6 +263,15 @@ class LoncapaResponse(object):
tree.set('tabindex', '-1') tree.set('tabindex', '-1')
tree.set('aria-label', response_label) tree.set('aria-label', response_label)
if self.xml.get('multiple_inputtypes'):
# add <div> to wrap all inputtypes
content = etree.SubElement(tree, 'div')
content.set('class', 'multi-inputs-group')
content.set('role', 'group')
content.set('aria-labelledby', self.xml.get('id'))
else:
content = tree
# problem author can make this span display:inline # problem author can make this span display:inline
if self.xml.get('inline', ''): if self.xml.get('inline', ''):
tree.set('class', 'inline') tree.set('class', 'inline')
...@@ -271,12 +280,12 @@ class LoncapaResponse(object): ...@@ -271,12 +280,12 @@ class LoncapaResponse(object):
# call provided procedure to do the rendering # call provided procedure to do the rendering
item_xhtml = renderer(item) item_xhtml = renderer(item)
if item_xhtml is not None: if item_xhtml is not None:
tree.append(item_xhtml) content.append(item_xhtml)
tree.tail = self.xml.tail tree.tail = self.xml.tail
# Add a <div> for the message at the end of the response # Add a <div> for the message at the end of the response
if response_msg: if response_msg:
tree.append(self._render_response_msg_html(response_msg)) content.append(self._render_response_msg_html(response_msg))
return tree return tree
......
<%! from django.utils.translation import ugettext as _ %> <%! from django.utils.translation import ugettext as _ %>
<% element_checked = False %> <% element_checked = False %>
% for choice_id, _ in choices: % for choice_id, _ in choices:
<%choice_id = choice_id %> <% choice_id = choice_id %>
%if choice_id in value: %if choice_id in value:
<% element_checked = True %> <% element_checked = True %>
%endif %endif
%endfor % endfor
<section id="choicetextinput_${id}" class="choicetextinput"> <section id="choicetextinput_${id}" class="choicetextinput">
<form class="choicetextgroup capa_inputtype" id="inputtype_${id}"> <form class="choicetextgroup capa_inputtype" id="inputtype_${id}">
<div class="script_placeholder" data-src="${STATIC_URL}js/capa/choicetextinput.js"/> <div class="script_placeholder" data-src="${STATIC_URL}js/capa/choicetextinput.js"/>
<fieldset aria-label="${response_data['label']}"> <fieldset aria-label="${response_data['label']}">
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<%choice_id= choice_id %> <% choice_id = choice_id %>
<section id="forinput${choice_id}" <section id="forinput${choice_id}"
% if input_type == 'radio' and choice_id in value : % if input_type == 'radio' and choice_id in value :
<% <%
......
...@@ -56,8 +56,8 @@ ...@@ -56,8 +56,8 @@
</div> </div>
% endif % endif
% if msg: % if msg:
<span class="message">${HTML(msg)}</span> <span class="message">${HTML(msg)}</span>
% endif % endif
</div> </div>
"""Tools for helping with testing capa.""" """Tools for helping with testing capa."""
import gettext import gettext
from path import path # pylint: disable=no-name-in-module
import os import os
import os.path import os.path
...@@ -9,12 +10,29 @@ import fs.osfs ...@@ -9,12 +10,29 @@ import fs.osfs
from capa.capa_problem import LoncapaProblem, LoncapaSystem from capa.capa_problem import LoncapaProblem, LoncapaSystem
from capa.inputtypes import Status from capa.inputtypes import Status
from mock import Mock, MagicMock from mock import Mock, MagicMock
from mako.lookup import TemplateLookup
import xml.sax.saxutils as saxutils import xml.sax.saxutils as saxutils
TEST_DIR = os.path.dirname(os.path.realpath(__file__)) TEST_DIR = os.path.dirname(os.path.realpath(__file__))
def get_template(template_name):
"""
Return template for a capa inputtype.
"""
return TemplateLookup(
directories=[path(__file__).dirname().dirname() / 'templates']
).get_template(template_name)
def capa_render_template(template, context):
"""
Render template for a capa inputtype.
"""
return get_template(template).render_unicode(**context)
def tst_render_template(template, context): def tst_render_template(template, context):
""" """
A test version of render to template. Renders to the repr of the context, completely ignoring A test version of render to template. Renders to the repr of the context, completely ignoring
...@@ -30,7 +48,7 @@ xqueue_interface = MagicMock() ...@@ -30,7 +48,7 @@ xqueue_interface = MagicMock()
xqueue_interface.send_to_queue.return_value = (0, 'Success!') xqueue_interface.send_to_queue.return_value = (0, 'Success!')
def test_capa_system(): def test_capa_system(render_template=None):
""" """
Construct a mock LoncapaSystem instance. Construct a mock LoncapaSystem instance.
...@@ -46,7 +64,7 @@ def test_capa_system(): ...@@ -46,7 +64,7 @@ def test_capa_system():
filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")), filestore=fs.osfs.OSFS(os.path.join(TEST_DIR, "test_files")),
i18n=gettext.NullTranslations(), i18n=gettext.NullTranslations(),
node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"), node_path=os.environ.get("NODE_PATH", "/usr/local/lib/node_modules"),
render_template=tst_render_template, render_template=render_template or tst_render_template,
seed=0, seed=0,
STATIC_URL='/dummy-static/', STATIC_URL='/dummy-static/',
STATUS_CLASS=Status, STATUS_CLASS=Status,
...@@ -66,9 +84,10 @@ def mock_capa_module(): ...@@ -66,9 +84,10 @@ def mock_capa_module():
return capa_module return capa_module
def new_loncapa_problem(xml, capa_system=None, seed=723): def new_loncapa_problem(xml, capa_system=None, seed=723, use_capa_render_template=False):
"""Construct a `LoncapaProblem` suitable for unit tests.""" """Construct a `LoncapaProblem` suitable for unit tests."""
return LoncapaProblem(xml, id='1', seed=seed, capa_system=capa_system or test_capa_system(), render_template = capa_render_template if use_capa_render_template else None
return LoncapaProblem(xml, id='1', seed=seed, capa_system=capa_system or test_capa_system(render_template),
capa_module=mock_capa_module()) capa_module=mock_capa_module())
......
...@@ -267,6 +267,9 @@ class CustomResponseXMLFactory(ResponseXMLFactory): ...@@ -267,6 +267,9 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
*answer_attr*: The "answer" attribute on the tag itself (treated as an *answer_attr*: The "answer" attribute on the tag itself (treated as an
alias to "expect", though "expect" takes priority if both are given) alias to "expect", though "expect" takes priority if both are given)
*group_label*: Text to represent group of inputs when there are
multiple inputs.
""" """
# Retrieve **kwargs # Retrieve **kwargs
...@@ -276,6 +279,7 @@ class CustomResponseXMLFactory(ResponseXMLFactory): ...@@ -276,6 +279,7 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
answer = kwargs.get('answer', None) answer = kwargs.get('answer', None)
options = kwargs.get('options', None) options = kwargs.get('options', None)
cfn_extra_args = kwargs.get('cfn_extra_args', None) cfn_extra_args = kwargs.get('cfn_extra_args', None)
group_label = kwargs.get('group_label', None)
# Create the response element # Create the response element
response_element = etree.Element("customresponse") response_element = etree.Element("customresponse")
...@@ -293,6 +297,10 @@ class CustomResponseXMLFactory(ResponseXMLFactory): ...@@ -293,6 +297,10 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
answer_element = etree.SubElement(response_element, "answer") answer_element = etree.SubElement(response_element, "answer")
answer_element.text = str(answer) answer_element.text = str(answer)
if group_label:
group_label_element = etree.SubElement(response_element, "label")
group_label_element.text = group_label
if options: if options:
response_element.set('options', str(options)) response_element.set('options', str(options))
......
""" """
Test capa problem. Test capa problem.
""" """
import ddt
import textwrap
from lxml import etree
import unittest import unittest
from . import new_loncapa_problem from . import new_loncapa_problem
from capa.capa_problem import DEFAULT_QUESTION_TEXT
class CAPAProblemTest(unittest.TestCase): class CAPAProblemTest(unittest.TestCase):
...@@ -35,10 +37,10 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -35,10 +37,10 @@ class CAPAProblemTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
{ {
'1_2': '1_2_1':
{ {
'label': 'Select the correct synonym of paranoid?', 'label': 'Select the correct synonym of paranoid?',
'descriptions': {'description_1_2_1': 'Only the paranoid survive.'} 'descriptions': {'description_1_1_1': 'Only the paranoid survive.'}
} }
} }
) )
...@@ -62,7 +64,13 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -62,7 +64,13 @@ class CAPAProblemTest(unittest.TestCase):
problem = new_loncapa_problem(xml) problem = new_loncapa_problem(xml)
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
{'1_2': {'label': question, 'descriptions': {}}} {
'1_2_1':
{
'label': question,
'descriptions': {}
}
}
) )
self.assertEqual( self.assertEqual(
...@@ -101,12 +109,12 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -101,12 +109,12 @@ class CAPAProblemTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
{ {
'1_2': '1_2_1':
{ {
'label': question1, 'label': question1,
'descriptions': {} 'descriptions': {}
}, },
'1_3': '1_3_1':
{ {
'label': question2, 'label': question2,
'descriptions': {} 'descriptions': {}
...@@ -139,39 +147,12 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -139,39 +147,12 @@ class CAPAProblemTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
{ {
'1_2': '1_2_1':
{ {
'label': '___ requires sacrifices.', 'label': '___ requires sacrifices.',
'descriptions': { 'descriptions': {
'description_1_2_1': "The problem with trying to be the bad guy, there's always someone worse.", 'description_1_1_1': "The problem with trying to be the bad guy, there's always someone worse.",
'description_1_2_2': "Anyone who looks the world as if it was a game of chess deserves to lose." 'description_1_1_2': "Anyone who looks the world as if it was a game of chess deserves to lose."
}
}
}
)
def test_default_question_text(self):
"""
Verify that default question text is shown when question is missing.
"""
xml = """
<problem>
<p>Be sure to check your spelling.</p>
<stringresponse answer="War" type="ci">
<description>Everybody needs somebody to talk to.</description>
<textline size="40"/>
</stringresponse>
</problem>
"""
problem = new_loncapa_problem(xml)
self.assertEqual(
problem.problem_data,
{
'1_2':
{
'label': DEFAULT_QUESTION_TEXT,
'descriptions': {
'description_1_2_1': "Everybody needs somebody to talk to."
} }
} }
} }
...@@ -195,7 +176,7 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -195,7 +176,7 @@ class CAPAProblemTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
{ {
'1_2': '1_2_1':
{ {
'label': 'Click the country which is home to the Pyramids.', 'label': 'Click the country which is home to the Pyramids.',
'descriptions': {} 'descriptions': {}
...@@ -224,7 +205,7 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -224,7 +205,7 @@ class CAPAProblemTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
{ {
'1_2': '1_2_1':
{ {
'label': '', 'label': '',
'descriptions': {} 'descriptions': {}
...@@ -266,15 +247,15 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -266,15 +247,15 @@ class CAPAProblemTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
{ {
'1_2': '1_2_1':
{ {
'label': 'Select the correct synonym of paranoid?', 'label': 'Select the correct synonym of paranoid?',
'descriptions': {'description_1_2_1': 'Only the paranoid survive.'} 'descriptions': {'description_1_1_1': 'Only the paranoid survive.'}
}, },
'1_3': '1_3_1':
{ {
'label': 'What Apple device competed with the portable CD player?', 'label': 'What Apple device competed with the portable CD player?',
'descriptions': {'description_1_3_1': 'Device looks like an egg plant.'} 'descriptions': {'description_1_2_1': 'Device looks like an egg plant.'}
} }
} }
) )
...@@ -306,7 +287,7 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -306,7 +287,7 @@ class CAPAProblemTest(unittest.TestCase):
self.assertEqual( self.assertEqual(
problem.problem_data, problem.problem_data,
{ {
'1_2': '1_2_1':
{ {
'label': question, 'label': question,
'descriptions': {} 'descriptions': {}
...@@ -317,3 +298,146 @@ class CAPAProblemTest(unittest.TestCase): ...@@ -317,3 +298,146 @@ class CAPAProblemTest(unittest.TestCase):
len(problem.tree.xpath('//p[text()="{}"]'.format(question))), len(problem.tree.xpath('//p[text()="{}"]'.format(question))),
0 0
) )
def test_multiple_inputtypes(self):
"""
Verify that group label and labels for individual inputtypes are extracted correctly.
"""
group_label = 'Choose the correct color'
input1_label = 'What color is the sky?'
input2_label = 'What color are pine needles?'
xml = """
<problem>
<optionresponse>
<label>{}</label>
<optioninput options="('yellow','blue','green')" correct="blue" label="{}"/>
<optioninput options="('yellow','blue','green')" correct="green" label="{}"/>
</optionresponse>
</problem>
""".format(group_label, input1_label, input2_label)
problem = new_loncapa_problem(xml)
self.assertEqual(
problem.problem_data,
{
'1_2_1':
{
'group_label': group_label,
'label': input1_label,
'descriptions': {}
},
'1_2_2':
{
'group_label': group_label,
'label': input2_label,
'descriptions': {}
}
}
)
def test_single_inputtypes(self):
"""
Verify that HTML is correctly rendered when there is single inputtype.
"""
xml = """
<problem>
<choiceresponse>
<label>Select the correct synonym of paranoid?</label>
<description>Only the paranoid survive.</description>
<checkboxgroup>
<choice correct="true">over-suspicious</choice>
<choice correct="false">funny</choice>
</checkboxgroup>
</choiceresponse>
</problem>
"""
problem = new_loncapa_problem(xml, use_capa_render_template=True)
problem_html = etree.XML(problem.get_html())
# verify that only no multi input group div is present
multi_inputs_group = problem_html.xpath('//div[@class="multi-inputs-group"]')
self.assertEqual(len(multi_inputs_group), 0)
@ddt.ddt
class CAPAMultiInputProblemTest(unittest.TestCase):
""" TestCase for CAPA problems with multiple inputtypes """
def capa_problem(self, xml):
"""
Create capa problem.
"""
return new_loncapa_problem(xml, use_capa_render_template=True)
def assert_problem_html(self, problme_html, group_label, *input_labels):
"""
Verify that correct html is rendered for multiple inputtypes.
"""
html = etree.XML(problme_html)
# verify that only one multi input group div is present at correct path
multi_inputs_group = html.xpath(
'//section[@class="wrapper-problem-response"]/div[@class="multi-inputs-group"]'
)
self.assertEqual(len(multi_inputs_group), 1)
# verify that multi input group label <p> tag exists and its
# id matches with correct multi input group aria-labelledby
multi_inputs_group_label_id = multi_inputs_group[0].attrib.get('aria-labelledby')
multi_inputs_group_label = html.xpath('//p[@id="{}"]'.format(multi_inputs_group_label_id))
self.assertEqual(len(multi_inputs_group_label), 1)
self.assertEqual(multi_inputs_group_label[0].text, group_label)
# verify that label for each input comes only once
for input_label in input_labels:
# normalize-space is used to remove whitespace around the text
input_label_element = multi_inputs_group[0].xpath('//*[normalize-space(text())="{}"]'.format(input_label))
self.assertEqual(len(input_label_element), 1)
def test_optionresponse(self):
"""
Verify that optionresponse problem with multiple inputtypes is rendered correctly.
"""
group_label = 'Choose the correct color'
input1_label = 'What color is the sky?'
input2_label = 'What color are pine needles?'
xml = """
<problem>
<optionresponse>
<label>{}</label>
<optioninput options="('yellow','blue','green')" correct="blue" label="{}"/>
<optioninput options="('yellow','blue','green')" correct="green" label="{}"/>
</optionresponse>
</problem>
""".format(group_label, input1_label, input2_label)
problem = self.capa_problem(xml)
self.assert_problem_html(problem.get_html(), group_label, input1_label, input2_label)
@ddt.unpack
@ddt.data(
{'inputtype': 'textline'},
{'inputtype': 'formulaequationinput'}
)
def test_customresponse(self, inputtype):
"""
Verify that customresponse problem with multiple textline
and formulaequationinput inputtypes is rendered correctly.
"""
group_label = 'Enter two integers that sum to 10.'
input1_label = 'Integer 1'
input2_label = 'Integer 2'
xml = textwrap.dedent("""
<problem>
<customresponse cfn="test_add_to_ten">
<script type="loncapa/python">
def test_add_to_ten(expect, ans):
return test_add(10, ans)
</script>
<label>{}</label>
<{inputtype} size="40" correct_answer="3" label="{}" /><br/>
<{inputtype} size="40" correct_answer="7" label="{}" />
</customresponse>
</problem>
""".format(group_label, input1_label, input2_label, inputtype=inputtype))
problem = self.capa_problem(xml)
self.assert_problem_html(problem.get_html(), group_label, input1_label, input2_label)
...@@ -7,7 +7,6 @@ import mock ...@@ -7,7 +7,6 @@ import mock
from .response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory from .response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory
from . import test_capa_system, new_loncapa_problem from . import test_capa_system, new_loncapa_problem
from capa.capa_problem import DEFAULT_QUESTION_TEXT
class CapaHtmlRenderTest(unittest.TestCase): class CapaHtmlRenderTest(unittest.TestCase):
...@@ -186,7 +185,7 @@ class CapaHtmlRenderTest(unittest.TestCase): ...@@ -186,7 +185,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
'id': '1_2_1', 'id': '1_2_1',
'trailing_text': '', 'trailing_text': '',
'size': None, 'size': None,
'response_data': {'label': DEFAULT_QUESTION_TEXT, 'descriptions': {}}, 'response_data': {'label': '', 'descriptions': {}},
'describedby': '' 'describedby': ''
} }
......
...@@ -1296,6 +1296,9 @@ class CapaMixin(CapaFields): ...@@ -1296,6 +1296,9 @@ class CapaMixin(CapaFields):
'correct': is_correct, 'correct': is_correct,
'variant': variant, 'variant': variant,
} }
# Add group_label in event data only if the responsetype contains multiple inputtypes
if answer_input.response_data.get('group_label'):
input_metadata[input_id]['group_label'] = answer_input.response_data.get('group_label')
return input_metadata return input_metadata
......
...@@ -21,7 +21,6 @@ from webob.multidict import MultiDict ...@@ -21,7 +21,6 @@ from webob.multidict import MultiDict
import xmodule import xmodule
from xmodule.tests import DATA_DIR from xmodule.tests import DATA_DIR
from capa import responsetypes from capa import responsetypes
from capa.capa_problem import DEFAULT_QUESTION_TEXT
from capa.responsetypes import (StudentInputError, LoncapaProblemError, from capa.responsetypes import (StudentInputError, LoncapaProblemError,
ResponseError) ResponseError)
from capa.xqueue_interface import XQueueInterface from capa.xqueue_interface import XQueueInterface
...@@ -2652,7 +2651,7 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2652,7 +2651,7 @@ class TestProblemCheckTracking(unittest.TestCase):
event = self.get_event_for_answers(module, answer_input_dict) event = self.get_event_for_answers(module, answer_input_dict)
self.assertEquals(event['submission'], { self.assertEquals(event['submission'], {
factory.answer_key(2): { factory.answer_key(2): {
'question': DEFAULT_QUESTION_TEXT, 'question': '',
'answer': '3.14', 'answer': '3.14',
'response_type': 'numericalresponse', 'response_type': 'numericalresponse',
'input_type': 'textline', 'input_type': 'textline',
...@@ -2662,19 +2661,19 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2662,19 +2661,19 @@ class TestProblemCheckTracking(unittest.TestCase):
}) })
def test_multiple_inputs(self): def test_multiple_inputs(self):
group_label = 'Choose the correct color'
input1_label = 'What color is the sky?'
input2_label = 'What color are pine needles?'
factory = self.capa_factory_for_problem_xml("""\ factory = self.capa_factory_for_problem_xml("""\
<problem display_name="Multiple Inputs"> <problem display_name="Multiple Inputs">
<p>Choose the correct color</p>
<optionresponse> <optionresponse>
<p>What color is the sky?</p> <label>{}</label>
<optioninput options="('yellow','blue','green')" correct="blue"/> <optioninput options="('yellow','blue','green')" correct="blue" label="{}"/>
<p>What color are pine needles?</p> <optioninput options="('yellow','blue','green')" correct="green" label="{}"/>
<optioninput options="('yellow','blue','green')" correct="green"/>
</optionresponse> </optionresponse>
</problem> </problem>
""") """.format(group_label, input1_label, input2_label))
module = factory.create() module = factory.create()
answer_input_dict = { answer_input_dict = {
factory.input_key(2, 1): 'blue', factory.input_key(2, 1): 'blue',
factory.input_key(2, 2): 'yellow', factory.input_key(2, 2): 'yellow',
...@@ -2683,7 +2682,8 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2683,7 +2682,8 @@ class TestProblemCheckTracking(unittest.TestCase):
event = self.get_event_for_answers(module, answer_input_dict) event = self.get_event_for_answers(module, answer_input_dict)
self.assertEquals(event['submission'], { self.assertEquals(event['submission'], {
factory.answer_key(2, 1): { factory.answer_key(2, 1): {
'question': DEFAULT_QUESTION_TEXT, 'group_label': group_label,
'question': input1_label,
'answer': 'blue', 'answer': 'blue',
'response_type': 'optionresponse', 'response_type': 'optionresponse',
'input_type': 'optioninput', 'input_type': 'optioninput',
...@@ -2691,7 +2691,8 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2691,7 +2691,8 @@ class TestProblemCheckTracking(unittest.TestCase):
'variant': '', 'variant': '',
}, },
factory.answer_key(2, 2): { factory.answer_key(2, 2): {
'question': DEFAULT_QUESTION_TEXT, 'group_label': group_label,
'question': input2_label,
'answer': 'yellow', 'answer': 'yellow',
'response_type': 'optionresponse', 'response_type': 'optionresponse',
'input_type': 'optioninput', 'input_type': 'optioninput',
...@@ -2702,11 +2703,14 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2702,11 +2703,14 @@ class TestProblemCheckTracking(unittest.TestCase):
def test_optioninput_extended_xml(self): def test_optioninput_extended_xml(self):
"""Test the new XML form of writing with <option> tag instead of options= attribute.""" """Test the new XML form of writing with <option> tag instead of options= attribute."""
group_label = 'Are you the Gatekeeper?'
input1_label = 'input 1 label'
input2_label = 'input 2 label'
factory = self.capa_factory_for_problem_xml("""\ factory = self.capa_factory_for_problem_xml("""\
<problem display_name="Woo Hoo"> <problem display_name="Woo Hoo">
<p>Are you the Gatekeeper?</p>
<optionresponse> <optionresponse>
<optioninput> <label>{}</label>
<optioninput label="{}">
<option correct="True" label="Good Job"> <option correct="True" label="Good Job">
apple apple
<optionhint> <optionhint>
...@@ -2721,7 +2725,7 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2721,7 +2725,7 @@ class TestProblemCheckTracking(unittest.TestCase):
</option> </option>
</optioninput> </optioninput>
<optioninput> <optioninput label="{}">
<option correct="True"> <option correct="True">
apple apple
<optionhint> <optionhint>
...@@ -2737,7 +2741,7 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2737,7 +2741,7 @@ class TestProblemCheckTracking(unittest.TestCase):
</optioninput> </optioninput>
</optionresponse> </optionresponse>
</problem> </problem>
""") """.format(group_label, input1_label, input2_label))
module = factory.create() module = factory.create()
answer_input_dict = { answer_input_dict = {
...@@ -2748,7 +2752,8 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2748,7 +2752,8 @@ class TestProblemCheckTracking(unittest.TestCase):
event = self.get_event_for_answers(module, answer_input_dict) event = self.get_event_for_answers(module, answer_input_dict)
self.assertEquals(event['submission'], { self.assertEquals(event['submission'], {
factory.answer_key(2, 1): { factory.answer_key(2, 1): {
'question': DEFAULT_QUESTION_TEXT, 'group_label': group_label,
'question': input1_label,
'answer': 'apple', 'answer': 'apple',
'response_type': 'optionresponse', 'response_type': 'optionresponse',
'input_type': 'optioninput', 'input_type': 'optioninput',
...@@ -2756,7 +2761,8 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2756,7 +2761,8 @@ class TestProblemCheckTracking(unittest.TestCase):
'variant': '', 'variant': '',
}, },
factory.answer_key(2, 2): { factory.answer_key(2, 2): {
'question': DEFAULT_QUESTION_TEXT, 'group_label': group_label,
'question': input2_label,
'answer': 'cucumber', 'answer': 'cucumber',
'response_type': 'optionresponse', 'response_type': 'optionresponse',
'input_type': 'optioninput', 'input_type': 'optioninput',
...@@ -2776,7 +2782,7 @@ class TestProblemCheckTracking(unittest.TestCase): ...@@ -2776,7 +2782,7 @@ class TestProblemCheckTracking(unittest.TestCase):
event = self.get_event_for_answers(module, answer_input_dict) event = self.get_event_for_answers(module, answer_input_dict)
self.assertEquals(event['submission'], { self.assertEquals(event['submission'], {
factory.answer_key(2): { factory.answer_key(2): {
'question': DEFAULT_QUESTION_TEXT, 'question': '',
'answer': '3.14', 'answer': '3.14',
'response_type': 'numericalresponse', 'response_type': 'numericalresponse',
'input_type': 'textline', 'input_type': 'textline',
......
...@@ -592,10 +592,10 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -592,10 +592,10 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
factory = CustomResponseXMLFactory() factory = CustomResponseXMLFactory()
factory_kwargs = { factory_kwargs = {
'question_text': 'Enter two integers that sum to 10.',
'cfn': 'test_add_to_ten', 'cfn': 'test_add_to_ten',
'expect': '10', 'expect': '10',
'num_inputs': 2, 'num_inputs': 2,
'group_label': 'Enter two integers that sum to 10.',
'script': textwrap.dedent(""" 'script': textwrap.dedent("""
def test_add_to_ten(expect,ans): def test_add_to_ten(expect,ans):
try: try:
...@@ -618,12 +618,6 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin): ...@@ -618,12 +618,6 @@ class ScriptProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
Additional setup for ScriptProblemTypeTest Additional setup for ScriptProblemTypeTest
""" """
super(ScriptProblemTypeTest, self).setUp(*args, **kwargs) super(ScriptProblemTypeTest, self).setUp(*args, **kwargs)
self.problem_page.a11y_audit.config.set_rules({
'ignore': [
'section', # TODO: AC-491
'label', # TODO: AC-287
]
})
def answer_problem(self, correct): def answer_problem(self, correct):
""" """
......
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