Commit 3556f2a3 by Ehtesham Committed by muzaffaryousaf

make CAPA problems (MultipleChoice and Checkboxes) accessible

parent d14928cc
......@@ -176,7 +176,7 @@ class LoncapaProblem(object):
# transformations. This also creates the dict (self.responders) of Response
# instances for each question in the problem. The dict has keys = xml subtree of
# Response, values = Response instance
self._preprocess_problem(self.tree)
self.problem_data = self._preprocess_problem(self.tree)
if not self.student_answers: # True when student_answers is an empty dict
self.set_initial_display()
......@@ -752,7 +752,10 @@ class LoncapaProblem(object):
if problemtree.tag in inputtypes.registry.registered_tags():
# If this is an inputtype subtree, let it render itself.
status = "unsubmitted"
response_id = self.problem_id + '_' + problemtree.get('response_id')
response_data = self.problem_data[response_id]
status = 'unsubmitted'
msg = ''
hint = ''
hintmode = None
......@@ -766,7 +769,7 @@ class LoncapaProblem(object):
hintmode = self.correct_map.get_hintmode(pid)
answervariable = self.correct_map.get_property(pid, 'answervariable')
value = ""
value = ''
if self.student_answers and problemid in self.student_answers:
value = self.student_answers[problemid]
......@@ -780,6 +783,7 @@ class LoncapaProblem(object):
'id': input_id,
'input_state': self.input_state[input_id],
'answervariable': answervariable,
'response_data': response_data,
'feedback': {
'message': msg,
'hint': hint,
......@@ -836,6 +840,7 @@ class LoncapaProblem(object):
Obtain all responder answers and save as self.responder_answers dict (key = response)
"""
response_id = 1
problem_data = {}
self.responders = {}
for response in tree.xpath('//' + "|//".join(responsetypes.registry.registered_tags())):
response_id_str = self.problem_id + "_" + str(response_id)
......@@ -857,6 +862,12 @@ class LoncapaProblem(object):
entry.attrib['id'] = "%s_%i_%i" % (self.problem_id, response_id, answer_id)
answer_id = answer_id + 1
# Find the label and save it for html transformation step
responsetype_label = response.find('label')
problem_data[self.problem_id + '_' + str(response_id)] = {
'label': responsetype_label.text if responsetype_label is not None else ''
}
# 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)
......@@ -881,3 +892,5 @@ class LoncapaProblem(object):
for solution in tree.findall('.//solution'):
solution.attrib['id'] = "%s_solution_%i" % (self.problem_id, solution_id)
solution_id += 1
return problem_data
......@@ -224,7 +224,8 @@ class InputTypeBase(object):
self.hint = feedback.get('hint', '')
self.hintmode = feedback.get('hintmode', None)
self.input_state = state.get('input_state', {})
self.answervariable = state.get("answervariable", None)
self.answervariable = state.get('answervariable', None)
self.response_data = state.get('response_data', None)
# put hint above msg if it should be displayed
if self.hintmode == 'always':
......@@ -316,8 +317,10 @@ class InputTypeBase(object):
'value': self.value,
'status': Status(self.status, self.capa_system.i18n.ugettext),
'msg': self.msg,
'response_data': self.response_data,
'STATIC_URL': self.capa_system.STATIC_URL,
}
context.update(
(a, v) for (a, v) in self.loaded_attributes.iteritems() if a in self.to_render
)
......
......@@ -250,8 +250,18 @@ class LoncapaResponse(object):
- renderer : procedure which produces HTML given an ElementTree
- response_msg: a message displayed at the end of the Response
"""
# render ourself as a <span> + our content
tree = etree.Element('span')
_ = self.capa_system.i18n.ugettext
# get responsetype index to make responsetype label
response_index = self.xml.attrib['id'].split('_')[-1]
# Translators: index here could be 1,2,3 and so on
response_label = _(u'Question {index}').format(index=response_index)
# wrap the content inside a section
tree = etree.Element('section')
tree.set('class', 'wrapper-problem-response')
tree.set('tabindex', '-1')
tree.set('aria-label', response_label)
# problem author can make this span display:inline
if self.xml.get('inline', ''):
......
<%
def is_radio_input(choice_id):
return input_type == 'radio' and ((isinstance(value, basestring) and (choice_id == value)) or (
not isinstance(value, basestring) and choice_id in value
))
%>
<form class="choicegroup capa_inputtype" id="inputtype_${id}">
<fieldset role="${input_type}group" aria-label="${label}">
% for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}"
<fieldset>
% if response_data and response_data['label']:
<legend id="${id}-legend" class="response-fieldset-legend question-text">${response_data['label']}</legend>
% else:
<legend>Question</legend>
% endif
% for choice_id, choice_label in choices:
<div class="field" aria-live="polite" aria-atomic="true">
<%
label_class = 'response-label field-label label-inline'
%>
<label id="${id}-${choice_id}-label"
## If the student has selected this choice...
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
% if is_radio_input(choice_id):
<%
if status == 'correct':
correctness = 'correct'
......@@ -14,30 +29,28 @@
else:
correctness = None
%>
% if correctness and not show_correctness=='never':
class="choicegroup_${correctness}"
% if correctness and not show_correctness == 'never':
<% label_class += ' choicegroup_' + correctness %>
% endif
% endif
>
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" aria-role="radio" aria-describedby="answer_${id}" value="${choice_id}"
class="${label_class}" >
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" class="field-input input-${input_type}" value="${choice_id}"
## If the student selected this choice...
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
% if is_radio_input(choice_id):
checked="true"
% elif input_type != 'radio' and choice_id in value:
checked="true"
% endif
% if input_type != 'radio':
aria-multiselectable="true"
% endif
/> ${choice_description}
/> ${choice_label}
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ):
% if status in ('correct', 'partially-correct', 'incorrect') and not show_correctness=='never':
<span class="sr status">${choice_description|h} - ${status.display_name}</span>
% if is_radio_input(choice_id):
% if status in ('correct', 'partially-correct', 'incorrect') and not show_correctness == 'never':
<span class="sr status" id="${id}-${choice_id}-labeltext">${status.display_name}</span>
% endif
% endif
</label>
</div>
% endfor
<span id="answer_${id}"></span>
</fieldset>
......@@ -45,9 +58,9 @@
% if input_type == 'checkbox' or not value:
<span class="status ${status.classname if show_correctness != 'never' else 'unanswered'}" id="status_${id}" aria-describedby="inputtype_${id}" data-tooltip="${status.display_tooltip}">
<span class="sr">
%for choice_id, choice_description in choices:
%for choice_id, choice_label in choices:
% if choice_id in value:
${choice_description},
${choice_label},
%endif
%endfor
-
......
......@@ -155,11 +155,12 @@ class CapaHtmlRenderTest(unittest.TestCase):
question_element = rendered_html.find("p")
self.assertEqual(question_element.text, "Test question")
# Expect that the response has been turned into a <span>
response_element = rendered_html.find("span")
self.assertEqual(response_element.tag, "span")
# Expect that the response has been turned into a <section> with correct attributes
response_element = rendered_html.find("section")
self.assertEqual(response_element.tag, "section")
self.assertEqual(response_element.attrib["aria-label"], "Question 1")
# Expect that the response <span>
# Expect that the response <section>
# that contains a <div> for the textline
textline_element = response_element.find("div")
self.assertEqual(textline_element.text, 'Input Template Render')
......@@ -201,6 +202,29 @@ class CapaHtmlRenderTest(unittest.TestCase):
expected_calls
)
def test_correct_aria_label(self):
xml = """
<problem>
<choiceresponse>
<checkboxgroup>
<choice correct="true">over-suspicious</choice>
<choice correct="false">funny</choice>
</checkboxgroup>
</choiceresponse>
<choiceresponse>
<checkboxgroup>
<choice correct="true">Urdu</choice>
<choice correct="false">Finnish</choice>
</checkboxgroup>
</choiceresponse>
</problem>
"""
problem = new_loncapa_problem(xml)
rendered_html = etree.XML(problem.get_html())
sections = rendered_html.findall('section')
self.assertEqual(sections[0].attrib['aria-label'], 'Question 1')
self.assertEqual(sections[1].attrib['aria-label'], 'Question 2')
def test_render_response_with_overall_msg(self):
# CustomResponse script that sets an overall_message
script = textwrap.dedent("""
......
......@@ -122,13 +122,16 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
def setUp(self):
choices = [('1', 'choice 1'), ('2', 'choice 2'), ('3', 'choice 3')]
self.context = {'id': '1',
self.context = {
'id': '1',
'choices': choices,
'status': Status('correct'),
'label': 'test',
'input_type': 'checkbox',
'name_array_suffix': '1',
'value': '3'}
'value': '3',
'response_data': {'label': 'test'}
}
super(ChoiceGroupTemplateTest, self).setUp()
def test_problem_marked_correct(self):
......@@ -229,7 +232,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//label[@class='choicegroup_correct']"
xpath = "//label[contains(@class, 'choicegroup_correct')]"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark the whole problem
......@@ -250,7 +253,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
for test_conditions in conditions:
self.context.update(test_conditions)
xml = self.render_to_xml(self.context)
xpath = "//label[@class='choicegroup_incorrect']"
xpath = "//label[contains(@class, 'choicegroup_incorrect')]"
self.assert_has_xpath(xml, xpath, self.context)
# Should NOT mark the whole problem
......@@ -340,8 +343,8 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
def test_label(self):
xml = self.render_to_xml(self.context)
xpath = "//fieldset[@aria-label='%s']" % self.context['label']
self.assert_has_xpath(xml, xpath, self.context)
xpath = "//legend"
self.assert_has_text(xml, xpath, self.context['label'])
class TextlineTemplateTest(TemplateTestCase):
......
......@@ -628,10 +628,10 @@ class @Problem
choicegroup: (element, display, answers) =>
element = $(element)
input_id = element.attr('id').replace(/inputtype_/,'')
input_id = element.attr('id').replace(/inputtype_/, '')
answer = answers[input_id]
for choice in answer
element.find("label[for='input_#{input_id}_#{choice}']").addClass 'choicegroup_correct'
element.find("#input_#{input_id}_#{choice}").parent("label").addClass 'choicegroup_correct'
javascriptinput: (element, display, answers) =>
answer_id = $(element).attr('id').split("_")[1...].join("_")
......@@ -641,7 +641,7 @@ class @Problem
choicetextgroup: (element, display, answers) =>
element = $(element)
input_id = element.attr('id').replace(/inputtype_/,'')
input_id = element.attr('id').replace(/inputtype_/, '')
answer = answers[input_id]
for choice in answer
element.find("section#forinput#{choice}").addClass 'choicetextgroup_show_correct'
......@@ -821,4 +821,3 @@ class @Problem
]
hint_container.attr('hint_index', response.hint_index)
@$('.hint-button').focus() # a11y focus on click, like the Check button
......@@ -129,6 +129,11 @@ class ProblemPage(PageObject):
self.q(css='div.problem button.reset').click()
self.wait_for_ajax()
def click_show_hide_button(self):
""" Click the Show/Hide button. """
self.q(css='div.problem div.action .show').click()
self.wait_for_ajax()
def wait_for_status_icon(self):
"""
wait for status icon
......@@ -199,3 +204,20 @@ class ProblemPage(PageObject):
"""
self.wait_for_element_visibility('body > .tooltip', 'A tooltip is visible.')
return self.q(css='body > .tooltip').text[0]
def is_solution_tag_present(self):
"""
Check if solution/explanation is shown.
"""
solution_selector = '.solution-span div.detailed-solution'
return self.q(css=solution_selector).is_present()
def is_correct_choice_highlighted(self, correct_choices):
"""
Check if correct answer/choice highlighted for choice group.
"""
xpath = '//fieldset/div[contains(@class, "field")][{0}]/label[contains(@class, "choicegroup_correct")]'
for choice in correct_choices:
if not self.q(xpath=xpath.format(choice)).is_present():
return False
return True
......@@ -218,11 +218,11 @@ class CertificateProgressPageTest(UniqueCourseTest):
self.course_nav.q(css='select option[value="{}"]'.format('blue')).first.click()
# Select correct radio button for the answer
self.course_nav.q(css='fieldset label:nth-child(3) input').nth(0).click()
self.course_nav.q(css='fieldset div.field:nth-child(4) input').nth(0).click()
# Select correct radio buttons for the answer
self.course_nav.q(css='fieldset label:nth-child(1) input').nth(1).click()
self.course_nav.q(css='fieldset label:nth-child(3) input').nth(1).click()
self.course_nav.q(css='fieldset div.field:nth-child(2) input').nth(1).click()
self.course_nav.q(css='fieldset div.field:nth-child(4) input').nth(1).click()
# Submit the answer
self.course_nav.q(css='button.check.Check').click()
......
......@@ -51,7 +51,7 @@ class ProblemsTest(UniqueCourseTest):
email=self.email,
password=self.password,
course_id=self.course_id,
staff=False
staff=True
).visit()
def get_problem(self):
......
......@@ -324,7 +324,8 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
'question_text': 'The correct answer is Choice 0 and Choice 2',
'choice_type': 'checkbox',
'choices': [True, False, True, False],
'choice_names': ['Choice 0', 'Choice 1', 'Choice 2', 'Choice 3']
'choice_names': ['Choice 0', 'Choice 1', 'Choice 2', 'Choice 3'],
'explanation_text': 'This is explanation text'
}
def setUp(self, *args, **kwargs):
......@@ -332,15 +333,6 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
Additional setup for CheckboxProblemTypeTest
"""
super(CheckboxProblemTypeTest, self).setUp(*args, **kwargs)
self.problem_page.a11y_audit.config.set_rules({
'ignore': [
'section', # TODO: AC-491
'aria-allowed-attr', # TODO: AC-251
'aria-valid-attr', # TODO: AC-251
'aria-roles', # TODO: AC-251
'checkboxgroup', # TODO: AC-251
]
})
def answer_problem(self, correct):
"""
......@@ -352,6 +344,30 @@ class CheckboxProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
else:
self.problem_page.click_choice("choice_1")
@attr('shard_7')
def test_can_show_hide_answer(self):
"""
Scenario: Verifies that show/hide answer button is working as expected.
Given that I am on courseware page
And I can see a CAPA problem with show answer button
When I click "Show Answer" button
Then I should see "Hide Answer" text on button
And I should see question's solution
And I should see correct choices highlighted
When I click "Hide Answer" button
Then I should see "Show Answer" text on button
And I should not see question's solution
And I should not see correct choices highlighted
"""
self.problem_page.click_show_hide_button()
self.assertTrue(self.problem_page.is_solution_tag_present())
self.assertTrue(self.problem_page.is_correct_choice_highlighted(correct_choices=[1, 3]))
self.problem_page.click_show_hide_button()
self.assertFalse(self.problem_page.is_solution_tag_present())
self.assertFalse(self.problem_page.is_correct_choice_highlighted(correct_choices=[1, 3]))
class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
"""
......@@ -378,13 +394,6 @@ class MultipleChoiceProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
Additional setup for MultipleChoiceProblemTypeTest
"""
super(MultipleChoiceProblemTypeTest, self).setUp(*args, **kwargs)
self.problem_page.a11y_audit.config.set_rules({
'ignore': [
'section', # TODO: AC-491
'aria-valid-attr', # TODO: AC-251
'radiogroup', # TODO: AC-251
]
})
def answer_problem(self, correct):
"""
......@@ -422,13 +431,6 @@ class RadioProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
Additional setup for RadioProblemTypeTest
"""
super(RadioProblemTypeTest, self).setUp(*args, **kwargs)
self.problem_page.a11y_audit.config.set_rules({
'ignore': [
'section', # TODO: AC-491
'aria-valid-attr', # TODO: AC-292
'radiogroup', # TODO: AC-292
]
})
def answer_problem(self, correct):
"""
......@@ -798,13 +800,6 @@ class RadioTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTestMix
Additional setup for RadioTextProblemTypeTest
"""
super(RadioTextProblemTypeTest, self).setUp(*args, **kwargs)
self.problem_page.a11y_audit.config.set_rules({
'ignore': [
'section', # TODO: AC-491
'label', # TODO: AC-285
'radiogroup', # TODO: AC-285
]
})
class CheckboxTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTestMixin):
......@@ -831,13 +826,6 @@ class CheckboxTextProblemTypeTest(ChoiceTextProbelmTypeTestBase, ProblemTypeTest
Additional setup for CheckboxTextProblemTypeTest
"""
super(CheckboxTextProblemTypeTest, self).setUp(*args, **kwargs)
self.problem_page.a11y_audit.config.set_rules({
'ignore': [
'section', # TODO: AC-491
'label', # TODO: AC-284
'checkboxgroup', # TODO: AC-284
]
})
class ImageProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
......@@ -885,9 +873,9 @@ class SymbolicProblemTypeTest(ProblemTypeTestBase, ProblemTypeTestMixin):
}
status_indicators = {
'correct': ['span div.correct'],
'incorrect': ['span div.incorrect'],
'unanswered': ['span div.unanswered'],
'correct': ['div.capa_inputtype div.correct'],
'incorrect': ['div.capa_inputtype div.incorrect'],
'unanswered': ['div.capa_inputtype div.unanswered'],
}
def setUp(self, *args, **kwargs):
......
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