From 0773f0687df3db8a5de1698e31283cb8194e7aea Mon Sep 17 00:00:00 2001 From: "Dave St.Germain" Date: Thu, 13 Feb 2014 13:50:41 -0500 Subject: [PATCH] Answer checks should offer feedback to assistive tech. This commit adds a page level javascript SR object to enable reading of alert messages. LMS-2158 --- CHANGELOG.rst | 3 + common/lib/capa/capa/inputtypes.py | 36 +++++- .../capa/templates/chemicalequationinput.html | 28 +---- .../lib/capa/capa/templates/choicegroup.html | 39 +++--- .../lib/capa/capa/templates/choicetext.html | 10 +- common/lib/capa/capa/templates/codeinput.html | 21 ++-- .../capa/templates/formulaequationinput.html | 16 ++- .../lib/capa/capa/templates/matlabinput.html | 23 ++-- .../lib/capa/capa/templates/optioninput.html | 23 +--- common/lib/capa/capa/templates/textline.html | 43 +++---- .../lib/capa/capa/tests/test_html_render.py | 2 + .../capa/capa/tests/test_input_templates.py | 115 +++++++++++------- common/lib/capa/capa/tests/test_inputtypes.py | 65 ++++++---- .../xmodule/xmodule/css/sequence/display.scss | 1 - .../xmodule/js/spec/capa/display_spec.coffee | 8 +- .../xmodule/js/src/capa/display.coffee | 23 +++- .../xmodule/js/src/sequence/display.coffee | 6 +- common/static/js/src/accessibility_tools.js | 36 ++++++ .../courseware/features/problems.feature | 8 +- .../courseware/tests/test_masquerade.py | 4 +- .../sass/course/courseware/_courseware.scss | 6 - lms/templates/courseware/courseware.html | 2 +- lms/templates/modal/accessible_confirm.html | 3 +- lms/templates/problem.html | 4 +- lms/templates/seq_module.html | 2 +- 25 files changed, 310 insertions(+), 217 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 7effefe152..6b29678a81 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, in roughly chronological order, most recent first. Add your entries at or near the top. Include a label indicating the component affected. +LMS: Enabled screen reader feedback of problem responses. + LMS-2158 + Blades: Removed tooltip from captions. BLD-629. Blades: Fix problem with loading YouTube API is it is not available. BLD-531. diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 45d9e02765..0c48d6dd16 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -259,6 +259,8 @@ class InputTypeBase(object): 'id': self.input_id, 'value': self.value, 'status': self.status, + 'status_class': self.status_class, + 'status_display': self.status_display, 'msg': self.msg, 'STATIC_URL': self.capa_system.STATIC_URL, } @@ -268,6 +270,34 @@ class InputTypeBase(object): context.update(self._extra_context()) return context + @property + def status_class(self): + """ + Return the CSS class for the associated status. + """ + statuses = { + 'unsubmitted': 'unanswered', + 'incomplete': 'incorrect', + 'queued': 'processing', + } + return statuses.get(self.status, self.status) + + @property + def status_display(self): + """ + Return the human-readable and translated word for the associated status. + """ + _ = self.capa_system.i18n.ugettext + statuses = { + 'correct': _('correct'), + 'incorrect': _('incorrect'), + 'incomplete': _('incomplete'), + 'unanswered': _('unanswered'), + 'unsubmitted': _('unanswered'), + 'queued': _('queued'), + } + return statuses.get(self.status, self.status) + def _extra_context(self): """ Subclasses can override this to return extra context that should be passed to their templates for rendering. @@ -1135,16 +1165,10 @@ class FormulaEquationInput(InputTypeBase): TODO (vshnayder): Get rid of 'previewer' once we have a standard way of requiring js to be loaded. """ # `reported_status` is basically `status`, except we say 'unanswered' - reported_status = '' - if self.status == 'unsubmitted': - reported_status = 'unanswered' - elif self.status in ('correct', 'incorrect', 'incomplete'): - reported_status = self.status return { 'previewer': '{static_url}js/capa/src/formula_equation_preview.js'.format( static_url=self.capa_system.STATIC_URL), - 'reported_status': reported_status, } def handle_ajax(self, dispatch, get): diff --git a/common/lib/capa/capa/templates/chemicalequationinput.html b/common/lib/capa/capa/templates/chemicalequationinput.html index 07b8ef26f5..d6a2e36a9a 100644 --- a/common/lib/capa/capa/templates/chemicalequationinput.html +++ b/common/lib/capa/capa/templates/chemicalequationinput.html @@ -1,15 +1,7 @@ -
+
- % if status == 'unsubmitted': -
- % elif status == 'correct': -
- % elif status == 'incorrect': -
- % elif status == 'incomplete': -
- % endif +

- % if status == 'unsubmitted': - unanswered - % elif status == 'correct': - correct - % elif status == 'incorrect': - incorrect - % elif status == 'incomplete': - incomplete - % endif + ${value|h} - + ${status_display}

-
-
+

% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
% endif -
+ diff --git a/common/lib/capa/capa/templates/choicegroup.html b/common/lib/capa/capa/templates/choicegroup.html index 2a04003874..c91cfb0c57 100644 --- a/common/lib/capa/capa/templates/choicegroup.html +++ b/common/lib/capa/capa/templates/choicegroup.html @@ -1,19 +1,23 @@
% if input_type == 'checkbox' or not value: - % if status == 'unsubmitted' or show_correctness == 'never': - - % elif status == 'correct': - Status: correct - % elif status == 'incorrect': - Status: incorrect - % elif status == 'incomplete': - Status: incomplete - % endif + + + %for choice_id, choice_description in choices: + % if choice_id in value: + ${choice_description}, + %endif + %endfor + - + ${status_display} + + % endif
-
+
% for choice_id, choice_description in choices: diff --git a/common/lib/capa/capa/templates/choicetext.html b/common/lib/capa/capa/templates/choicetext.html index 55d18e7017..4dadb83d9e 100644 --- a/common/lib/capa/capa/templates/choicetext.html +++ b/common/lib/capa/capa/templates/choicetext.html @@ -10,15 +10,7 @@
% if input_type == 'checkbox' or not element_checked: - % if status == 'unsubmitted': - - % elif status == 'correct': - - % elif status == 'incorrect': - - % elif status == 'incomplete': - - % endif + % endif
diff --git a/common/lib/capa/capa/templates/codeinput.html b/common/lib/capa/capa/templates/codeinput.html index 024655b8f6..f213099e6b 100644 --- a/common/lib/capa/capa/templates/codeinput.html +++ b/common/lib/capa/capa/templates/codeinput.html @@ -16,27 +16,26 @@ >${value|h}
- % if status == 'unsubmitted': - Status: Unanswered - % elif status == 'correct': - Status: Correct - % elif status == 'incorrect': - Status: Incorrect - % elif status == 'queued': - Status: Queued - + + ${status_display} + + % if status == 'queued': + % endif % if hidden:
% endif -

${status}

+

${status_display}

-
+
${msg|n}
diff --git a/common/lib/capa/capa/templates/formulaequationinput.html b/common/lib/capa/capa/templates/formulaequationinput.html index 9606546b56..f5d9821232 100644 --- a/common/lib/capa/capa/templates/formulaequationinput.html +++ b/common/lib/capa/capa/templates/formulaequationinput.html @@ -1,6 +1,6 @@ <% doinline = 'style="display:inline-block;vertical-align:top"' if inline else "" %>
-
+
-

${reported_status}

+
\[\] diff --git a/common/lib/capa/capa/templates/matlabinput.html b/common/lib/capa/capa/templates/matlabinput.html index 519d208ee9..2296424e66 100644 --- a/common/lib/capa/capa/templates/matlabinput.html +++ b/common/lib/capa/capa/templates/matlabinput.html @@ -17,30 +17,29 @@ >${value|h}
- % if status == 'unsubmitted': - Status: Unanswered - % elif status == 'correct': - Status: Correct - % elif status == 'incorrect': - Status: Incorrect - % elif status == 'queued': - Status: Queued - + + ${status_display} + + % if status == 'queued': + % endif % if hidden:
% endif -

${status}

+

${status_display}

-
+
${msg|n}
-
+
${queue_msg|n}
diff --git a/common/lib/capa/capa/templates/optioninput.html b/common/lib/capa/capa/templates/optioninput.html index 55b539eca6..1546209cf1 100644 --- a/common/lib/capa/capa/templates/optioninput.html +++ b/common/lib/capa/capa/templates/optioninput.html @@ -12,25 +12,12 @@ % endfor - - - % if status == 'unsubmitted': - - Status: unsubmitted + + + ${value|h} - ${status_display} - % elif status == 'correct': - - Status: correct - - % elif status == 'incorrect': - - Status: incorrect - - % elif status == 'incomplete': - - Status: incomplete - - % endif % if msg: ${msg|n} diff --git a/common/lib/capa/capa/templates/textline.html b/common/lib/capa/capa/templates/textline.html index 0d0fd4eec0..c355c12e9f 100644 --- a/common/lib/capa/capa/templates/textline.html +++ b/common/lib/capa/capa/templates/textline.html @@ -1,20 +1,14 @@ <% doinline = "inline" if inline else "" %> -
+
% if preprocessor is not None:
% endif - % if status == 'unsubmitted': -
- % elif status == 'correct': -
- % elif status == 'incorrect': -
- % elif status == 'incomplete': -
+ % if status in ('unsubmitted', 'correct', 'incorrect', 'incomplete'): +
% endif % if hidden:
@@ -33,28 +27,29 @@ /> ${trailing_text | h} -

- % if status == 'unsubmitted': - unanswered - % elif status == 'correct': - correct - % elif status == 'incorrect': - incorrect - % elif status == 'incomplete': - incomplete - % endif +

-

+ % if do_math:
`{::}`
- + % endif -% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: +% if status in ('unsubmitted', 'correct', 'incorrect', 'incomplete'):
% endif @@ -62,4 +57,4 @@ ${msg|n} % endif -
+
diff --git a/common/lib/capa/capa/tests/test_html_render.py b/common/lib/capa/capa/tests/test_html_render.py index bf71e7fe38..a929c454c0 100644 --- a/common/lib/capa/capa/tests/test_html_render.py +++ b/common/lib/capa/capa/tests/test_html_render.py @@ -154,6 +154,8 @@ class CapaHtmlRenderTest(unittest.TestCase): expected_textline_context = { 'STATIC_URL': '/dummy-static/', 'status': 'unsubmitted', + 'status_class': 'unanswered', + 'status_display': u'unanswered', 'label': '', 'value': '', 'preprocessor': None, diff --git a/common/lib/capa/capa/tests/test_input_templates.py b/common/lib/capa/capa/tests/test_input_templates.py index a5a3d27753..6933a48b99 100644 --- a/common/lib/capa/capa/tests/test_input_templates.py +++ b/common/lib/capa/capa/tests/test_input_templates.py @@ -124,6 +124,8 @@ class ChoiceGroupTemplateTest(TemplateTestCase): self.context = {'id': '1', 'choices': choices, 'status': 'correct', + 'status_class': 'correct', + 'status_display': u'correct', 'label': 'test', 'input_type': 'checkbox', 'name_array_suffix': '1', @@ -136,13 +138,13 @@ class ChoiceGroupTemplateTest(TemplateTestCase): (not a particular option) is marked correct. """ - self.context['status'] = 'correct' + self.context['status'] = self.context['status_class'] = self.context['status_display'] = 'correct' self.context['input_type'] = 'checkbox' self.context['value'] = ['1', '2'] # Should mark the entire problem correct xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='correct']" + xpath = "//div[@class='indicator_container']/span[@class='status correct']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -158,19 +160,19 @@ class ChoiceGroupTemplateTest(TemplateTestCase): (not a particular option) is marked incorrect. """ conditions = [ - {'status': 'incorrect', 'input_type': 'radio', 'value': ''}, - {'status': 'incorrect', 'input_type': 'checkbox', 'value': []}, - {'status': 'incorrect', 'input_type': 'checkbox', 'value': ['2']}, - {'status': 'incorrect', 'input_type': 'checkbox', 'value': ['2', '3']}, - {'status': 'incomplete', 'input_type': 'radio', 'value': ''}, - {'status': 'incomplete', 'input_type': 'checkbox', 'value': []}, - {'status': 'incomplete', 'input_type': 'checkbox', 'value': ['2']}, - {'status': 'incomplete', 'input_type': 'checkbox', 'value': ['2', '3']}] + {'status': 'incorrect', 'input_type': 'radio', 'value': '', 'status_class': 'incorrect'}, + {'status': 'incorrect', 'input_type': 'checkbox', 'value': [], 'status_class': 'incorrect'}, + {'status': 'incorrect', 'input_type': 'checkbox', 'value': ['2'], 'status_class': 'incorrect'}, + {'status': 'incorrect', 'input_type': 'checkbox', 'value': ['2', '3'], 'status_class': 'incorrect'}, + {'status': 'incomplete', 'input_type': 'radio', 'value': '', 'status_class': 'incorrect'}, + {'status': 'incomplete', 'input_type': 'checkbox', 'value': [], 'status_class': 'incorrect'}, + {'status': 'incomplete', 'input_type': 'checkbox', 'value': ['2'], 'status_class': 'incorrect'}, + {'status': 'incomplete', 'input_type': 'checkbox', 'value': ['2', '3'], 'status_class': 'incorrect'}] for test_conditions in conditions: self.context.update(test_conditions) xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='incorrect']" + xpath = "//div[@class='indicator_container']/span[@class='status incorrect']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -188,21 +190,21 @@ class ChoiceGroupTemplateTest(TemplateTestCase): (not a particular option) is marked unanswered. """ conditions = [ - {'status': 'unsubmitted', 'input_type': 'radio', 'value': ''}, - {'status': 'unsubmitted', 'input_type': 'radio', 'value': []}, - {'status': 'unsubmitted', 'input_type': 'checkbox', 'value': []}, + {'status': 'unsubmitted', 'input_type': 'radio', 'value': '', 'status_class': 'unanswered'}, + {'status': 'unsubmitted', 'input_type': 'radio', 'value': [], 'status_class': 'unanswered'}, + {'status': 'unsubmitted', 'input_type': 'checkbox', 'value': [], 'status_class': 'unanswered'}, {'input_type': 'radio', 'value': ''}, {'input_type': 'radio', 'value': []}, {'input_type': 'checkbox', 'value': []}, {'input_type': 'checkbox', 'value': ['1']}, {'input_type': 'checkbox', 'value': ['1', '2']}] - self.context['status'] = 'unanswered' + self.context['status'] = self.context['status_class'] = 'unanswered' for test_conditions in conditions: self.context.update(test_conditions) xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='unanswered']" + xpath = "//div[@class='indicator_container']/span[@class='status unanswered']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -244,7 +246,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): {'input_type': 'radio', 'value': '2'}, {'input_type': 'radio', 'value': ['2']}] - self.context['status'] = 'incorrect' + self.context['status'] = self.context['status_class'] = 'incorrect' for test_conditions in conditions: self.context.update(test_conditions) @@ -268,16 +270,16 @@ class ChoiceGroupTemplateTest(TemplateTestCase): """ conditions = [ - {'input_type': 'radio', 'status': 'correct', 'value': ''}, - {'input_type': 'radio', 'status': 'correct', 'value': '2'}, - {'input_type': 'radio', 'status': 'correct', 'value': ['2']}, - {'input_type': 'radio', 'status': 'incorrect', 'value': '2'}, - {'input_type': 'radio', 'status': 'incorrect', 'value': []}, - {'input_type': 'radio', 'status': 'incorrect', 'value': ['2']}, - {'input_type': 'checkbox', 'status': 'correct', 'value': []}, - {'input_type': 'checkbox', 'status': 'correct', 'value': ['2']}, - {'input_type': 'checkbox', 'status': 'incorrect', 'value': []}, - {'input_type': 'checkbox', 'status': 'incorrect', 'value': ['2']}] + {'input_type': 'radio', 'status': 'correct', 'value': '', 'status_class': 'correct'}, + {'input_type': 'radio', 'status': 'correct', 'value': '2', 'status_class': 'correct'}, + {'input_type': 'radio', 'status': 'correct', 'value': ['2'], 'status_class': 'correct'}, + {'input_type': 'radio', 'status': 'incorrect', 'value': '2', 'status_class': 'incorrect'}, + {'input_type': 'radio', 'status': 'incorrect', 'value': [], 'status_class': 'incorrect'}, + {'input_type': 'radio', 'status': 'incorrect', 'value': ['2'], 'status_class': 'incorrect'}, + {'input_type': 'checkbox', 'status': 'correct', 'value': [], 'status_class': 'correct'}, + {'input_type': 'checkbox', 'status': 'correct', 'value': ['2'], 'status_class': 'correct'}, + {'input_type': 'checkbox', 'status': 'incorrect', 'value': [], 'status_class': 'incorrect'}, + {'input_type': 'checkbox', 'status': 'incorrect', 'value': ['2'], 'status_class': 'incorrect'}] self.context['show_correctness'] = 'never' self.context['submitted_message'] = 'Test message' @@ -287,10 +289,10 @@ class ChoiceGroupTemplateTest(TemplateTestCase): xml = self.render_to_xml(self.context) # Should NOT mark the entire problem correct/incorrect - xpath = "//div[@class='indicator_container']/span[@class='correct']" + xpath = "//div[@class='indicator_container']/span[@class='status correct']" self.assert_no_xpath(xml, xpath, self.context) - xpath = "//div[@class='indicator_container']/span[@class='incorrect']" + xpath = "//div[@class='indicator_container']/span[@class='status incorrect']" self.assert_no_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -353,6 +355,8 @@ class TextlineTemplateTest(TemplateTestCase): def setUp(self): self.context = {'id': '1', 'status': 'correct', + 'status_class': 'correct', + 'status_display': u'correct', 'label': 'test', 'value': '3', 'preprocessor': None, @@ -369,7 +373,7 @@ class TextlineTemplateTest(TemplateTestCase): base_context = self.context.copy() base_context.update(context) xml = self.render_to_xml(base_context) - xpath = "//section[@class='%s']" % css_class + xpath = "//div[@class='%s']" % css_class self.assert_has_xpath(xml, xpath, self.context) def test_status(self): @@ -380,6 +384,8 @@ class TextlineTemplateTest(TemplateTestCase): for (context_status, div_class, status_mark) in cases: self.context['status'] = context_status + self.context['status_class'] = div_class + self.context['status_display'] = status_mark xml = self.render_to_xml(self.context) # Expect that we get a
with correct class @@ -456,6 +462,7 @@ class TextlineTemplateTest(TemplateTestCase): for (context_status, div_class) in cases: self.context['status'] = context_status + self.context['status_class'] = div_class xml = self.render_to_xml(self.context) # Expect that we get a
with correct class @@ -481,6 +488,8 @@ class FormulaEquationInputTemplateTest(TemplateTestCase): 'id': 2, 'value': 'PREFILLED_VALUE', 'status': 'unsubmitted', + 'status_class': 'unanswered', + 'status_display': u'unsubmitted', 'label': 'test', 'previewer': 'file.js', 'reported_status': 'REPORTED_STATUS', @@ -517,6 +526,8 @@ class AnnotationInputTemplateTest(TemplateTestCase): 'has_options_value': False, 'debug': False, 'status': 'unsubmitted', + 'status_class': 'unanswered', + 'status_display': u'unsubmitted', 'return_to_annotation': False, 'msg': '

This is a test message

', } super(AnnotationInputTemplateTest, self).setUp() @@ -673,7 +684,15 @@ class OptionInputTemplateTest(TemplateTestCase): TEMPLATE_NAME = 'optioninput.html' def setUp(self): - self.context = {'id': 2, 'options': [], 'status': 'unsubmitted', 'label': 'test', 'value': 0} + self.context = { + 'id': 2, + 'options': [], + 'status': 'unsubmitted', + 'status_class': 'unanswered', + 'status_display': u'unanswered', + 'label': 'test', + 'value': 0 + } super(OptionInputTemplateTest, self).setUp() def test_select_options(self): @@ -704,13 +723,14 @@ class OptionInputTemplateTest(TemplateTestCase): # Test cases, where each tuple represents # `(input_status, expected_css_class)` - test_cases = [('unsubmitted', 'unanswered'), - ('correct', 'correct'), - ('incorrect', 'incorrect'), - ('incomplete', 'incorrect')] + test_cases = [('unsubmitted', 'status unanswered'), + ('correct', 'status correct'), + ('incorrect', 'status incorrect'), + ('incomplete', 'status incorrect')] for (input_status, expected_css_class) in test_cases: self.context['status'] = input_status + self.context['status_class'] = expected_css_class.split(' ')[1] xml = self.render_to_xml(self.context) xpath = "//span[@class='{0}']".format(expected_css_class) @@ -795,6 +815,8 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase): self.context = {'id': '1', 'choices': choices, 'status': 'correct', + 'status_class': 'correct', + 'status_display': u'correct', 'input_type': 'radio', 'label': 'choicetext label', 'value': self.VALUE_DICT} @@ -826,7 +848,7 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase): # Should mark the entire problem correct xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='correct']" + xpath = "//div[@class='indicator_container']/span[@class='status correct']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -841,19 +863,19 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase): (not a particular option) is marked incorrect""" grouping_tags = {'radio': 'label', 'checkbox': 'section'} conditions = [ - {'status': 'incorrect', 'input_type': 'radio', 'value': {}}, - {'status': 'incorrect', 'input_type': 'checkbox', 'value': self.WRONG_CHOICE_CHECKBOX}, - {'status': 'incorrect', 'input_type': 'checkbox', 'value': self.BOTH_CHOICE_CHECKBOX}, - {'status': 'incorrect', 'input_type': 'checkbox', 'value': self.VALUE_DICT}, - {'status': 'incomplete', 'input_type': 'radio', 'value': {}}, - {'status': 'incomplete', 'input_type': 'checkbox', 'value': self.WRONG_CHOICE_CHECKBOX}, - {'status': 'incomplete', 'input_type': 'checkbox', 'value': self.BOTH_CHOICE_CHECKBOX}, - {'status': 'incomplete', 'input_type': 'checkbox', 'value': self.VALUE_DICT}] + {'status': 'incorrect', 'status_class': 'incorrect', 'input_type': 'radio', 'value': {}}, + {'status': 'incorrect', 'status_class': 'incorrect', 'input_type': 'checkbox', 'value': self.WRONG_CHOICE_CHECKBOX}, + {'status': 'incorrect', 'status_class': 'incorrect', 'input_type': 'checkbox', 'value': self.BOTH_CHOICE_CHECKBOX}, + {'status': 'incorrect', 'status_class': 'incorrect', 'input_type': 'checkbox', 'value': self.VALUE_DICT}, + {'status': 'incomplete', 'status_class': 'incorrect', 'input_type': 'radio', 'value': {}}, + {'status': 'incomplete', 'status_class': 'incorrect', 'input_type': 'checkbox', 'value': self.WRONG_CHOICE_CHECKBOX}, + {'status': 'incomplete', 'status_class': 'incorrect', 'input_type': 'checkbox', 'value': self.BOTH_CHOICE_CHECKBOX}, + {'status': 'incomplete', 'status_class': 'incorrect', 'input_type': 'checkbox', 'value': self.VALUE_DICT}] for test_conditions in conditions: self.context.update(test_conditions) xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='incorrect']" + xpath = "//div[@class='indicator_container']/span[@class='status incorrect']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options @@ -880,11 +902,12 @@ class ChoiceTextGroupTemplateTest(TemplateTestCase): {'status': 'unsubmitted', 'input_type': 'checkbox', 'value': self.BOTH_CHOICE_CHECKBOX}] self.context['status'] = 'unanswered' + self.context['status_class'] = 'unanswered' for test_conditions in conditions: self.context.update(test_conditions) xml = self.render_to_xml(self.context) - xpath = "//div[@class='indicator_container']/span[@class='unanswered']" + xpath = "//div[@class='indicator_container']/span[@class='status unanswered']" self.assert_has_xpath(xml, xpath, self.context) # Should NOT mark individual options diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index ec20156727..0450f71103 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -57,6 +57,8 @@ class OptionInputTest(unittest.TestCase): 'value': 'Down', 'options': [('Up', 'Up'), ('Down', 'Down'), ('Don\'t know', 'Don\'t know')], 'status': 'answered', + 'status_class': 'answered', + 'status_display': 'answered', 'label': '', 'msg': '', 'inline': False, @@ -117,6 +119,8 @@ class ChoiceGroupTest(unittest.TestCase): 'id': 'sky_input', 'value': 'foil3', 'status': 'answered', + 'status_class': 'answered', + 'status_display': 'answered', 'label': '', 'msg': '', 'input_type': expected_input_type, @@ -170,6 +174,8 @@ class JavascriptInputTest(unittest.TestCase): 'STATIC_URL': '/dummy-static/', 'id': 'prob_1_2', 'status': 'unanswered', + 'status_class': 'unanswered', + 'status_display': u'unanswered', # 'label': '', 'msg': '', 'value': '3', @@ -203,6 +209,8 @@ class TextLineTest(unittest.TestCase): 'id': 'prob_1_2', 'value': 'BumbleBee', 'status': 'unanswered', + 'status_class': 'unanswered', + 'status_display': u'unanswered', 'label': 'testing 123', 'size': size, 'msg': '', @@ -235,6 +243,8 @@ class TextLineTest(unittest.TestCase): 'id': 'prob_1_2', 'value': 'BumbleBee', 'status': 'unanswered', + 'status_class': 'unanswered', + 'status_display': u'unanswered', 'label': '', 'size': size, 'msg': '', @@ -279,6 +289,8 @@ class TextLineTest(unittest.TestCase): 'id': 'prob_1_2', 'value': 'BumbleBee', 'status': 'unanswered', + 'status_class': 'unanswered', + 'status_display': u'unanswered', 'label': '', 'size': size, 'msg': '', @@ -320,6 +332,8 @@ class FileSubmissionTest(unittest.TestCase): 'STATIC_URL': '/dummy-static/', 'id': 'prob_1_2', 'status': 'queued', + 'status_class': 'processing', + 'status_display': u'queued', 'label': '', 'msg': the_input.submitted_msg, 'value': 'BumbleBee.py', @@ -370,6 +384,8 @@ class CodeInputTest(unittest.TestCase): 'id': 'prob_1_2', 'value': 'print "good evening"', 'status': 'queued', + 'status_class': 'processing', + 'status_display': u'queued', # 'label': '', 'msg': the_input.submitted_msg, 'mode': mode, @@ -424,6 +440,8 @@ class MatlabTest(unittest.TestCase): 'id': 'prob_1_2', 'value': 'print "good evening"', 'status': 'queued', + 'status_class': 'processing', + 'status_display': u'queued', # 'label': '', 'msg': self.the_input.submitted_msg, 'mode': self.mode, @@ -455,6 +473,8 @@ class MatlabTest(unittest.TestCase): 'id': 'prob_1_2', 'value': 'print "good evening"', 'status': 'queued', + 'status_class': 'processing', + 'status_display': u'queued', # 'label': '', 'msg': the_input.submitted_msg, 'mode': self.mode, @@ -486,6 +506,8 @@ class MatlabTest(unittest.TestCase): 'id': 'prob_1_2', 'value': 'print "good evening"', 'status': status, + 'status_class': status, + 'status_display': unicode(status), # 'label': '', 'msg': '', 'mode': self.mode, @@ -516,6 +538,8 @@ class MatlabTest(unittest.TestCase): 'id': 'prob_1_2', 'value': 'print "good evening"', 'status': 'queued', + 'status_class': 'processing', + 'status_display': u'queued', # 'label': '', 'msg': the_input.submitted_msg, 'mode': self.mode, @@ -593,7 +617,7 @@ class MatlabTest(unittest.TestCase): output = self.the_input.get_html() self.assertEqual( etree.tostring(output), - """
{\'status\': \'queued\', \'button_enabled\': True, \'rows\': \'10\', \'queue_len\': \'3\', \'mode\': \'\', \'cols\': \'80\', \'STATIC_URL\': \'/dummy-static/\', \'linenumbers\': \'true\', \'queue_msg\': \'\', \'value\': \'print "good evening"\', \'msg\': u\'Submitted. As soon as a response is returned, this message will be replaced by that feedback.\', \'matlab_editor_js\': \'/dummy-static/js/vendor/CodeMirror/addons/octave.js\', \'hidden\': \'\', \'id\': \'prob_1_2\', \'tabsize\': 4}
""" + """
{\'status\': \'queued\', \'button_enabled\': True, \'linenumbers\': \'true\', \'rows\': \'10\', \'queue_len\': \'3\', \'mode\': \'\', \'cols\': \'80\', \'value\': \'print "good evening"\', \'status_class\': \'processing\', \'queue_msg\': \'\', \'STATIC_URL\': \'/dummy-static/\', \'msg\': u\'Submitted. As soon as a response is returned, this message will be replaced by that feedback.\', \'matlab_editor_js\': \'/dummy-static/js/vendor/CodeMirror/addons/octave.js\', \'hidden\': \'\', \'status_display\': u\'queued\', \'id\': \'prob_1_2\', \'tabsize\': 4}
""" ) # test html, that is correct HTML5 html, but is not parsable by XML parser. @@ -661,6 +685,8 @@ class SchematicTest(unittest.TestCase): 'id': 'prob_1_2', 'value': value, 'status': 'unsubmitted', + 'status_class': 'unanswered', + 'status_display': u'unanswered', 'label': '', 'msg': '', 'initial_value': initial_value, @@ -704,6 +730,8 @@ class ImageInputTest(unittest.TestCase): 'id': 'prob_1_2', 'value': value, 'status': 'unsubmitted', + 'status_class': 'unanswered', + 'status_display': u'unanswered', 'label': '', 'width': width, 'height': height, @@ -759,6 +787,8 @@ class CrystallographyTest(unittest.TestCase): 'id': 'prob_1_2', 'value': value, 'status': 'unsubmitted', + 'status_class': 'unanswered', + 'status_display': u'unanswered', # 'label': '', 'msg': '', 'width': width, @@ -801,6 +831,8 @@ class VseprTest(unittest.TestCase): 'id': 'prob_1_2', 'value': value, 'status': 'unsubmitted', + 'status_class': 'unanswered', + 'status_display': u'unanswered', 'msg': '', 'width': width, 'height': height, @@ -833,6 +865,8 @@ class ChemicalEquationTest(unittest.TestCase): 'id': 'prob_1_2', 'value': 'H2OYeah', 'status': 'unanswered', + 'status_class': 'unanswered', + 'status_display': 'unanswered', 'label': '', 'msg': '', 'size': self.size, @@ -921,8 +955,9 @@ class FormulaEquationTest(unittest.TestCase): 'id': 'prob_1_2', 'value': 'x^2+1/2', 'status': 'unanswered', + 'status_class': 'unanswered', + 'status_display': u'unanswered', 'label': '', - 'reported_status': '', 'msg': '', 'size': self.size, 'previewer': '/dummy-static/js/capa/src/formula_equation_preview.js', @@ -930,24 +965,6 @@ class FormulaEquationTest(unittest.TestCase): } self.assertEqual(context, expected) - def test_rendering_reported_status(self): - """ - Verify that the 'reported status' matches expectations. - """ - test_values = { - '': '', # Default - 'unsubmitted': 'unanswered', - 'correct': 'correct', - 'incorrect': 'incorrect', - 'incomplete': 'incomplete', - 'not a status': '' - } - - for self_status, reported_status in test_values.iteritems(): - self.the_input.status = self_status - context = self.the_input._get_render_context() # pylint: disable=W0212 - self.assertEqual(context['reported_status'], reported_status) - def test_formcalc_ajax_sucess(self): """ Verify that using the correct dispatch and valid data produces a valid response @@ -1069,6 +1086,8 @@ class DragAndDropTest(unittest.TestCase): 'id': 'prob_1_2', 'value': value, 'status': 'unsubmitted', + 'status_class': 'unanswered', + 'status_display': u'unanswered', # 'label': '', 'msg': '', 'drag_and_drop_json': json.dumps(user_input) @@ -1122,6 +1141,8 @@ class AnnotationInputTest(unittest.TestCase): 'id': 'annotation_input', 'value': value, 'status': 'answered', + 'status_class': 'answered', + 'status_display': 'answered', # 'label': '', 'msg': '', 'title': 'foo', @@ -1181,7 +1202,9 @@ class TestChoiceText(unittest.TestCase): state = { 'value': '{}', 'id': 'choicetext_input', - 'status': 'answered' + 'status': 'answered', + 'status_class': 'answered', + 'status_display': u'answered', } first_input = self.build_choice_element('numtolerance_input', 'choiceinput_0_textinput_0', 'false', '') diff --git a/common/lib/xmodule/xmodule/css/sequence/display.scss b/common/lib/xmodule/xmodule/css/sequence/display.scss index adb37eb182..90ab82feb9 100644 --- a/common/lib/xmodule/xmodule/css/sequence/display.scss +++ b/common/lib/xmodule/xmodule/css/sequence/display.scss @@ -402,4 +402,3 @@ nav.sequence-bottom { } */ } - diff --git a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee index 79ce05250d..baf860dc53 100644 --- a/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee +++ b/common/lib/xmodule/xmodule/js/spec/capa/display_spec.coffee @@ -7,6 +7,10 @@ describe 'Problem', -> @stubbedJax = root: jasmine.createSpyObj('jax.root', ['toMathML']) MathJax.Hub.getAllJax.andReturn [@stubbedJax] window.update_schematics = -> + # mock the screen reader alert + window.SR = + readElts: `function(){}` + readText: `function(){}` # Load this function from spec/helper.coffee # Note that if your test fails with a message like: @@ -232,7 +236,7 @@ describe 'Problem', -> it 'toggle the show answer button', -> spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {}) @problem.show() - expect($('.show .show-label')).toHaveText 'Hide Answer(s)' + expect($('.show .show-label')).toHaveText 'Hide Answer' it 'add the showed class to element', -> spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {}) @@ -431,7 +435,7 @@ describe 'Problem', -> it 'toggle the show answer button', -> @problem.show() - expect($('.show .show-label')).toHaveText 'Show Answer(s)' + expect($('.show .show-label')).toHaveText 'Show Answer' it 'remove the showed class from element', -> @problem.show() diff --git a/common/lib/xmodule/xmodule/js/src/capa/display.coffee b/common/lib/xmodule/xmodule/js/src/capa/display.coffee index ece63e7f7d..cfe8e67273 100644 --- a/common/lib/xmodule/xmodule/js/src/capa/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/capa/display.coffee @@ -129,11 +129,13 @@ class @Problem render: (content) -> if content + @el.attr({'aria-busy': 'true', 'aria-live': 'off', 'aria-atomic': 'false'}) @el.html(content) JavascriptLoader.executeModuleScripts @el, () => @setupInputTypes() @bind() @queueing() + @el.attr('aria-busy', 'false') else $.postWithPrefix "#{@url}/problem_get", (response) => @el.html(response.html) @@ -226,7 +228,8 @@ class @Problem required_files.splice(required_files.indexOf(file.name), 1) if file.size > max_filesize file_too_large = true - errors.push 'Your file "' + file.name '" is too large (max size: ' + max_filesize/(1000*1000) + ' MB)' + max_size = max_filesize / (1000*1000) + errors.push "Your file #{file.name} is too large (max size: {max_size}MB)" fd.append(element.id, file) if element.files.length == 0 file_not_selected = true @@ -281,10 +284,12 @@ class @Problem $.postWithPrefix "#{@url}/problem_check", @answers, (response) => switch response.success when 'incorrect', 'correct' + window.SR.readElts($(response.contents).find('.status')) @render(response.contents) @updateProgress response if @el.hasClass 'showed' @el.removeClass 'showed' + @$('div.action input.check').focus() else @gentle_alert response.success Logger.log 'problem_graded', [@answers, response.contents], @id @@ -301,16 +306,23 @@ class @Problem show: => if !@el.hasClass 'showed' Logger.log 'problem_show', problem: @id + answer_text = [] $.postWithPrefix "#{@url}/problem_show", (response) => answers = response.answers $.each answers, (key, value) => if $.isArray(value) for choice in value @$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' + answer_text.push('

' + gettext('Answer:') + ' ' + value + '

') else answer = @$("#answer_#{key}, #solution_#{key}") answer.html(value) Collapsible.setCollapsibles(answer) + solution = $(value).find('.detailed-solution') + if solution.length + answer_text.push(solution) + else + answer_text.push('

' + gettext('Answer:') + ' ' + value + '

') # TODO remove the above once everything is extracted into its own # inputtype functions. @@ -327,15 +339,19 @@ class @Problem MathJax.Hub.Queue ["Typeset", MathJax.Hub, element] `// Translators: the word Answer here refers to the answer to a problem the student must solve.` - @$('.show-label').text gettext('Hide Answer(s)') + @$('.show-label').text gettext('Hide Answer') + @$('.show-label .sr').text gettext('Hide Answer') @el.addClass 'showed' @updateProgress response + window.SR.readElts(answer_text) else @$('[id^=answer_], [id^=solution_]').text '' @$('[correct_answer]').attr correct_answer: null @el.removeClass 'showed' `// Translators: the word Answer here refers to the answer to a problem the student must solve.` - @$('.show-label').text gettext('Show Answer(s)') + @$('.show-label').text gettext('Show Answer') + @$('.show-label .sr').text gettext('Reveal Answer') + window.SR.readText(gettext('Answer hidden')) @el.find(".capa_inputtype").each (index, inputtype) => display = @inputtypeDisplays[$(inputtype).attr('id')] @@ -350,6 +366,7 @@ class @Problem alert_elem = "
" + msg + "
" @el.find('.action').after(alert_elem) @el.find('.capa_alert').css(opacity: 0).animate(opacity: 1, 700) + window.SR.readElts(msg) save: => if not @check_save_waitfor(@save_internal) diff --git a/common/lib/xmodule/xmodule/js/src/sequence/display.coffee b/common/lib/xmodule/xmodule/js/src/sequence/display.coffee index 4f9a33329b..4363043789 100644 --- a/common/lib/xmodule/xmodule/js/src/sequence/display.coffee +++ b/common/lib/xmodule/xmodule/js/src/sequence/display.coffee @@ -193,10 +193,12 @@ class @Sequence mark_active: (position) -> # Mark the correct tab as selected, for a11y helpfulness. - @$("#sequence-list a[aria-selected='true']").attr("aria-selected", "false") + @$('#sequence-list [role="tab"]').attr({ + 'aria-selected' : null + }); # Don't overwrite class attribute to avoid changing Progress class element = @link_for(position) element.removeClass("inactive") .removeClass("visited") .addClass("active") - .attr("aria-selected", "true") + .attr({"aria-selected": "true", 'tabindex': '0'}) diff --git a/common/static/js/src/accessibility_tools.js b/common/static/js/src/accessibility_tools.js index 64137c2bfa..5d626568b0 100644 --- a/common/static/js/src/accessibility_tools.js +++ b/common/static/js/src/accessibility_tools.js @@ -141,3 +141,39 @@ $('.nav-skip').keypress(function(e) { } } }); + +// Creates a window level SR object that can be used for giving audible feedback to screen readers. +$(function(){ + var SRAlert; + + SRAlert = (function() { + + function SRAlert() { + $('body').append(''); + this.el = $('#reader-feedback'); + } + + SRAlert.prototype.clear = function() { + return this.el.html(' '); + }; + + SRAlert.prototype.readElts = function(elts) { + var feedback, + _this = this; + feedback = ''; + $.each(elts, function(idx, value) { + return feedback += '

' + $(value).html() + '

\n'; + }); + return this.el.html(feedback); + }; + + SRAlert.prototype.readText = function(text) { + return this.el.text(text); + }; + + return SRAlert; + + })(); + + window.SR = new SRAlert; +}); diff --git a/lms/djangoapps/courseware/features/problems.feature b/lms/djangoapps/courseware/features/problems.feature index 960cfc4a0a..3b08be7b34 100644 --- a/lms/djangoapps/courseware/features/problems.feature +++ b/lms/djangoapps/courseware/features/problems.feature @@ -130,11 +130,11 @@ Feature: LMS.Answer problems Scenario: I can view and hide the answer if the problem has it: Given I am viewing a "numerical" that shows the answer "always" - When I press the button with the label "Show Answer(s)" - Then the Show/Hide button label is "Hide Answer(s)" + When I press the button with the label "Show Answer" + Then the Show/Hide button label is "Hide Answer" And I should see "4.14159" somewhere in the page - When I press the button with the label "Hide Answer(s)" - Then the Show/Hide button label is "Show Answer(s)" + When I press the button with the label "Hide Answer" + Then the Show/Hide button label is "Show Answer" And I should not see "4.14159" anywhere on the page Scenario: I can see my score on a problem when I answer it and after I reset it diff --git a/lms/djangoapps/courseware/tests/test_masquerade.py b/lms/djangoapps/courseware/tests/test_masquerade.py index 4caa73da41..abdbccf889 100644 --- a/lms/djangoapps/courseware/tests/test_masquerade.py +++ b/lms/djangoapps/courseware/tests/test_masquerade.py @@ -105,7 +105,7 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) resp = self.get_problem() html = json.loads(resp.content)['html'] print html - sabut = '' + sabut = '' self.assertTrue(sabut in html) def test_no_showanswer_for_student(self): @@ -115,5 +115,5 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) resp = self.get_problem() html = json.loads(resp.content)['html'] - sabut = '' + sabut = '' self.assertFalse(sabut in html) diff --git a/lms/static/sass/course/courseware/_courseware.scss b/lms/static/sass/course/courseware/_courseware.scss index 0e10292bc6..5daaef6885 100644 --- a/lms/static/sass/course/courseware/_courseware.scss +++ b/lms/static/sass/course/courseware/_courseware.scss @@ -232,12 +232,6 @@ div.course-wrapper { } -.xblock { - &:focus { - outline: 0; - } -} - textarea.short-form-response { height: 200px; padding: 5px; diff --git a/lms/templates/courseware/courseware.html b/lms/templates/courseware/courseware.html index 386dd974db..835a5df0df 100644 --- a/lms/templates/courseware/courseware.html +++ b/lms/templates/courseware/courseware.html @@ -183,7 +183,7 @@ ${fragment.foot_html()}
% if accordion: -
+