Commit 055fd6ea by Albert St. Aubin

Refactored the status icon and span to a unified template and updated tests

parent 04fa2a42
...@@ -39,25 +39,27 @@ graded status as'status' ...@@ -39,25 +39,27 @@ graded status as'status'
# makes sense, but a bunch of problems have markup that assumes block. Bigger TODO: figure out a # makes sense, but a bunch of problems have markup that assumes block. Bigger TODO: figure out a
# general css and layout strategy for capa, document it, then implement it. # general css and layout strategy for capa, document it, then implement it.
import time
import json import json
import logging import logging
from lxml import etree
import re
import shlex # for splitting quoted strings import shlex # for splitting quoted strings
import sys import sys
import pyparsing import time
import html5lib from datetime import datetime
import bleach
from .util import sanitize_html import bleach
from .registry import TagRegistry import html5lib
from chem import chemcalc import pyparsing
import re
from calc.preview import latex_preview from calc.preview import latex_preview
from chem import chemcalc
from lxml import etree
from openedx.core.djangolib.markup import HTML, Text
import xqueue_interface import xqueue_interface
from xqueue_interface import XQUEUE_TIMEOUT
from datetime import datetime
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
from capa.xqueue_interface import XQUEUE_TIMEOUT
from .registry import TagRegistry
from .util import sanitize_html
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -232,7 +234,8 @@ class InputTypeBase(object): ...@@ -232,7 +234,8 @@ class InputTypeBase(object):
# put hint above msg if it should be displayed # put hint above msg if it should be displayed
if self.hintmode == 'always': if self.hintmode == 'always':
self.msg = self.hint + ('<br/>' if self.msg else '') + self.msg self.msg = HTML('{hint}<br/>{msg}' if self.msg else '{hint}').format(hint=HTML(self.hint),
msg=HTML(self.msg))
self.status = state.get('status', 'unanswered') self.status = state.get('status', 'unanswered')
...@@ -322,15 +325,18 @@ class InputTypeBase(object): ...@@ -322,15 +325,18 @@ class InputTypeBase(object):
'msg': self.msg, 'msg': self.msg,
'response_data': self.response_data, 'response_data': self.response_data,
'STATIC_URL': self.capa_system.STATIC_URL, 'STATIC_URL': self.capa_system.STATIC_URL,
'describedby_html': '', 'describedby_html': HTML(''),
} }
# Don't add aria-describedby attribute if there are no descriptions # Generate the list of ids to be used with the aria-describedby field.
if self.response_data.get('descriptions'): # Every list should contain the status id
description_ids = ' '.join(self.response_data.get('descriptions').keys()) status_id = 'status_' + self.input_id
context.update( descriptions = list([status_id])
{'describedby_html': 'aria-describedby="{}"'.format(description_ids)} descriptions.extend(self.response_data.get('descriptions', {}).keys())
) description_ids = ' '.join(descriptions)
context.update(
{'describedby_html': HTML('aria-describedby="{}"').format(description_ids)}
)
context.update( context.update(
(a, v) for (a, v) in self.loaded_attributes.iteritems() if a in self.to_render (a, v) for (a, v) in self.loaded_attributes.iteritems() if a in self.to_render
...@@ -522,9 +528,10 @@ class ChoiceGroup(InputTypeBase): ...@@ -522,9 +528,10 @@ class ChoiceGroup(InputTypeBase):
choices.append((choice.get("name"), stringify_children(choice))) choices.append((choice.get("name"), stringify_children(choice)))
else: else:
if choice.tag != 'compoundhint': if choice.tag != 'compoundhint':
msg = u'[capa.inputtypes.extract_choices] {error_message}'.format( msg = Text('[capa.inputtypes.extract_choices] {error_message}').format(
# Translators: '<choice>' and '<compoundhint>' are tag names and should not be translated. error_message=Text(
error_message=_('Expected a <choice> or <compoundhint> tag; got {given_tag} instead').format( # Translators: '<choice>' and '<compoundhint>' are tag names and should not be translated.
_('Expected a <choice> or <compoundhint> tag; got {given_tag} instead')).format(
given_tag=choice.tag given_tag=choice.tag
) )
) )
...@@ -939,13 +946,13 @@ class MatlabInput(CodeInput): ...@@ -939,13 +946,13 @@ class MatlabInput(CodeInput):
queue_msg = self.queue_msg queue_msg = self.queue_msg
if len(self.queue_msg) > 0: # An empty string cannot be parsed as XML but is okay to include in the template. if len(self.queue_msg) > 0: # An empty string cannot be parsed as XML but is okay to include in the template.
try: try:
etree.XML(u'<div>{0}</div>'.format(self.queue_msg)) etree.XML(HTML(u'<div>{0}</div>').format(HTML(self.queue_msg)))
except etree.XMLSyntaxError: except etree.XMLSyntaxError:
try: try:
html5lib.parseFragment(self.queue_msg, treebuilder='lxml', namespaceHTMLElements=False)[0] html5lib.parseFragment(self.queue_msg, treebuilder='lxml', namespaceHTMLElements=False)[0]
except (IndexError, ValueError): except (IndexError, ValueError):
# If neither can parse queue_msg, it contains invalid xml. # If neither can parse queue_msg, it contains invalid xml.
queue_msg = u"<span>{0}</span>".format(_("Error running code.")) queue_msg = HTML("<span>{0}</span>").format(_("Error running code."))
extra_context = { extra_context = {
'queue_len': str(self.queue_len), 'queue_len': str(self.queue_len),
...@@ -1797,10 +1804,10 @@ class ChoiceTextGroup(InputTypeBase): ...@@ -1797,10 +1804,10 @@ class ChoiceTextGroup(InputTypeBase):
for choice in element: for choice in element:
if choice.tag != 'choice': if choice.tag != 'choice':
msg = u"[capa.inputtypes.extract_choices] {0}".format( msg = Text("[capa.inputtypes.extract_choices] {0}").format(
# Translators: a "tag" is an XML element, such as "<b>" in HTML # Translators: a "tag" is an XML element, such as "<b>" in HTML
_("Expected a {expected_tag} tag; got {given_tag} instead").format( Text(_("Expected a {expected_tag} tag; got {given_tag} instead")).format(
expected_tag=u"<choice>", expected_tag="<choice>",
given_tag=choice.tag, given_tag=choice.tag,
) )
) )
......
...@@ -22,12 +22,10 @@ ...@@ -22,12 +22,10 @@
% for option in options: % for option in options:
<li> <li>
% if has_options_value: % if has_options_value:
% if all([c == 'correct' for c in option['choice'], status]): % if all([c == status.classname for c in option['choice'], status]):
<span class="tag-status correct" id="status_${id}" aria-describedby="input_${id}_comment"><span class="sr">Status: Correct</span></span> <span class="tag-status ${status.classname}" aria-describedby="input_${id}_comment">
% elif all([c == 'partially-correct' for c in option['choice'], status]): <%include file="status_span.html" args="status=status"/>
<span class="tag-status partially-correct" id="status_${id}" aria-describedby="input_${id}_comment"><span class="sr">Status: Partially Correct</span></span> </span>
% elif all([c == 'incorrect' for c in option['choice'], status]):
<span class="tag-status incorrect" id="status_${id}" aria-describedby="input_${id}_comment"><span class="sr">Status: Incorrect</span></span>
% endif % endif
% endif % endif
...@@ -53,7 +51,7 @@ ...@@ -53,7 +51,7 @@
<input type="hidden" class="value" name="input_${id}" id="input_${id}" value="${value|h}" /> <input type="hidden" class="value" name="input_${id}" id="input_${id}" value="${value|h}" />
% endif % endif
<span class="status ${status.classname}" id="status_${id}" aria-describedby="label_${id}"><span class="sr">${status.display_name}</span></span> <%include file="status_span.html" args="status=status, status_id=id"/>
<p id="answer_${id}" class="answer answer-annotation"></p> <p id="answer_${id}" class="answer answer-annotation"></p>
</div> </div>
......
...@@ -2,23 +2,21 @@ ...@@ -2,23 +2,21 @@
<div 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}"/>
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}">
<input type="text" name="input_${id}" id="input_${id}" aria-label="${remove_markup(response_data['label'])}" aria-describedby="answer_${id}" data-input-id="${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-label="${remove_markup(response_data['label'])}"
% if size: aria-describedby="answer_${id}" data-input-id="${id}" value="${value|h}"
size="${size}" % if size:
% endif size="${size}"
/> % endif
/>
<p class="status" aria-describedby="input_${id}"> <p class="status">
${value|h} ${value|h}
<span class="sr">${status.display_name}</span> <%include file="status_span.html" args="status=status, status_id=id"/>
</p> </p>
<div id="input_${id}_preview" class="equation"></div> <div id="input_${id}_preview" class="equation"></div>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
</div>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
</div>
% endif
</div> </div>
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
)) ))
%> %>
<form class="choicegroup capa_inputtype" id="inputtype_${id}"> <form class="choicegroup capa_inputtype" id="inputtype_${id}">
<fieldset ${HTML(describedby_html)}> <fieldset ${describedby_html}>
% if response_data['label']: % if response_data['label']:
<legend id="${id}-legend" class="response-fieldset-legend field-group-hd">${response_data['label']}</legend> <legend id="${id}-legend" class="response-fieldset-legend field-group-hd">${response_data['label']}</legend>
% endif % endif
...@@ -37,7 +37,7 @@ ...@@ -37,7 +37,7 @@
% endif % endif
% endif % endif
class="${label_class}" class="${label_class}"
${HTML(describedby_html)} ${describedby_html}
> >
<input type="${input_type}" name="input_${id}${name_array_suffix}" id="input_${id}_${choice_id}" class="field-input input-${input_type}" value="${choice_id}" <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 the student selected this choice...
...@@ -49,8 +49,8 @@ ...@@ -49,8 +49,8 @@
/> ${HTML(choice_label)} /> ${HTML(choice_label)}
% if is_radio_input(choice_id): % if is_radio_input(choice_id):
% if status in ('correct', 'partially-correct', 'incorrect') and not show_correctness == 'never': % if not show_correctness == 'never' and status.classname != 'unanswered':
<span class="sr status" id="${id}-${choice_id}-labeltext">${status.display_name}</span> <%include file="status_span.html" args="status=status, status_id=id"/>
% endif % endif
% endif % endif
</label> </label>
...@@ -59,10 +59,12 @@ ...@@ -59,10 +59,12 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
</fieldset> </fieldset>
<div class="indicator-container"> <div class="indicator-container">
% if input_type == 'checkbox' or not value: % if input_type == 'checkbox' or status.classname == 'unanswered':
<span class="status ${status.classname if show_correctness != 'never' else 'unanswered'}" id="status_${id}" aria-describedby="${id}-legend" data-tooltip="${status.display_tooltip}"> % if show_correctness != 'never':
<span class="sr">${status.display_tooltip}</span> <%include file="status_span.html" args="status=status, status_id=id"/>
</span> % else:
<%include file="status_span.html" args="status=status, status_id=id, hide_correctness=True"/>
% endif
% endif % endif
</div> </div>
% if show_correctness == "never" and (value or status not in ['unsubmitted']): % if show_correctness == "never" and (value or status not in ['unsubmitted']):
......
...@@ -66,9 +66,7 @@ from openedx.core.djangolib.markup import HTML ...@@ -66,9 +66,7 @@ from openedx.core.djangolib.markup import HTML
<div class="indicator-container"> <div class="indicator-container">
% if input_type == 'checkbox' or not element_checked: % if input_type == 'checkbox' or not element_checked:
<span class="status ${status.classname}" id="status_${id}"> <%include file="status_span.html" args="status=status, status_id=id"/>
<span class="sr">${status.display_name}</span>
</span>
% endif % endif
</div> </div>
......
...@@ -26,12 +26,9 @@ from openedx.core.djangolib.markup import HTML ...@@ -26,12 +26,9 @@ from openedx.core.djangolib.markup import HTML
</span> </span>
<div class="grader-status" tabindex="-1"> <div class="grader-status" tabindex="-1">
<span id="status_${id}"
class="${status.classname}" <%include file="status_span.html" args="status=status, status_id=id"/>
aria-describedby="input_${id}"
>
<span class="status sr">${status.display_name}</span>
</span>
% if status == 'queued': % if status == 'queued':
<span style="display:none;" class="xqueue" id="${id}">${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}">${queue_len}</span>
% endif % endif
......
...@@ -16,9 +16,7 @@ ...@@ -16,9 +16,7 @@
<input type="text" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}" style="display:none;"/> <input type="text" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}" style="display:none;"/>
<p class="status" aria-describedby="input_${id}"> <%include file="status_span.html" args="status=status, status_id=id"/>
<span class="sr">${status.display_name}</span>
</p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
......
...@@ -10,9 +10,7 @@ ...@@ -10,9 +10,7 @@
<input type="hidden" name="target_shape" id="target_shape" value ="${target_shape}"></input> <input type="hidden" name="target_shape" id="target_shape" value ="${target_shape}"></input>
<input type="hidden" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"/>
<p class="status" aria-describedby="input_${id}"> <%include file="status_span.html" args="status=status, status_id=id"/>
<span class="sr">${status.display_name}</span>
</p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
......
...@@ -17,8 +17,9 @@ ...@@ -17,8 +17,9 @@
<input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}" <input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"
style="display:none;"/> style="display:none;"/>
<p class="status drag-and-drop--status" aria-describedby="input_${id}"> <p class="status drag-and-drop--status" aria-describedby="input_${id}">
<span class="sr">${status.display_name}</span> <%include file="status_span.html" args="status=status, status_id=id"/>
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
<div class="script_placeholder" data-src="${applet_loader}"/> <div class="script_placeholder" data-src="${applet_loader}"/>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}">
% endif % endif
<div id="genex_container"></div> <div id="genex_container"></div>
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
<input type="hidden" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}"/>
<p class="status" aria-describedby="input_${id}"> <p class="status" aria-describedby="input_${id}">
<span class="sr">${status.display_name}</span> <%include file="status_span.html" args="status=status, status_id=id"/>
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<div class="script_placeholder" data-src="${applet_loader}"/> <div class="script_placeholder" data-src="${applet_loader}"/>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}">
% endif % endif
<div id="applet_${id}" class="applet" data-molfile-src="${file}" style="display:block;width:500px;height:400px"> <div id="applet_${id}" class="applet" data-molfile-src="${file}" style="display:block;width:500px;height:400px">
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
<p class="status" aria-describedby="input_${id}"> <p class="status" aria-describedby="input_${id}">
<span class="sr">${status.display_name}</span> <%include file="status_span.html" args="status=status, status_id=id"/>
</p> </p>
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div> <div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
<%! from openedx.core.djangolib.markup import HTML %> <%! from openedx.core.djangolib.markup import HTML %>
<% doinline = 'style="display:inline-block;vertical-align:top"' if inline else "" %> <% doinline = 'style="display:inline-block;vertical-align:top"' if inline else "" %>
<div id="formulaequationinput_${id}" class="inputtype formulaequationinput" ${doinline | n, decode.utf8}> <div id="formulaequationinput_${id}" class="inputtype formulaequationinput" ${doinline | n, decode.utf8}>
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}">
% if response_data['label']: % if response_data['label']:
<label class="problem-group-label" for="input_${id}" id="label_${id}">${response_data['label']}</label> <label class="problem-group-label" for="input_${id}" id="label_${id}">${response_data['label']}</label>
% endif % endif
...@@ -11,16 +11,14 @@ ...@@ -11,16 +11,14 @@
% endfor % endfor
<input type="text" name="input_${id}" id="input_${id}" <input type="text" name="input_${id}" id="input_${id}"
data-input-id="${id}" value="${value}" data-input-id="${id}" value="${value}"
${HTML(describedby_html)} ${describedby_html}
% if size: % if size:
size="${size}" size="${size}"
% endif % endif
/> />
<span class="trailing_text">${trailing_text}</span> <span class="trailing_text">${trailing_text}</span>
<span class="status" id="${id}_status" aria-describedby="label_${id}" data-tooltip="${status.display_tooltip}"> <%include file="status_span.html" args="status=status, status_id=id"/>
<span class="sr">${status.display_tooltip}</span>
</span>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
......
...@@ -40,11 +40,5 @@ ...@@ -40,11 +40,5 @@
(new ImageInput('${id}')); (new ImageInput('${id}'));
</script> </script>
<span <%include file="status_span.html" args="status=status, status_id=id"/>
class="status ${status.classname}"
id="status_${id}"
aria-describedby="input_${id}"
>
<span class="sr">${status.display_name}</span>
</span>
</div> </div>
<%page expression_filter="h"/>
<form class="javascriptinput capa_inputtype" id="inputtype_${id}"> <form class="javascriptinput capa_inputtype" id="inputtype_${id}">
<input type="hidden" name="input_${id}" id="input_${id}" class="javascriptinput_input"/> <input type="hidden" name="input_${id}" id="input_${id}" class="javascriptinput_input"/>
<div class="javascriptinput_data" data-display_class="${display_class}" <div class="javascriptinput_data" data-display_class="${display_class}"
data-problem_state="${problem_state}" data-params="${params}" data-problem_state="${problem_state}" data-params="${params}"
data-submission="${value|h}" data-evaluation="${msg|h}"> data-submission="${value}" data-evaluation="${msg}">
</div> </div>
<div class="script_placeholder" data-src="/static/js/${display_file}"></div> <div class="script_placeholder" data-src="/static/js/${display_file}"></div>
<div class="javascriptinput_container"></div> <div class="javascriptinput_container"></div>
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
<div class="script_placeholder" data-src="${jschannel_loader}"/> <div class="script_placeholder" data-src="${jschannel_loader}"/>
<div class="script_placeholder" data-src="${jsinput_loader}"/> <div class="script_placeholder" data-src="${jsinput_loader}"/>
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
<div class="${status.classname}" id="status_${id}"> <div class="${status.classname}">
% endif % endif
<iframe name="iframe_${id}" <iframe name="iframe_${id}"
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
<p class="status"> <p class="status">
<span class="sr">${status.display_name}</span> <%include file="status_span.html" args="status=status, status_id=id"/>
</p> </p>
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div> <div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
......
...@@ -17,12 +17,9 @@ ...@@ -17,12 +17,9 @@
>${value|h}</textarea> >${value|h}</textarea>
<div class="grader-status" tabindex="-1"> <div class="grader-status" tabindex="-1">
<span id="status_${id}"
class="${status.classname}" <%include file="status_span.html" args="status=status, status_id=id"/>
aria-describedby="input_${id}"
>
<span class="status sr">${status.display_name}</span>
</span>
% if status == 'queued': % if status == 'queued':
<span style="display:none;" class="xqueue" id="${id}">${queue_len}</span> <span style="display:none;" class="xqueue" id="${id}">${queue_len}</span>
% endif % endif
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<p class="question-description" id="${description_id}">${description_text}</p> <p class="question-description" id="${description_id}">${description_text}</p>
% endfor % endfor
<select name="input_${id}" id="input_${id}" ${HTML(describedby_html)}> <select name="input_${id}" id="input_${id}" ${describedby_html}>
<option value="option_${id}_dummy_default">${default_option_text}</option> <option value="option_${id}_dummy_default">${default_option_text}</option>
% for option_id, option_description in options: % for option_id, option_description in options:
<option value="${option_id}" <option value="${option_id}"
...@@ -23,11 +23,7 @@ ...@@ -23,11 +23,7 @@
</select> </select>
<div class="indicator-container"> <div class="indicator-container">
<span class="status ${status.classname}" <%include file="status_span.html" args="status=status, status_id=id"/>
id="status_${id}"
aria-describedby="label_${id}" data-tooltip="${status.display_tooltip}">
<span class="sr">${status.display_tooltip}</span>
</span>
</div> </div>
<p class="answer" id="answer_${id}"></p> <p class="answer" id="answer_${id}"></p>
% if msg: % if msg:
......
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
<div class="indicator-container"> <div class="indicator-container">
<span class="status ${status.classname}" id="status_${id}" aria-describedby="input_${id}"></span> <%include file="status_span.html" args="status=status, status_id=id"/>
<span class="sr">${status.display_tooltip}</span>
</div> </div>
</div> </div>
<%page expression_filter="h"/>
<div class="solution-span"> <div class="solution-span">
<span id="solution_${id}"></span> <span id="solution_${id}"></span>
</div> </div>
<%page expression_filter="h" args="status, status_id='', hide_correctness=False"/>
% if status_id == '':
<span class="status ${'' if hide_correctness == True else status.classname}"
data-tooltip="${'' if hide_correctness == True else status.display_tooltip}">
% else:
<span class="status ${'' if hide_correctness == True else status.classname}" id="status_${status_id}"
data-tooltip="${'' if hide_correctness == True else status.display_tooltip}">
% endif
% if hide_correctness == False:
<span class="sr">${status.display_name}</span><span class="status-icon" aria-hidden="true"></span>
% endif
</span>
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
% endif % endif
% if status in ('unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'): % if status in ('unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete'):
<div class="${status.classname} ${doinline}" id="status_${id}"> <div class="${status.classname} ${doinline}">
% endif % endif
% if hidden: % if hidden:
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
% for description_id, description_text in response_data['descriptions'].items(): % for description_id, description_text in response_data['descriptions'].items():
<p class="question-description" id="${description_id}">${description_text}</p> <p class="question-description" id="${description_id}">${description_text}</p>
% endfor % endfor
<input type="text" name="input_${id}" id="input_${id}" ${HTML(describedby_html)} value="${value}" <input type="text" name="input_${id}" id="input_${id}" ${describedby_html} value="${value}"
% if do_math: % if do_math:
class="math" class="math"
% endif % endif
...@@ -36,9 +36,7 @@ ...@@ -36,9 +36,7 @@
/> />
<span class="trailing_text">${trailing_text}</span> <span class="trailing_text">${trailing_text}</span>
<span class="status" aria-describedby="label_${id}" data-tooltip="${status.display_tooltip}"> <%include file="status_span.html" args="status=status, status_id=id"/>
<span class="sr">${status.display_tooltip}</span>
</span>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
......
...@@ -22,7 +22,8 @@ def get_template(template_name): ...@@ -22,7 +22,8 @@ def get_template(template_name):
Return template for a capa inputtype. Return template for a capa inputtype.
""" """
return TemplateLookup( return TemplateLookup(
directories=[path(__file__).dirname().dirname() / 'templates'] directories=[path(__file__).dirname().dirname() / 'templates'],
default_filters=['decode.utf8']
).get_template(template_name) ).get_template(template_name)
......
""" """
CAPA HTML rendering tests. CAPA HTML rendering tests.
""" """
import ddt
import unittest
from lxml import etree
import os
import textwrap import textwrap
import unittest
import ddt
import mock import mock
import os
from capa.tests.helpers import test_capa_system, new_loncapa_problem
from lxml import etree
from openedx.core.djangolib.markup import HTML
from .response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory from .response_xml_factory import StringResponseXMLFactory, CustomResponseXMLFactory
from capa.tests.helpers import test_capa_system, new_loncapa_problem
@ddt.ddt @ddt.ddt
...@@ -190,7 +191,7 @@ class CapaHtmlRenderTest(unittest.TestCase): ...@@ -190,7 +191,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
'trailing_text': '', 'trailing_text': '',
'size': None, 'size': None,
'response_data': {'label': 'Test question', 'descriptions': {}}, 'response_data': {'label': 'Test question', 'descriptions': {}},
'describedby_html': '' 'describedby_html': HTML('aria-describedby="status_1_2_1"')
} }
expected_solution_context = {'id': '1_solution_1'} expected_solution_context = {'id': '1_solution_1'}
......
...@@ -2,15 +2,16 @@ ...@@ -2,15 +2,16 @@
Tests for the logic in input type mako templates. Tests for the logic in input type mako templates.
""" """
from collections import OrderedDict
import unittest
import capa
import os.path
import json import json
import unittest
from collections import OrderedDict
from capa.inputtypes import Status
from capa.tests.helpers import capa_render_template
from lxml import etree from lxml import etree
from mako.template import Template as MakoTemplate
from mako import exceptions from mako import exceptions
from capa.inputtypes import Status from openedx.core.djangolib.markup import HTML
from xmodule.stringify import stringify_children from xmodule.stringify import stringify_children
...@@ -23,7 +24,7 @@ class TemplateError(Exception): ...@@ -23,7 +24,7 @@ class TemplateError(Exception):
class TemplateTestCase(unittest.TestCase): class TemplateTestCase(unittest.TestCase):
""" """
Utilitites for testing templates. Utilities for testing templates.
""" """
# Subclasses override this to specify the file name of the template # Subclasses override this to specify the file name of the template
...@@ -46,16 +47,9 @@ class TemplateTestCase(unittest.TestCase): ...@@ -46,16 +47,9 @@ class TemplateTestCase(unittest.TestCase):
def setUp(self): def setUp(self):
""" """
Load the template under test. Initialize the context.
""" """
super(TemplateTestCase, self).setUp() super(TemplateTestCase, self).setUp()
capa_path = capa.__path__[0]
self.template_path = os.path.join(capa_path,
'templates',
self.TEMPLATE_NAME)
with open(self.template_path) as f:
self.template = MakoTemplate(f.read(), default_filters=['decode.utf8'])
self.context = {} self.context = {}
def render_to_xml(self, context_dict): def render_to_xml(self, context_dict):
...@@ -66,7 +60,7 @@ class TemplateTestCase(unittest.TestCase): ...@@ -66,7 +60,7 @@ class TemplateTestCase(unittest.TestCase):
# add dummy STATIC_URL to template context # add dummy STATIC_URL to template context
context_dict.setdefault("STATIC_URL", "/dummy-static/") context_dict.setdefault("STATIC_URL", "/dummy-static/")
try: try:
xml_str = self.template.render_unicode(**context_dict) xml_str = capa_render_template(self.TEMPLATE_NAME, context_dict)
except: except:
raise TemplateError(exceptions.text_error_template().render()) raise TemplateError(exceptions.text_error_template().render())
...@@ -196,10 +190,10 @@ class TemplateTestCase(unittest.TestCase): ...@@ -196,10 +190,10 @@ class TemplateTestCase(unittest.TestCase):
# (used to by CSS to draw the green check / red x) # (used to by CSS to draw the green check / red x)
self.assert_has_text( self.assert_has_text(
xml, xml,
"//span[@class=normalize-space('status {}')]/span[@class='sr']".format( "//span[@class='status {}']/span[@class='sr']".format(
div_class if status_class else '' div_class if status_class else ''
), ),
self.context['status'].display_tooltip self.context['status'].display_name
) )
def assert_label(self, xpath=None, aria_label=False): def assert_label(self, xpath=None, aria_label=False):
...@@ -259,7 +253,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -259,7 +253,7 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
'name_array_suffix': '1', 'name_array_suffix': '1',
'value': '3', 'value': '3',
'response_data': self.RESPONSE_DATA, 'response_data': self.RESPONSE_DATA,
'describedby_html': self.DESCRIBEDBY, 'describedby_html': HTML(self.DESCRIBEDBY),
} }
def test_problem_marked_correct(self): def test_problem_marked_correct(self):
...@@ -290,11 +284,9 @@ class ChoiceGroupTemplateTest(TemplateTestCase): ...@@ -290,11 +284,9 @@ class ChoiceGroupTemplateTest(TemplateTestCase):
(not a particular option) is marked incorrect. (not a particular option) is marked incorrect.
""" """
conditions = [ conditions = [
{'status': Status('incorrect'), 'input_type': 'radio', 'value': ''},
{'status': Status('incorrect'), 'input_type': 'checkbox', 'value': []}, {'status': Status('incorrect'), 'input_type': 'checkbox', 'value': []},
{'status': Status('incorrect'), 'input_type': 'checkbox', 'value': ['2']}, {'status': Status('incorrect'), 'input_type': 'checkbox', 'value': ['2']},
{'status': Status('incorrect'), 'input_type': 'checkbox', 'value': ['2', '3']}, {'status': Status('incorrect'), 'input_type': 'checkbox', 'value': ['2', '3']},
{'status': Status('incomplete'), 'input_type': 'radio', 'value': ''},
{'status': Status('incomplete'), 'input_type': 'checkbox', 'value': []}, {'status': Status('incomplete'), 'input_type': 'checkbox', 'value': []},
{'status': Status('incomplete'), 'input_type': 'checkbox', 'value': ['2']}, {'status': Status('incomplete'), 'input_type': 'checkbox', 'value': ['2']},
{'status': Status('incomplete'), 'input_type': 'checkbox', 'value': ['2', '3']}] {'status': Status('incomplete'), 'input_type': 'checkbox', 'value': ['2', '3']}]
...@@ -506,7 +498,7 @@ class TextlineTemplateTest(TemplateTestCase): ...@@ -506,7 +498,7 @@ class TextlineTemplateTest(TemplateTestCase):
'preprocessor': None, 'preprocessor': None,
'trailing_text': None, 'trailing_text': None,
'response_data': self.RESPONSE_DATA, 'response_data': self.RESPONSE_DATA,
'describedby_html': self.DESCRIBEDBY, 'describedby_html': HTML(self.DESCRIBEDBY),
} }
def test_section_class(self): def test_section_class(self):
...@@ -526,7 +518,7 @@ class TextlineTemplateTest(TemplateTestCase): ...@@ -526,7 +518,7 @@ class TextlineTemplateTest(TemplateTestCase):
""" """
Verify status information. Verify status information.
""" """
self.assert_status(status_div=True) self.assert_status(status_class=True)
def test_label(self): def test_label(self):
""" """
...@@ -632,7 +624,7 @@ class FormulaEquationInputTemplateTest(TemplateTestCase): ...@@ -632,7 +624,7 @@ class FormulaEquationInputTemplateTest(TemplateTestCase):
'reported_status': 'REPORTED_STATUS', 'reported_status': 'REPORTED_STATUS',
'trailing_text': None, 'trailing_text': None,
'response_data': self.RESPONSE_DATA, 'response_data': self.RESPONSE_DATA,
'describedby_html': self.DESCRIBEDBY, 'describedby_html': HTML(self.DESCRIBEDBY),
} }
def test_no_size(self): def test_no_size(self):
...@@ -657,7 +649,7 @@ class FormulaEquationInputTemplateTest(TemplateTestCase): ...@@ -657,7 +649,7 @@ class FormulaEquationInputTemplateTest(TemplateTestCase):
""" """
Verify status information. Verify status information.
""" """
self.assert_status(status_div=True) self.assert_status(status_class=True)
def test_label(self): def test_label(self):
""" """
...@@ -852,7 +844,7 @@ class OptionInputTemplateTest(TemplateTestCase): ...@@ -852,7 +844,7 @@ class OptionInputTemplateTest(TemplateTestCase):
'value': 0, 'value': 0,
'default_option_text': 'Select an option', 'default_option_text': 'Select an option',
'response_data': self.RESPONSE_DATA, 'response_data': self.RESPONSE_DATA,
'describedby_html': self.DESCRIBEDBY, 'describedby_html': HTML(self.DESCRIBEDBY),
} }
def test_select_options(self): def test_select_options(self):
...@@ -929,8 +921,8 @@ class DragAndDropTemplateTest(TemplateTestCase): ...@@ -929,8 +921,8 @@ class DragAndDropTemplateTest(TemplateTestCase):
xpath = "//div[@class='{0}']".format(expected_css_class) xpath = "//div[@class='{0}']".format(expected_css_class)
self.assert_has_xpath(xml, xpath, self.context) self.assert_has_xpath(xml, xpath, self.context)
# Expect a <p> with the status # Expect a <span> with the status
xpath = "//p[@class='status drag-and-drop--status']/span[@class='sr']" xpath = "//span[@class='status {0}']/span[@class='sr']".format(expected_css_class)
self.assert_has_text(xml, xpath, expected_text, exact=False) self.assert_has_text(xml, xpath, expected_text, exact=False)
def test_drag_and_drop_json_html(self): def test_drag_and_drop_json_html(self):
...@@ -1206,7 +1198,7 @@ class CodeinputTemplateTest(TemplateTestCase): ...@@ -1206,7 +1198,7 @@ class CodeinputTemplateTest(TemplateTestCase):
'aria_label': 'python editor', 'aria_label': 'python editor',
'code_mirror_exit_message': 'Press ESC then TAB or click outside of the code editor to exit', 'code_mirror_exit_message': 'Press ESC then TAB or click outside of the code editor to exit',
'response_data': self.RESPONSE_DATA, 'response_data': self.RESPONSE_DATA,
'describedby': self.DESCRIBEDBY, 'describedby': HTML(self.DESCRIBEDBY),
} }
def test_label(self): def test_label(self):
......
...@@ -23,9 +23,6 @@ ...@@ -23,9 +23,6 @@
// ==================== // ====================
$annotation-yellow: rgba(255,255,10,0.3); $annotation-yellow: rgba(255,255,10,0.3);
$color-copy-tip: rgb(100,100,100); $color-copy-tip: rgb(100,100,100);
$correct: $green-d2;
$partially-correct: $green-d2;
$incorrect: $red;
// FontAwesome Icon code // FontAwesome Icon code
// ==================== // ====================
...@@ -50,11 +47,13 @@ $asterisk-icon: '\f069'; // .fa-asterisk ...@@ -50,11 +47,13 @@ $asterisk-icon: '\f069'; // .fa-asterisk
// ==================== // ====================
@mixin status-icon($color: $gray, $fontAwesomeIcon: "\f00d"){ @mixin status-icon($color: $gray, $fontAwesomeIcon: "\f00d"){
&:after { .status-icon {
@extend %use-font-awesome; &:after {
color: $color; @extend %use-font-awesome;
font-size: 1.2em; color: $color;
content: $fontAwesomeIcon; font-size: 1.2em;
content: $fontAwesomeIcon;
}
} }
} }
...@@ -318,6 +317,12 @@ div.problem { ...@@ -318,6 +317,12 @@ div.problem {
@include status-icon($incorrect, $cross-icon); @include status-icon($incorrect, $cross-icon);
} }
&.unsubmitted,
&.unanswered {
.status-icon {
content: '';
}
}
} }
} }
} }
...@@ -818,12 +823,14 @@ div.problem { ...@@ -818,12 +823,14 @@ div.problem {
} }
.status { .status {
&:after { .status-icon {
content: ''; // clear out correct or incorrect icon &:after {
content: '';
}
} }
} }
} }
} }
.trailing_text { .trailing_text {
......
...@@ -962,7 +962,6 @@ ...@@ -962,7 +962,6 @@
$status = $('#status_' + id); $status = $('#status_' + id);
if ($status[0]) { if ($status[0]) {
$status.removeClass().addClass('unanswered'); $status.removeClass().addClass('unanswered');
$status.empty().css('display', 'inline-block');
} else { } else {
$('<span>', { $('<span>', {
class: 'unanswered', class: 'unanswered',
...@@ -979,8 +978,8 @@ ...@@ -979,8 +978,8 @@
id = ($select.attr('id').match(/^input_(.*)$/))[1]; id = ($select.attr('id').match(/^input_(.*)$/))[1];
return $select.on('change', function() { return $select.on('change', function() {
return $('#status_' + id).removeClass().addClass('unanswered') return $('#status_' + id).removeClass().addClass('unanswered')
.find('span') .find('.sr')
.text(gettext('Status: unsubmitted')); .text(gettext('unsubmitted'));
}); });
}, },
textline: function(element) { textline: function(element) {
......
...@@ -144,6 +144,13 @@ $success-color: rgb(0, 155, 0) !default; ...@@ -144,6 +144,13 @@ $success-color: rgb(0, 155, 0) !default;
$warning-color: rgb(255, 192, 31) !default; $warning-color: rgb(255, 192, 31) !default;
$warning-color-accent: rgb(255, 252, 221) !default; $warning-color-accent: rgb(255, 252, 221) !default;
// CAPA correctness color to be consistent with Alert styles above
$correct: $success-color;
$partially-correct: $success-color;
$incorrect: $error-color;
// BUTTONS // BUTTONS
// disabled button // disabled button
......
...@@ -617,12 +617,12 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase): ...@@ -617,12 +617,12 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
metadata={'graded': True}, metadata={'graded': True},
display_name='Problem Vertical' display_name='Problem Vertical'
) )
self.define_option_problem(u'Pröblem1', parent=vertical) self.define_option_problem(u'Problem1', parent=vertical)
self.submit_student_answer(self.student_1.username, u'Pröblem1', ['Option 1']) self.submit_student_answer(self.student_1.username, u'Problem1', ['Option 1'])
result = upload_problem_grade_report(None, None, self.course.id, None, 'graded') result = upload_problem_grade_report(None, None, self.course.id, None, 'graded')
self.assertDictContainsSubset({'action_name': 'graded', 'attempted': 2, 'succeeded': 2, 'failed': 0}, result) self.assertDictContainsSubset({'action_name': 'graded', 'attempted': 2, 'succeeded': 2, 'failed': 0}, result)
problem_name = u'Homework 1: Problem - Pröblem1' problem_name = u'Homework 1: Problem - Problem1'
header_row = self.csv_header_row + [problem_name + ' (Earned)', problem_name + ' (Possible)'] header_row = self.csv_header_row + [problem_name + ' (Earned)', problem_name + ' (Possible)']
self.verify_rows_in_csv([ self.verify_rows_in_csv([
dict(zip( dict(zip(
...@@ -646,7 +646,7 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase): ...@@ -646,7 +646,7 @@ class TestProblemGradeReport(TestReportMixin, InstructorTaskModuleTestCase):
@patch('lms.djangoapps.instructor_task.tasks_helper._get_current_task') @patch('lms.djangoapps.instructor_task.tasks_helper._get_current_task')
@patch('lms.djangoapps.instructor_task.tasks_helper.iterate_grades_for') @patch('lms.djangoapps.instructor_task.tasks_helper.iterate_grades_for')
@ddt.data(u'Cannöt grade student', '') @ddt.data(u'Cannot grade student', '')
def test_grading_failure(self, error_message, mock_iterate_grades_for, _mock_current_task): def test_grading_failure(self, error_message, mock_iterate_grades_for, _mock_current_task):
""" """
Test that any grading errors are properly reported in the progress Test that any grading errors are properly reported in the progress
...@@ -683,8 +683,8 @@ class TestProblemReportSplitTestContent(TestReportMixin, TestConditionalContent, ...@@ -683,8 +683,8 @@ class TestProblemReportSplitTestContent(TestReportMixin, TestConditionalContent,
def setUp(self): def setUp(self):
super(TestProblemReportSplitTestContent, self).setUp() super(TestProblemReportSplitTestContent, self).setUp()
self.problem_a_url = u'pröblem_a_url' self.problem_a_url = u'problem_a_url'
self.problem_b_url = u'pröblem_b_url' self.problem_b_url = u'problem_b_url'
self.define_option_problem(self.problem_a_url, parent=self.vertical_a) self.define_option_problem(self.problem_a_url, parent=self.vertical_a)
self.define_option_problem(self.problem_b_url, parent=self.vertical_b) self.define_option_problem(self.problem_b_url, parent=self.vertical_b)
...@@ -711,7 +711,7 @@ class TestProblemReportSplitTestContent(TestReportMixin, TestConditionalContent, ...@@ -711,7 +711,7 @@ class TestProblemReportSplitTestContent(TestReportMixin, TestConditionalContent,
{'action_name': 'graded', 'attempted': 2, 'succeeded': 2, 'failed': 0}, result {'action_name': 'graded', 'attempted': 2, 'succeeded': 2, 'failed': 0}, result
) )
problem_names = [u'Homework 1: Problem - pröblem_a_url', u'Homework 1: Problem - pröblem_b_url'] problem_names = [u'Homework 1: Problem - problem_a_url', u'Homework 1: Problem - problem_b_url']
header_row = [u'Student ID', u'Email', u'Username', u'Final Grade'] header_row = [u'Student ID', u'Email', u'Username', u'Final Grade']
for problem in problem_names: for problem in problem_names:
header_row += [problem + ' (Earned)', problem + ' (Possible)'] header_row += [problem + ' (Earned)', problem + ' (Possible)']
...@@ -817,12 +817,12 @@ class TestProblemReportCohortedContent(TestReportMixin, ContentGroupTestCase, In ...@@ -817,12 +817,12 @@ class TestProblemReportCohortedContent(TestReportMixin, ContentGroupTestCase, In
display_name='Problem Vertical' display_name='Problem Vertical'
) )
self.define_option_problem( self.define_option_problem(
u"Pröblem0", u"Problem0",
parent=vertical, parent=vertical,
group_access={self.course.user_partitions[0].id: [self.course.user_partitions[0].groups[0].id]} group_access={self.course.user_partitions[0].id: [self.course.user_partitions[0].groups[0].id]}
) )
self.define_option_problem( self.define_option_problem(
u"Pröblem1", u"Problem1",
parent=vertical, parent=vertical,
group_access={self.course.user_partitions[0].id: [self.course.user_partitions[0].groups[1].id]} group_access={self.course.user_partitions[0].id: [self.course.user_partitions[0].groups[1].id]}
) )
...@@ -845,20 +845,20 @@ class TestProblemReportCohortedContent(TestReportMixin, ContentGroupTestCase, In ...@@ -845,20 +845,20 @@ class TestProblemReportCohortedContent(TestReportMixin, ContentGroupTestCase, In
)) ))
def test_cohort_content(self): def test_cohort_content(self):
self.submit_student_answer(self.alpha_user.username, u'Pröblem0', ['Option 1', 'Option 1']) self.submit_student_answer(self.alpha_user.username, u'Problem0', ['Option 1', 'Option 1'])
resp = self.submit_student_answer(self.alpha_user.username, u'Pröblem1', ['Option 1', 'Option 1']) resp = self.submit_student_answer(self.alpha_user.username, u'Problem1', ['Option 1', 'Option 1'])
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
resp = self.submit_student_answer(self.beta_user.username, u'Pröblem0', ['Option 1', 'Option 2']) resp = self.submit_student_answer(self.beta_user.username, u'Problem0', ['Option 1', 'Option 2'])
self.assertEqual(resp.status_code, 404) self.assertEqual(resp.status_code, 404)
self.submit_student_answer(self.beta_user.username, u'Pröblem1', ['Option 1', 'Option 2']) self.submit_student_answer(self.beta_user.username, u'Problem1', ['Option 1', 'Option 2'])
with patch('lms.djangoapps.instructor_task.tasks_helper._get_current_task'): with patch('lms.djangoapps.instructor_task.tasks_helper._get_current_task'):
result = upload_problem_grade_report(None, None, self.course.id, None, 'graded') result = upload_problem_grade_report(None, None, self.course.id, None, 'graded')
self.assertDictContainsSubset( self.assertDictContainsSubset(
{'action_name': 'graded', 'attempted': 4, 'succeeded': 4, 'failed': 0}, result {'action_name': 'graded', 'attempted': 4, 'succeeded': 4, 'failed': 0}, result
) )
problem_names = [u'Homework 1: Problem - Pröblem0', u'Homework 1: Problem - Pröblem1'] problem_names = [u'Homework 1: Problem - Problem0', u'Homework 1: Problem - Problem1']
header_row = [u'Student ID', u'Email', u'Username', u'Final Grade'] header_row = [u'Student ID', u'Email', u'Username', u'Final Grade']
for problem in problem_names: for problem in problem_names:
header_row += [problem + ' (Earned)', problem + ' (Possible)'] header_row += [problem + ' (Earned)', problem + ' (Possible)']
......
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