Commit 0773f068 by Dave St.Germain

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
parent 021e6f5b
...@@ -5,6 +5,9 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -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 in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. 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: Removed tooltip from captions. BLD-629.
Blades: Fix problem with loading YouTube API is it is not available. BLD-531. Blades: Fix problem with loading YouTube API is it is not available. BLD-531.
......
...@@ -259,6 +259,8 @@ class InputTypeBase(object): ...@@ -259,6 +259,8 @@ class InputTypeBase(object):
'id': self.input_id, 'id': self.input_id,
'value': self.value, 'value': self.value,
'status': self.status, 'status': self.status,
'status_class': self.status_class,
'status_display': self.status_display,
'msg': self.msg, 'msg': self.msg,
'STATIC_URL': self.capa_system.STATIC_URL, 'STATIC_URL': self.capa_system.STATIC_URL,
} }
...@@ -268,6 +270,34 @@ class InputTypeBase(object): ...@@ -268,6 +270,34 @@ class InputTypeBase(object):
context.update(self._extra_context()) context.update(self._extra_context())
return 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): def _extra_context(self):
""" """
Subclasses can override this to return extra context that should be passed to their templates for rendering. Subclasses can override this to return extra context that should be passed to their templates for rendering.
...@@ -1135,16 +1165,10 @@ class FormulaEquationInput(InputTypeBase): ...@@ -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. 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` 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 { return {
'previewer': '{static_url}js/capa/src/formula_equation_preview.js'.format( 'previewer': '{static_url}js/capa/src/formula_equation_preview.js'.format(
static_url=self.capa_system.STATIC_URL), static_url=self.capa_system.STATIC_URL),
'reported_status': reported_status,
} }
def handle_ajax(self, dispatch, get): def handle_ajax(self, dispatch, get):
......
<section id="chemicalequationinput_${id}" class="chemicalequationinput"> <div id="chemicalequationinput_${id}" class="chemicalequationinput">
<div class="script_placeholder" data-src="${previewer}"/> <div class="script_placeholder" data-src="${previewer}"/>
% if status == 'unsubmitted': <div class="${status_class}" id="status_${id}">
<div class="unanswered" id="status_${id}">
% elif status == 'correct':
<div class="correct" id="status_${id}">
% elif status == 'incorrect':
<div class="incorrect" id="status_${id}">
% elif status == 'incomplete':
<div class="incorrect" id="status_${id}">
% endif
<input type="text" name="input_${id}" id="input_${id}" aria-label="${label}" aria-describedby="answer_${id}" data-input-id="${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-label="${label}" aria-describedby="answer_${id}" data-input-id="${id}" value="${value|h}"
% if size: % if size:
...@@ -18,22 +10,14 @@ ...@@ -18,22 +10,14 @@
/> />
<p class="status" aria-describedby="input_${id}"> <p class="status" aria-describedby="input_${id}">
% if status == 'unsubmitted': ${value|h} -
unanswered ${status_display}
% elif status == 'correct':
correct
% elif status == 'incorrect':
incorrect
% elif status == 'incomplete':
incomplete
% endif
</p> </p>
<div id="input_${id}_preview" class="equation"> <div id="input_${id}_preview" class="equation"></div>
</div>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']:
</div> </div>
% endif % endif
</section> </div>
<form class="choicegroup capa_inputtype" id="inputtype_${id}"> <form class="choicegroup capa_inputtype" id="inputtype_${id}">
<div class="indicator_container"> <div class="indicator_container">
% if input_type == 'checkbox' or not value: % if input_type == 'checkbox' or not value:
% if status == 'unsubmitted' or show_correctness == 'never': <span class="status ${status_class if show_correctness != 'never' else 'unanswered'}"
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span> id="status_${id}"
% elif status == 'correct': aria-describedby="inputtype_${id}">
<span class="correct" id="status_${id}"><span class="sr">Status: correct</span></span> <span class="sr">
% elif status == 'incorrect': %for choice_id, choice_description in choices:
<span class="incorrect" id="status_${id}"><span class="sr">Status: incorrect</span></span> % if choice_id in value:
% elif status == 'incomplete': ${choice_description},
<span class="incorrect" id="status_${id}"><span class="sr">Status: incomplete</span></span> %endif
% endif %endfor
-
${status_display}
</span>
</span>
% endif % endif
</div> </div>
<fieldset role="radiogroup" aria-label="${label}"> <fieldset role="${input_type}group" aria-label="${label}">
% for choice_id, choice_description in choices: % for choice_id, choice_description in choices:
<label for="input_${id}_${choice_id}" <label for="input_${id}_${choice_id}"
...@@ -39,20 +43,15 @@ ...@@ -39,20 +43,15 @@
% elif input_type != 'radio' and choice_id in value: % elif input_type != 'radio' and choice_id in value:
checked="true" checked="true"
% endif % endif
% if input_type != 'radio':
aria-multiselectable="true"
% endif
/> ${choice_description} /> ${choice_description}
% if input_type == 'radio' and ( (isinstance(value, basestring) and (choice_id == value)) or (not isinstance(value, basestring) and choice_id in value) ): % 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', 'incorrect') and not show_correctness=='never':
if status == 'correct': <span class="sr status">${choice_description|h} - ${status_display}</span>
correctness = 'correct'
elif status == 'incorrect':
correctness = 'incorrect'
else:
correctness = None
%>
% if correctness and not show_correctness=='never':
<span class="sr" aria-describedby="input_${id}_${choice_id}">Status: ${correctness}</span>
% endif % endif
% endif % endif
</label> </label>
......
...@@ -10,15 +10,7 @@ ...@@ -10,15 +10,7 @@
<div class="script_placeholder" data-src="/static/js/capa/choicetextinput.js"/> <div class="script_placeholder" data-src="/static/js/capa/choicetextinput.js"/>
<div class="indicator_container"> <div class="indicator_container">
% if input_type == 'checkbox' or not element_checked: % if input_type == 'checkbox' or not element_checked:
% if status == 'unsubmitted': <span class="status ${status_class}" id="status_${id}"></span>
<span class="unanswered" style="display:inline-block;" id="status_${id}"></span>
% elif status == 'correct':
<span class="correct" id="status_${id}"></span>
% elif status == 'incorrect':
<span class="incorrect" id="status_${id}"></span>
% elif status == 'incomplete':
<span class="incorrect" id="status_${id}"></span>
% endif
% endif % endif
</div> </div>
......
...@@ -16,27 +16,26 @@ ...@@ -16,27 +16,26 @@
>${value|h}</textarea> >${value|h}</textarea>
<div class="grader-status" tabindex="-1"> <div class="grader-status" tabindex="-1">
% if status == 'unsubmitted': <span id="status_${id}"
<span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Unanswered</span> class="${status_class}"
% elif status == 'correct': aria-describedby="input_${id}"
<span class="correct" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Correct</span> >
% elif status == 'incorrect': <span class="status sr">${status_display}</span>
<span class="incorrect" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Incorrect</span> </span>
% elif status == 'queued': % if status == 'queued':
<span class="processing" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Queued</span> <span style="display:none;" class="xqueue" id="${id}">${queue_len}</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
% if hidden: % if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<p class="debug">${status}</p> <p class="debug">${status_display}</p>
</div> </div>
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
<div class="external-grader-message"> <div class="external-grader-message" aria-live="polite">
${msg|n} ${msg|n}
</div> </div>
</section> </section>
<% doinline = 'style="display:inline-block;vertical-align:top"' if inline else "" %> <% doinline = 'style="display:inline-block;vertical-align:top"' if inline else "" %>
<section id="formulaequationinput_${id}" class="inputtype formulaequationinput" ${doinline}> <section id="formulaequationinput_${id}" class="inputtype formulaequationinput" ${doinline}>
<div class="${reported_status}" id="status_${id}"> <div class="${status_class}" id="status_${id}">
<input type="text" name="input_${id}" id="input_${id}" <input type="text" name="input_${id}" id="input_${id}"
data-input-id="${id}" value="${value|h}" data-input-id="${id}" value="${value|h}"
aria-label="${label}" aria-label="${label}"
...@@ -9,7 +9,19 @@ ...@@ -9,7 +9,19 @@
% endif % endif
/> />
<p class="status">${reported_status}</p> <p class="status"
%if status != 'unsubmitted':
aria-hidden="true"
%endif
>
<span class="sr equation">
%if value:
${value|h}
% else:
${label}
%endif
</span> - ${status_display}
</p>
<div id="input_${id}_preview" class="equation"> <div id="input_${id}_preview" class="equation">
\[\] \[\]
......
...@@ -17,30 +17,29 @@ ...@@ -17,30 +17,29 @@
>${value|h}</textarea> >${value|h}</textarea>
<div class="grader-status" tabindex="-1"> <div class="grader-status" tabindex="-1">
% if status == 'unsubmitted': <span id="status_${id}"
<span class="unanswered" style="display:inline-block;" id="status_${id}"><span class="sr">Status: </span>Unanswered</span> class="${status_class}"
% elif status == 'correct': aria-describedby="input_${id}"
<span class="correct" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Correct</span> >
% elif status == 'incorrect': <span class="status sr">${status_display}</span>
<span class="incorrect" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Incorrect</span> </span>
% elif status == 'queued': % if status == 'queued':
<span class="processing" id="status_${id}" aria-describedby="input_${id}"><span class="sr">Status: </span>Queued</span> <span style="display:none;" class="xqueue" id="${id}">${queue_len}</span>
<span style="display:none;" class="xqueue" id="${id}" >${queue_len}</span>
% endif % endif
% if hidden: % if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
% endif % endif
<p class="debug">${status}</p> <p class="debug">${status_display}</p>
</div> </div>
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
<div class="external-grader-message"> <div class="external-grader-message" aria-live="polite">
${msg|n} ${msg|n}
</div> </div>
<div class="external-grader-message"> <div class="external-grader-message" aria-live="polite">
${queue_msg|n} ${queue_msg|n}
</div> </div>
......
...@@ -12,25 +12,12 @@ ...@@ -12,25 +12,12 @@
% endfor % endfor
</select> </select>
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
<span class="status ${status_class}"
% if status == 'unsubmitted': id="status_${id}"
<span class="unanswered" style="display:inline-block;" id="status_${id}" aria-describedby="input_${id}"> aria-describedby="input_${id}">
<span class="sr">Status: unsubmitted</span> <span class="sr">${value|h} - ${status_display}</span>
</span>
% elif status == 'correct':
<span class="correct" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: correct</span>
</span>
% elif status == 'incorrect':
<span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incorrect</span>
</span> </span>
% elif status == 'incomplete':
<span class="incorrect" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">Status: incomplete</span>
</span>
% endif
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
......
<% doinline = "inline" if inline else "" %> <% doinline = "inline" if inline else "" %>
<section id="inputtype_${id}" class="${'text-input-dynamath' if do_math else ''} capa_inputtype ${doinline} textline" > <div id="inputtype_${id}" class="${'text-input-dynamath' if do_math else ''} capa_inputtype ${doinline} textline" >
% if preprocessor is not None: % if preprocessor is not None:
<div class="text-input-dynamath_data ${doinline}" data-preprocessor="${preprocessor['class_name']}"/> <div class="text-input-dynamath_data ${doinline}" data-preprocessor="${preprocessor['class_name']}"/>
<div class="script_placeholder" data-src="${preprocessor['script_src']}"/> <div class="script_placeholder" data-src="${preprocessor['script_src']}"/>
% endif % endif
% if status == 'unsubmitted': % if status in ('unsubmitted', 'correct', 'incorrect', 'incomplete'):
<div class="unanswered ${doinline}" id="status_${id}"> <div class="${status_class} ${doinline}" id="status_${id}">
% elif status == 'correct':
<div class="correct ${doinline}" id="status_${id}">
% elif status == 'incorrect':
<div class="incorrect ${doinline}" id="status_${id}">
% elif status == 'incomplete':
<div class="incorrect ${doinline}" id="status_${id}">
% endif % endif
% if hidden: % if hidden:
<div style="display:none;" name="${hidden}" inputid="input_${id}" /> <div style="display:none;" name="${hidden}" inputid="input_${id}" />
...@@ -33,28 +27,29 @@ ...@@ -33,28 +27,29 @@
/> />
${trailing_text | h} ${trailing_text | h}
<p class="status" aria-describedby="input_${id}"> <p class="status"
% if status == 'unsubmitted': %if status != 'unsubmitted':
unanswered aria-hidden="true"
% elif status == 'correct': %endif
correct aria-describedby="input_${id}">
% elif status == 'incorrect': %if value:
incorrect ${value|h}
% elif status == 'incomplete': % else:
incomplete ${label}
% endif %endif
-
${status_display}
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer" aria-hidden="true"></p>
% if do_math: % if do_math:
<div id="display_${id}" class="equation">`{::}`</div> <div id="display_${id}" class="equation">`{::}`</div>
<textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"> <textarea style="display:none" id="input_${id}_dynamath" name="input_${id}_dynamath"></textarea>
</textarea>
% endif % endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: % if status in ('unsubmitted', 'correct', 'incorrect', 'incomplete'):
</div> </div>
% endif % endif
...@@ -62,4 +57,4 @@ ...@@ -62,4 +57,4 @@
<span class="message">${msg|n}</span> <span class="message">${msg|n}</span>
% endif % endif
</section> </div>
...@@ -154,6 +154,8 @@ class CapaHtmlRenderTest(unittest.TestCase): ...@@ -154,6 +154,8 @@ class CapaHtmlRenderTest(unittest.TestCase):
expected_textline_context = { expected_textline_context = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'status': 'unsubmitted', 'status': 'unsubmitted',
'status_class': 'unanswered',
'status_display': u'unanswered',
'label': '', 'label': '',
'value': '', 'value': '',
'preprocessor': None, 'preprocessor': None,
......
...@@ -57,6 +57,8 @@ class OptionInputTest(unittest.TestCase): ...@@ -57,6 +57,8 @@ class OptionInputTest(unittest.TestCase):
'value': 'Down', 'value': 'Down',
'options': [('Up', 'Up'), ('Down', 'Down'), ('Don\'t know', 'Don\'t know')], 'options': [('Up', 'Up'), ('Down', 'Down'), ('Don\'t know', 'Don\'t know')],
'status': 'answered', 'status': 'answered',
'status_class': 'answered',
'status_display': 'answered',
'label': '', 'label': '',
'msg': '', 'msg': '',
'inline': False, 'inline': False,
...@@ -117,6 +119,8 @@ class ChoiceGroupTest(unittest.TestCase): ...@@ -117,6 +119,8 @@ class ChoiceGroupTest(unittest.TestCase):
'id': 'sky_input', 'id': 'sky_input',
'value': 'foil3', 'value': 'foil3',
'status': 'answered', 'status': 'answered',
'status_class': 'answered',
'status_display': 'answered',
'label': '', 'label': '',
'msg': '', 'msg': '',
'input_type': expected_input_type, 'input_type': expected_input_type,
...@@ -170,6 +174,8 @@ class JavascriptInputTest(unittest.TestCase): ...@@ -170,6 +174,8 @@ class JavascriptInputTest(unittest.TestCase):
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': 'prob_1_2',
'status': 'unanswered', 'status': 'unanswered',
'status_class': 'unanswered',
'status_display': u'unanswered',
# 'label': '', # 'label': '',
'msg': '', 'msg': '',
'value': '3', 'value': '3',
...@@ -203,6 +209,8 @@ class TextLineTest(unittest.TestCase): ...@@ -203,6 +209,8 @@ class TextLineTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': 'BumbleBee', 'value': 'BumbleBee',
'status': 'unanswered', 'status': 'unanswered',
'status_class': 'unanswered',
'status_display': u'unanswered',
'label': 'testing 123', 'label': 'testing 123',
'size': size, 'size': size,
'msg': '', 'msg': '',
...@@ -235,6 +243,8 @@ class TextLineTest(unittest.TestCase): ...@@ -235,6 +243,8 @@ class TextLineTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': 'BumbleBee', 'value': 'BumbleBee',
'status': 'unanswered', 'status': 'unanswered',
'status_class': 'unanswered',
'status_display': u'unanswered',
'label': '', 'label': '',
'size': size, 'size': size,
'msg': '', 'msg': '',
...@@ -279,6 +289,8 @@ class TextLineTest(unittest.TestCase): ...@@ -279,6 +289,8 @@ class TextLineTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': 'BumbleBee', 'value': 'BumbleBee',
'status': 'unanswered', 'status': 'unanswered',
'status_class': 'unanswered',
'status_display': u'unanswered',
'label': '', 'label': '',
'size': size, 'size': size,
'msg': '', 'msg': '',
...@@ -320,6 +332,8 @@ class FileSubmissionTest(unittest.TestCase): ...@@ -320,6 +332,8 @@ class FileSubmissionTest(unittest.TestCase):
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': 'prob_1_2',
'status': 'queued', 'status': 'queued',
'status_class': 'processing',
'status_display': u'queued',
'label': '', 'label': '',
'msg': the_input.submitted_msg, 'msg': the_input.submitted_msg,
'value': 'BumbleBee.py', 'value': 'BumbleBee.py',
...@@ -370,6 +384,8 @@ class CodeInputTest(unittest.TestCase): ...@@ -370,6 +384,8 @@ class CodeInputTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': 'print "good evening"', 'value': 'print "good evening"',
'status': 'queued', 'status': 'queued',
'status_class': 'processing',
'status_display': u'queued',
# 'label': '', # 'label': '',
'msg': the_input.submitted_msg, 'msg': the_input.submitted_msg,
'mode': mode, 'mode': mode,
...@@ -424,6 +440,8 @@ class MatlabTest(unittest.TestCase): ...@@ -424,6 +440,8 @@ class MatlabTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': 'print "good evening"', 'value': 'print "good evening"',
'status': 'queued', 'status': 'queued',
'status_class': 'processing',
'status_display': u'queued',
# 'label': '', # 'label': '',
'msg': self.the_input.submitted_msg, 'msg': self.the_input.submitted_msg,
'mode': self.mode, 'mode': self.mode,
...@@ -455,6 +473,8 @@ class MatlabTest(unittest.TestCase): ...@@ -455,6 +473,8 @@ class MatlabTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': 'print "good evening"', 'value': 'print "good evening"',
'status': 'queued', 'status': 'queued',
'status_class': 'processing',
'status_display': u'queued',
# 'label': '', # 'label': '',
'msg': the_input.submitted_msg, 'msg': the_input.submitted_msg,
'mode': self.mode, 'mode': self.mode,
...@@ -486,6 +506,8 @@ class MatlabTest(unittest.TestCase): ...@@ -486,6 +506,8 @@ class MatlabTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': 'print "good evening"', 'value': 'print "good evening"',
'status': status, 'status': status,
'status_class': status,
'status_display': unicode(status),
# 'label': '', # 'label': '',
'msg': '', 'msg': '',
'mode': self.mode, 'mode': self.mode,
...@@ -516,6 +538,8 @@ class MatlabTest(unittest.TestCase): ...@@ -516,6 +538,8 @@ class MatlabTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': 'print "good evening"', 'value': 'print "good evening"',
'status': 'queued', 'status': 'queued',
'status_class': 'processing',
'status_display': u'queued',
# 'label': '', # 'label': '',
'msg': the_input.submitted_msg, 'msg': the_input.submitted_msg,
'mode': self.mode, 'mode': self.mode,
...@@ -593,7 +617,7 @@ class MatlabTest(unittest.TestCase): ...@@ -593,7 +617,7 @@ class MatlabTest(unittest.TestCase):
output = self.the_input.get_html() output = self.the_input.get_html()
self.assertEqual( self.assertEqual(
etree.tostring(output), etree.tostring(output),
"""<div>{\'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}</div>""" """<div>{\'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}</div>"""
) )
# test html, that is correct HTML5 html, but is not parsable by XML parser. # test html, that is correct HTML5 html, but is not parsable by XML parser.
...@@ -661,6 +685,8 @@ class SchematicTest(unittest.TestCase): ...@@ -661,6 +685,8 @@ class SchematicTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': value, 'value': value,
'status': 'unsubmitted', 'status': 'unsubmitted',
'status_class': 'unanswered',
'status_display': u'unanswered',
'label': '', 'label': '',
'msg': '', 'msg': '',
'initial_value': initial_value, 'initial_value': initial_value,
...@@ -704,6 +730,8 @@ class ImageInputTest(unittest.TestCase): ...@@ -704,6 +730,8 @@ class ImageInputTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': value, 'value': value,
'status': 'unsubmitted', 'status': 'unsubmitted',
'status_class': 'unanswered',
'status_display': u'unanswered',
'label': '', 'label': '',
'width': width, 'width': width,
'height': height, 'height': height,
...@@ -759,6 +787,8 @@ class CrystallographyTest(unittest.TestCase): ...@@ -759,6 +787,8 @@ class CrystallographyTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': value, 'value': value,
'status': 'unsubmitted', 'status': 'unsubmitted',
'status_class': 'unanswered',
'status_display': u'unanswered',
# 'label': '', # 'label': '',
'msg': '', 'msg': '',
'width': width, 'width': width,
...@@ -801,6 +831,8 @@ class VseprTest(unittest.TestCase): ...@@ -801,6 +831,8 @@ class VseprTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': value, 'value': value,
'status': 'unsubmitted', 'status': 'unsubmitted',
'status_class': 'unanswered',
'status_display': u'unanswered',
'msg': '', 'msg': '',
'width': width, 'width': width,
'height': height, 'height': height,
...@@ -833,6 +865,8 @@ class ChemicalEquationTest(unittest.TestCase): ...@@ -833,6 +865,8 @@ class ChemicalEquationTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': 'H2OYeah', 'value': 'H2OYeah',
'status': 'unanswered', 'status': 'unanswered',
'status_class': 'unanswered',
'status_display': 'unanswered',
'label': '', 'label': '',
'msg': '', 'msg': '',
'size': self.size, 'size': self.size,
...@@ -921,8 +955,9 @@ class FormulaEquationTest(unittest.TestCase): ...@@ -921,8 +955,9 @@ class FormulaEquationTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': 'x^2+1/2', 'value': 'x^2+1/2',
'status': 'unanswered', 'status': 'unanswered',
'status_class': 'unanswered',
'status_display': u'unanswered',
'label': '', 'label': '',
'reported_status': '',
'msg': '', 'msg': '',
'size': self.size, 'size': self.size,
'previewer': '/dummy-static/js/capa/src/formula_equation_preview.js', 'previewer': '/dummy-static/js/capa/src/formula_equation_preview.js',
...@@ -930,24 +965,6 @@ class FormulaEquationTest(unittest.TestCase): ...@@ -930,24 +965,6 @@ class FormulaEquationTest(unittest.TestCase):
} }
self.assertEqual(context, expected) 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): def test_formcalc_ajax_sucess(self):
""" """
Verify that using the correct dispatch and valid data produces a valid response Verify that using the correct dispatch and valid data produces a valid response
...@@ -1069,6 +1086,8 @@ class DragAndDropTest(unittest.TestCase): ...@@ -1069,6 +1086,8 @@ class DragAndDropTest(unittest.TestCase):
'id': 'prob_1_2', 'id': 'prob_1_2',
'value': value, 'value': value,
'status': 'unsubmitted', 'status': 'unsubmitted',
'status_class': 'unanswered',
'status_display': u'unanswered',
# 'label': '', # 'label': '',
'msg': '', 'msg': '',
'drag_and_drop_json': json.dumps(user_input) 'drag_and_drop_json': json.dumps(user_input)
...@@ -1122,6 +1141,8 @@ class AnnotationInputTest(unittest.TestCase): ...@@ -1122,6 +1141,8 @@ class AnnotationInputTest(unittest.TestCase):
'id': 'annotation_input', 'id': 'annotation_input',
'value': value, 'value': value,
'status': 'answered', 'status': 'answered',
'status_class': 'answered',
'status_display': 'answered',
# 'label': '', # 'label': '',
'msg': '', 'msg': '',
'title': 'foo', 'title': 'foo',
...@@ -1181,7 +1202,9 @@ class TestChoiceText(unittest.TestCase): ...@@ -1181,7 +1202,9 @@ class TestChoiceText(unittest.TestCase):
state = { state = {
'value': '{}', 'value': '{}',
'id': 'choicetext_input', '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', '') first_input = self.build_choice_element('numtolerance_input', 'choiceinput_0_textinput_0', 'false', '')
......
...@@ -402,4 +402,3 @@ nav.sequence-bottom { ...@@ -402,4 +402,3 @@ nav.sequence-bottom {
} }
*/ */
} }
...@@ -7,6 +7,10 @@ describe 'Problem', -> ...@@ -7,6 +7,10 @@ describe 'Problem', ->
@stubbedJax = root: jasmine.createSpyObj('jax.root', ['toMathML']) @stubbedJax = root: jasmine.createSpyObj('jax.root', ['toMathML'])
MathJax.Hub.getAllJax.andReturn [@stubbedJax] MathJax.Hub.getAllJax.andReturn [@stubbedJax]
window.update_schematics = -> window.update_schematics = ->
# mock the screen reader alert
window.SR =
readElts: `function(){}`
readText: `function(){}`
# Load this function from spec/helper.coffee # Load this function from spec/helper.coffee
# Note that if your test fails with a message like: # Note that if your test fails with a message like:
...@@ -232,7 +236,7 @@ describe 'Problem', -> ...@@ -232,7 +236,7 @@ describe 'Problem', ->
it 'toggle the show answer button', -> it 'toggle the show answer button', ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {}) spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {})
@problem.show() @problem.show()
expect($('.show .show-label')).toHaveText 'Hide Answer(s)' expect($('.show .show-label')).toHaveText 'Hide Answer'
it 'add the showed class to element', -> it 'add the showed class to element', ->
spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {}) spyOn($, 'postWithPrefix').andCallFake (url, callback) -> callback(answers: {})
...@@ -431,7 +435,7 @@ describe 'Problem', -> ...@@ -431,7 +435,7 @@ describe 'Problem', ->
it 'toggle the show answer button', -> it 'toggle the show answer button', ->
@problem.show() @problem.show()
expect($('.show .show-label')).toHaveText 'Show Answer(s)' expect($('.show .show-label')).toHaveText 'Show Answer'
it 'remove the showed class from element', -> it 'remove the showed class from element', ->
@problem.show() @problem.show()
......
...@@ -129,11 +129,13 @@ class @Problem ...@@ -129,11 +129,13 @@ class @Problem
render: (content) -> render: (content) ->
if content if content
@el.attr({'aria-busy': 'true', 'aria-live': 'off', 'aria-atomic': 'false'})
@el.html(content) @el.html(content)
JavascriptLoader.executeModuleScripts @el, () => JavascriptLoader.executeModuleScripts @el, () =>
@setupInputTypes() @setupInputTypes()
@bind() @bind()
@queueing() @queueing()
@el.attr('aria-busy', 'false')
else else
$.postWithPrefix "#{@url}/problem_get", (response) => $.postWithPrefix "#{@url}/problem_get", (response) =>
@el.html(response.html) @el.html(response.html)
...@@ -226,7 +228,8 @@ class @Problem ...@@ -226,7 +228,8 @@ class @Problem
required_files.splice(required_files.indexOf(file.name), 1) required_files.splice(required_files.indexOf(file.name), 1)
if file.size > max_filesize if file.size > max_filesize
file_too_large = true 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) fd.append(element.id, file)
if element.files.length == 0 if element.files.length == 0
file_not_selected = true file_not_selected = true
...@@ -281,10 +284,12 @@ class @Problem ...@@ -281,10 +284,12 @@ class @Problem
$.postWithPrefix "#{@url}/problem_check", @answers, (response) => $.postWithPrefix "#{@url}/problem_check", @answers, (response) =>
switch response.success switch response.success
when 'incorrect', 'correct' when 'incorrect', 'correct'
window.SR.readElts($(response.contents).find('.status'))
@render(response.contents) @render(response.contents)
@updateProgress response @updateProgress response
if @el.hasClass 'showed' if @el.hasClass 'showed'
@el.removeClass 'showed' @el.removeClass 'showed'
@$('div.action input.check').focus()
else else
@gentle_alert response.success @gentle_alert response.success
Logger.log 'problem_graded', [@answers, response.contents], @id Logger.log 'problem_graded', [@answers, response.contents], @id
...@@ -301,16 +306,23 @@ class @Problem ...@@ -301,16 +306,23 @@ class @Problem
show: => show: =>
if !@el.hasClass 'showed' if !@el.hasClass 'showed'
Logger.log 'problem_show', problem: @id Logger.log 'problem_show', problem: @id
answer_text = []
$.postWithPrefix "#{@url}/problem_show", (response) => $.postWithPrefix "#{@url}/problem_show", (response) =>
answers = response.answers answers = response.answers
$.each answers, (key, value) => $.each answers, (key, value) =>
if $.isArray(value) if $.isArray(value)
for choice in value for choice in value
@$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true' @$("label[for='input_#{key}_#{choice}']").attr correct_answer: 'true'
answer_text.push('<p>' + gettext('Answer:') + ' ' + value + '</p>')
else else
answer = @$("#answer_#{key}, #solution_#{key}") answer = @$("#answer_#{key}, #solution_#{key}")
answer.html(value) answer.html(value)
Collapsible.setCollapsibles(answer) Collapsible.setCollapsibles(answer)
solution = $(value).find('.detailed-solution')
if solution.length
answer_text.push(solution)
else
answer_text.push('<p>' + gettext('Answer:') + ' ' + value + '</p>')
# TODO remove the above once everything is extracted into its own # TODO remove the above once everything is extracted into its own
# inputtype functions. # inputtype functions.
...@@ -327,15 +339,19 @@ class @Problem ...@@ -327,15 +339,19 @@ class @Problem
MathJax.Hub.Queue ["Typeset", MathJax.Hub, element] MathJax.Hub.Queue ["Typeset", MathJax.Hub, element]
`// Translators: the word Answer here refers to the answer to a problem the student must solve.` `// 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' @el.addClass 'showed'
@updateProgress response @updateProgress response
window.SR.readElts(answer_text)
else else
@$('[id^=answer_], [id^=solution_]').text '' @$('[id^=answer_], [id^=solution_]').text ''
@$('[correct_answer]').attr correct_answer: null @$('[correct_answer]').attr correct_answer: null
@el.removeClass 'showed' @el.removeClass 'showed'
`// Translators: the word Answer here refers to the answer to a problem the student must solve.` `// 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) => @el.find(".capa_inputtype").each (index, inputtype) =>
display = @inputtypeDisplays[$(inputtype).attr('id')] display = @inputtypeDisplays[$(inputtype).attr('id')]
...@@ -350,6 +366,7 @@ class @Problem ...@@ -350,6 +366,7 @@ class @Problem
alert_elem = "<div class='capa_alert'>" + msg + "</div>" alert_elem = "<div class='capa_alert'>" + msg + "</div>"
@el.find('.action').after(alert_elem) @el.find('.action').after(alert_elem)
@el.find('.capa_alert').css(opacity: 0).animate(opacity: 1, 700) @el.find('.capa_alert').css(opacity: 0).animate(opacity: 1, 700)
window.SR.readElts(msg)
save: => save: =>
if not @check_save_waitfor(@save_internal) if not @check_save_waitfor(@save_internal)
......
...@@ -193,10 +193,12 @@ class @Sequence ...@@ -193,10 +193,12 @@ class @Sequence
mark_active: (position) -> mark_active: (position) ->
# Mark the correct tab as selected, for a11y helpfulness. # 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 # Don't overwrite class attribute to avoid changing Progress class
element = @link_for(position) element = @link_for(position)
element.removeClass("inactive") element.removeClass("inactive")
.removeClass("visited") .removeClass("visited")
.addClass("active") .addClass("active")
.attr("aria-selected", "true") .attr({"aria-selected": "true", 'tabindex': '0'})
...@@ -141,3 +141,39 @@ $('.nav-skip').keypress(function(e) { ...@@ -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('<div id="reader-feedback" class="sr" style="display:none" aria-hidden="false" aria-atomic="true" aria-live="assertive"></div>');
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 += '<p>' + $(value).html() + '</p>\n';
});
return this.el.html(feedback);
};
SRAlert.prototype.readText = function(text) {
return this.el.text(text);
};
return SRAlert;
})();
window.SR = new SRAlert;
});
...@@ -130,11 +130,11 @@ Feature: LMS.Answer problems ...@@ -130,11 +130,11 @@ Feature: LMS.Answer problems
Scenario: I can view and hide the answer if the problem has it: Scenario: I can view and hide the answer if the problem has it:
Given I am viewing a "numerical" that shows the answer "always" Given I am viewing a "numerical" that shows the answer "always"
When I press the button with the label "Show Answer(s)" When I press the button with the label "Show Answer"
Then the Show/Hide button label is "Hide Answer(s)" Then the Show/Hide button label is "Hide Answer"
And I should see "4.14159" somewhere in the page And I should see "4.14159" somewhere in the page
When I press the button with the label "Hide Answer(s)" When I press the button with the label "Hide Answer"
Then the Show/Hide button label is "Show Answer(s)" Then the Show/Hide button label is "Show Answer"
And I should not see "4.14159" anywhere on the page 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 Scenario: I can see my score on a problem when I answer it and after I reset it
......
...@@ -105,7 +105,7 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) ...@@ -105,7 +105,7 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
resp = self.get_problem() resp = self.get_problem()
html = json.loads(resp.content)['html'] html = json.loads(resp.content)['html']
print html print html
sabut = '<button class="show"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>' sabut = '<button class="show"><span class="show-label" aria-hidden="true">Show Answer</span> <span class="sr">Reveal Answer</span></button>'
self.assertTrue(sabut in html) self.assertTrue(sabut in html)
def test_no_showanswer_for_student(self): def test_no_showanswer_for_student(self):
...@@ -115,5 +115,5 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase) ...@@ -115,5 +115,5 @@ class TestStaffMasqueradeAsStudent(ModuleStoreTestCase, LoginEnrollmentTestCase)
resp = self.get_problem() resp = self.get_problem()
html = json.loads(resp.content)['html'] html = json.loads(resp.content)['html']
sabut = '<button class="show"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>' sabut = '<button class="show"><span class="show-label" aria-hidden="true">Show Answer</span> <span class="sr">Reveal answer above</span></button>'
self.assertFalse(sabut in html) self.assertFalse(sabut in html)
...@@ -232,12 +232,6 @@ div.course-wrapper { ...@@ -232,12 +232,6 @@ div.course-wrapper {
} }
.xblock {
&:focus {
outline: 0;
}
}
textarea.short-form-response { textarea.short-form-response {
height: 200px; height: 200px;
padding: 5px; padding: 5px;
......
...@@ -183,7 +183,7 @@ ${fragment.foot_html()} ...@@ -183,7 +183,7 @@ ${fragment.foot_html()}
<div class="course-wrapper"> <div class="course-wrapper">
% if accordion: % if accordion:
<div class="course-index"> <div class="course-index" role="navigation">
<header id="open_close_accordion"> <header id="open_close_accordion">
<a href="#">${_("close")}</a> <a href="#">${_("close")}</a>
</header> </header>
......
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
<i class="icon-remove"></i> <i class="icon-remove"></i>
<span class="sr"> <span class="sr">
## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen) ## Translators: this is a control to allow users to exit out of this modal interface (a menu or piece of UI that takes the full focus of the screen)
${_('Close Modal')} ${_('Close')}
</span> </span>
</button> </button>
...@@ -43,6 +43,5 @@ var accessible_confirm = function(message, callback) { ...@@ -43,6 +43,5 @@ var accessible_confirm = function(message, callback) {
accessible_modal("#accessibile-confirm-modal #confirm_open_button", "#accessibile-confirm-modal .close-modal", "#accessibile-confirm-modal", ".content-wrapper"); accessible_modal("#accessibile-confirm-modal #confirm_open_button", "#accessibile-confirm-modal .close-modal", "#accessibile-confirm-modal", ".content-wrapper");
$("#accessibile-confirm-modal #confirm_open_button").click(); $("#accessibile-confirm-modal #confirm_open_button").click();
$("#accessibile-confirm-modal .message-title").html(message); $("#accessibile-confirm-modal .message-title").html(message);
// SR.readText(message);
}; };
</script> </script>
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
<div class="problem-progress"></div> <div class="problem-progress"></div>
<div class="problem"> <div class="problem" role="application">
${ problem['html'] } ${ problem['html'] }
<div class="action"> <div class="action">
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
<input class="save" type="button" value="${_('Save')}" /> <input class="save" type="button" value="${_('Save')}" />
% endif % endif
% if answer_available: % if answer_available:
<button class="show"><span class="show-label">${_('Show Answer(s)')}</span> <span class="sr">${_("(for question(s) above - adjacent to each field)")}</span></button> <button class="show"><span class="show-label" aria-hidden="true">${_('Show Answer')}</span> <span class="sr">${_("Reveal Answer")}</span></button>
% endif % endif
% if attempts_allowed : % if attempts_allowed :
<div class="submission_feedback"> <div class="submission_feedback">
......
...@@ -24,7 +24,7 @@ ...@@ -24,7 +24,7 @@
id="tab_${idx}" id="tab_${idx}"
tabindex="0" tabindex="0"
role="tab"> role="tab">
<p aria-hidden="false">${item['title']}<span class="sr" aria-hidden="true">, ${item['type']}</span></p> <p aria-hidden="false">${item['title']}</p>
</a> </a>
</li> </li>
% endfor % endfor
......
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