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

This commit is contained in:
Albert St. Aubin
2016-06-08 14:18:10 +05:00
parent 04fa2a4263
commit 055fd6ea8f
29 changed files with 254 additions and 250 deletions

View File

@@ -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
import bleach
from .util import sanitize_html
from .registry import TagRegistry
from chem import chemcalc
from calc.preview import latex_preview
import xqueue_interface
from xqueue_interface import XQUEUE_TIMEOUT
from datetime import datetime from datetime import datetime
import bleach
import html5lib
import pyparsing
import re
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
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):
# 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):
'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):
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):
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):
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,
) )
) )

View File

@@ -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 @@
<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>

View File

@@ -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>

View File

@@ -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 @@
% 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 @@
/> ${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 @@
<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']):

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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']:

View File

@@ -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>

View File

@@ -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 @@
<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>

View File

@@ -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 @@
<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>

View File

@@ -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 @@
% 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>

View File

@@ -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>

View File

@@ -1,8 +1,9 @@
<%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>

View File

@@ -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 @@
<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>

View File

@@ -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

View File

@@ -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 @@
</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:

View File

@@ -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>

View File

@@ -1,3 +1,4 @@
<%page expression_filter="h"/>
<div class="solution-span"> <div class="solution-span">
<span id="solution_${id}"></span> <span id="solution_${id}"></span>
</div> </div>

View File

@@ -0,0 +1,13 @@
<%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>

View File

@@ -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 @@
% 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 @@
/> />
<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>

View File

@@ -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)

View File

@@ -1,16 +1,17 @@
""" """
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):
'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'}

View File

@@ -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
from lxml import etree import unittest
from mako.template import Template as MakoTemplate from collections import OrderedDict
from mako import exceptions
from capa.inputtypes import Status from capa.inputtypes import Status
from capa.tests.helpers import capa_render_template
from lxml import etree
from mako import exceptions
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):
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):
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):
# 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):
# (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):
'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):
(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):
'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):
""" """
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):
'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):
""" """
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):
'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):
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):
'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):

View File

@@ -16,27 +16,27 @@ TODO:
- test funny xml chars -- should never get xml parse error if things are escaped properly. - test funny xml chars -- should never get xml parse error if things are escaped properly.
""" """
from collections import OrderedDict
import json import json
from lxml import etree
from lxml.html import fromstring
import unittest
import textwrap import textwrap
import unittest
import xml.sax.saxutils as saxutils import xml.sax.saxutils as saxutils
from collections import OrderedDict
from capa.tests.helpers import test_capa_system
from capa import inputtypes from capa import inputtypes
from capa.checker import DemoSystem from capa.checker import DemoSystem
from mock import ANY, patch from capa.tests.helpers import test_capa_system
from pyparsing import ParseException
from capa.xqueue_interface import XQUEUE_TIMEOUT from capa.xqueue_interface import XQUEUE_TIMEOUT
from lxml import etree
from lxml.html import fromstring
from mock import ANY, patch
from openedx.core.djangolib.markup import HTML
from pyparsing import ParseException
# just a handy shortcut # just a handy shortcut
lookup_tag = inputtypes.registry.get_class_for_tag lookup_tag = inputtypes.registry.get_class_for_tag
DESCRIBEDBY = 'aria-describedby="desc-1 desc-2"' DESCRIBEDBY = HTML('aria-describedby="status_{status_id} desc-1 desc-2"')
DESCRIPTIONS = OrderedDict([('desc-1', 'description text 1'), ('desc-2', 'description text 2')]) DESCRIPTIONS = OrderedDict([('desc-1', 'description text 1'), ('desc-2', 'description text 2')])
RESPONSE_DATA = { RESPONSE_DATA = {
'label': 'question text 101', 'label': 'question text 101',
@@ -67,7 +67,7 @@ class OptionInputTest(unittest.TestCase):
option_input = lookup_tag('optioninput')(test_capa_system(), element, state) option_input = lookup_tag('optioninput')(test_capa_system(), element, state)
context = option_input._get_render_context() # pylint: disable=protected-access context = option_input._get_render_context() # pylint: disable=protected-access
prob_id = 'sky_input'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'value': 'Down', 'value': 'Down',
@@ -75,10 +75,10 @@ class OptionInputTest(unittest.TestCase):
'status': inputtypes.Status('answered'), 'status': inputtypes.Status('answered'),
'msg': '', 'msg': '',
'inline': False, 'inline': False,
'id': 'sky_input', 'id': prob_id,
'default_option_text': 'Select an option', 'default_option_text': 'Select an option',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id='sky_input')
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -147,7 +147,7 @@ class ChoiceGroupTest(unittest.TestCase):
'submitted_message': 'Answer received.', 'submitted_message': 'Answer received.',
'name_array_suffix': expected_suffix, # what is this for?? 'name_array_suffix': expected_suffix, # what is this for??
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id='sky_input')
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -189,10 +189,10 @@ class JavascriptInputTest(unittest.TestCase):
the_input = lookup_tag('javascriptinput')(test_capa_system(), element, state) the_input = lookup_tag('javascriptinput')(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'status': inputtypes.Status('unanswered'), 'status': inputtypes.Status('unanswered'),
'msg': '', 'msg': '',
'value': '3', 'value': '3',
@@ -201,7 +201,7 @@ class JavascriptInputTest(unittest.TestCase):
'display_class': display_class, 'display_class': display_class,
'problem_state': problem_state, 'problem_state': problem_state,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -225,10 +225,10 @@ class TextLineTest(unittest.TestCase):
the_input = lookup_tag('textline')(test_capa_system(), element, state) the_input = lookup_tag('textline')(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'BumbleBee', 'value': 'BumbleBee',
'status': inputtypes.Status('unanswered'), 'status': inputtypes.Status('unanswered'),
'size': size, 'size': size,
@@ -239,7 +239,7 @@ class TextLineTest(unittest.TestCase):
'trailing_text': '', 'trailing_text': '',
'preprocessor': None, 'preprocessor': None,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -261,10 +261,10 @@ class TextLineTest(unittest.TestCase):
the_input = lookup_tag('textline')(test_capa_system(), element, state) the_input = lookup_tag('textline')(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'BumbleBee', 'value': 'BumbleBee',
'status': inputtypes.Status('unanswered'), 'status': inputtypes.Status('unanswered'),
'size': size, 'size': size,
@@ -278,7 +278,7 @@ class TextLineTest(unittest.TestCase):
'script_src': script, 'script_src': script,
}, },
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -309,10 +309,10 @@ class TextLineTest(unittest.TestCase):
the_input = lookup_tag('textline')(test_capa_system(), element, state) the_input = lookup_tag('textline')(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'BumbleBee', 'value': 'BumbleBee',
'status': inputtypes.Status('unanswered'), 'status': inputtypes.Status('unanswered'),
'size': size, 'size': size,
@@ -323,7 +323,7 @@ class TextLineTest(unittest.TestCase):
'trailing_text': expected_text, 'trailing_text': expected_text,
'preprocessor': None, 'preprocessor': None,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -355,10 +355,10 @@ class FileSubmissionTest(unittest.TestCase):
the_input = input_class(test_capa_system(), element, state) the_input = input_class(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'status': inputtypes.Status('queued'), 'status': inputtypes.Status('queued'),
'msg': the_input.submitted_msg, 'msg': the_input.submitted_msg,
'value': 'BumbleBee.py', 'value': 'BumbleBee.py',
@@ -366,7 +366,7 @@ class FileSubmissionTest(unittest.TestCase):
'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]', 'allowed_files': '["runme.py", "nooooo.rb", "ohai.java"]',
'required_files': '["cookies.py"]', 'required_files': '["cookies.py"]',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -407,10 +407,10 @@ class CodeInputTest(unittest.TestCase):
the_input = input_class(test_capa_system(), element, state) the_input = input_class(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'print "good evening"', 'value': 'print "good evening"',
'status': inputtypes.Status('queued'), 'status': inputtypes.Status('queued'),
'msg': the_input.submitted_msg, 'msg': the_input.submitted_msg,
@@ -424,7 +424,7 @@ class CodeInputTest(unittest.TestCase):
'aria_label': '{mode} editor'.format(mode=mode), 'aria_label': '{mode} editor'.format(mode=mode),
'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': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -486,7 +486,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '3', 'queue_len': '3',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js', 'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': {}, 'response_data': {},
'describedby_html': '' 'describedby_html': HTML('aria-describedby="status_prob_1_2"')
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -503,10 +503,10 @@ class MatlabTest(unittest.TestCase):
the_input = self.input_class(test_capa_system(), elt, state) the_input = self.input_class(test_capa_system(), elt, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'print "good evening"', 'value': 'print "good evening"',
'status': inputtypes.Status('queued'), 'status': inputtypes.Status('queued'),
'msg': the_input.submitted_msg, 'msg': the_input.submitted_msg,
@@ -521,7 +521,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '3', 'queue_len': '3',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js', 'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -535,12 +535,12 @@ class MatlabTest(unittest.TestCase):
'response_data': RESPONSE_DATA 'response_data': RESPONSE_DATA
} }
elt = etree.fromstring(self.xml) elt = etree.fromstring(self.xml)
prob_id = 'prob_1_2'
the_input = self.input_class(test_capa_system(), elt, state) the_input = self.input_class(test_capa_system(), elt, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'print "good evening"', 'value': 'print "good evening"',
'status': inputtypes.Status(status), 'status': inputtypes.Status(status),
'msg': '', 'msg': '',
@@ -555,7 +555,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '0', 'queue_len': '0',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js', 'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -569,12 +569,12 @@ class MatlabTest(unittest.TestCase):
'response_data': RESPONSE_DATA 'response_data': RESPONSE_DATA
} }
elt = etree.fromstring(self.xml) elt = etree.fromstring(self.xml)
prob_id = 'prob_1_2'
the_input = self.input_class(test_capa_system(), elt, state) the_input = self.input_class(test_capa_system(), elt, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'print "good evening"', 'value': 'print "good evening"',
'status': inputtypes.Status('queued'), 'status': inputtypes.Status('queued'),
'msg': the_input.submitted_msg, 'msg': the_input.submitted_msg,
@@ -589,7 +589,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '1', 'queue_len': '1',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js', 'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -713,7 +713,8 @@ class MatlabTest(unittest.TestCase):
\'msg\': u\'Submitted. As soon as a response is returned, \'msg\': u\'Submitted. As soon as a response is returned,
this message will be replaced by that feedback.\', this message will be replaced by that feedback.\',
\'matlab_editor_js\': \'/dummy-static/js/vendor/CodeMirror/octave.js\', \'matlab_editor_js\': \'/dummy-static/js/vendor/CodeMirror/octave.js\',
\'hidden\': \'\', \'id\': \'prob_1_2\', \'describedby_html\': \'\', \'hidden\': \'\', \'id\': \'prob_1_2\',
\'describedby_html\': Markup(u\'aria-describedby="status_prob_1_2"\'),
\'response_data\': {}}</div> \'response_data\': {}}</div>
""").replace('\n', ' ').strip() """).replace('\n', ' ').strip()
) )
@@ -804,9 +805,10 @@ class MatlabTest(unittest.TestCase):
context = self.the_input._get_render_context() # pylint: disable=protected-access context = self.the_input._get_render_context() # pylint: disable=protected-access
self.maxDiff = None self.maxDiff = None
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'print "good evening"', 'value': 'print "good evening"',
'status': inputtypes.Status('queued'), 'status': inputtypes.Status('queued'),
'msg': self.the_input.submitted_msg, 'msg': self.the_input.submitted_msg,
@@ -821,7 +823,7 @@ class MatlabTest(unittest.TestCase):
'queue_len': '3', 'queue_len': '3',
'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js', 'matlab_editor_js': '/dummy-static/js/vendor/CodeMirror/octave.js',
'response_data': {}, 'response_data': {},
'describedby_html': '' 'describedby_html': 'aria-describedby="status_{id}"'.format(id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -917,10 +919,10 @@ class SchematicTest(unittest.TestCase):
the_input = lookup_tag('schematic')(test_capa_system(), element, state) the_input = lookup_tag('schematic')(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': value, 'value': value,
'status': inputtypes.Status('unsubmitted'), 'status': inputtypes.Status('unsubmitted'),
'msg': '', 'msg': '',
@@ -932,7 +934,7 @@ class SchematicTest(unittest.TestCase):
'analyses': analyses, 'analyses': analyses,
'submit_analyses': submit_analyses, 'submit_analyses': submit_analyses,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -965,10 +967,10 @@ class ImageInputTest(unittest.TestCase):
the_input = lookup_tag('imageinput')(test_capa_system(), element, state) the_input = lookup_tag('imageinput')(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': value, 'value': value,
'status': inputtypes.Status('unsubmitted'), 'status': inputtypes.Status('unsubmitted'),
'width': width, 'width': width,
@@ -978,7 +980,7 @@ class ImageInputTest(unittest.TestCase):
'gy': egy, 'gy': egy,
'msg': '', 'msg': '',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -1024,17 +1026,17 @@ class CrystallographyTest(unittest.TestCase):
the_input = lookup_tag('crystallography')(test_capa_system(), element, state) the_input = lookup_tag('crystallography')(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': value, 'value': value,
'status': inputtypes.Status('unsubmitted'), 'status': inputtypes.Status('unsubmitted'),
'msg': '', 'msg': '',
'width': width, 'width': width,
'height': height, 'height': height,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -1070,10 +1072,10 @@ class VseprTest(unittest.TestCase):
the_input = lookup_tag('vsepr_input')(test_capa_system(), element, state) the_input = lookup_tag('vsepr_input')(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': value, 'value': value,
'status': inputtypes.Status('unsubmitted'), 'status': inputtypes.Status('unsubmitted'),
'msg': '', 'msg': '',
@@ -1082,7 +1084,7 @@ class VseprTest(unittest.TestCase):
'molecules': molecules, 'molecules': molecules,
'geometries': geometries, 'geometries': geometries,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -1108,17 +1110,17 @@ class ChemicalEquationTest(unittest.TestCase):
def test_rendering(self): def test_rendering(self):
''' Verify that the render context matches the expected render context''' ''' Verify that the render context matches the expected render context'''
context = self.the_input._get_render_context() # pylint: disable=protected-access context = self.the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'H2OYeah', 'value': 'H2OYeah',
'status': inputtypes.Status('unanswered'), 'status': inputtypes.Status('unanswered'),
'msg': '', 'msg': '',
'size': self.size, 'size': self.size,
'previewer': '/dummy-static/js/capa/chemical_equation_preview.js', 'previewer': '/dummy-static/js/capa/chemical_equation_preview.js',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -1201,10 +1203,10 @@ class FormulaEquationTest(unittest.TestCase):
Verify that the render context matches the expected render context Verify that the render context matches the expected render context
""" """
context = self.the_input._get_render_context() # pylint: disable=protected-access context = self.the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'x^2+1/2', 'value': 'x^2+1/2',
'status': inputtypes.Status('unanswered'), 'status': inputtypes.Status('unanswered'),
'msg': '', 'msg': '',
@@ -1213,7 +1215,7 @@ class FormulaEquationTest(unittest.TestCase):
'inline': False, 'inline': False,
'trailing_text': '', 'trailing_text': '',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -1247,10 +1249,10 @@ class FormulaEquationTest(unittest.TestCase):
the_input = lookup_tag('formulaequationinput')(test_capa_system(), element, state) the_input = lookup_tag('formulaequationinput')(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'prob_1_2'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': 'x^2+1/2', 'value': 'x^2+1/2',
'status': inputtypes.Status('unanswered'), 'status': inputtypes.Status('unanswered'),
'msg': '', 'msg': '',
@@ -1259,7 +1261,7 @@ class FormulaEquationTest(unittest.TestCase):
'inline': False, 'inline': False,
'trailing_text': expected_text, 'trailing_text': expected_text,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.assertEqual(context, expected) self.assertEqual(context, expected)
@@ -1381,17 +1383,17 @@ class DragAndDropTest(unittest.TestCase):
} }
the_input = lookup_tag('drag_and_drop_input')(test_capa_system(), element, state) the_input = lookup_tag('drag_and_drop_input')(test_capa_system(), element, state)
prob_id = 'prob_1_2'
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'prob_1_2', 'id': prob_id,
'value': value, 'value': value,
'status': inputtypes.Status('unsubmitted'), 'status': inputtypes.Status('unsubmitted'),
'msg': '', 'msg': '',
'drag_and_drop_json': json.dumps(user_input), 'drag_and_drop_json': json.dumps(user_input),
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
# as we are dumping 'draggables' dicts while dumping user_input, string # as we are dumping 'draggables' dicts while dumping user_input, string
@@ -1437,10 +1439,10 @@ class AnnotationInputTest(unittest.TestCase):
the_input = lookup_tag(tag)(test_capa_system(), element, state) the_input = lookup_tag(tag)(test_capa_system(), element, state)
context = the_input._get_render_context() # pylint: disable=protected-access context = the_input._get_render_context() # pylint: disable=protected-access
prob_id = 'annotation_input'
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'id': 'annotation_input', 'id': prob_id,
'status': inputtypes.Status('answered'), 'status': inputtypes.Status('answered'),
'msg': '', 'msg': '',
'title': 'foo', 'title': 'foo',
@@ -1460,7 +1462,7 @@ class AnnotationInputTest(unittest.TestCase):
'debug': False, 'debug': False,
'return_to_annotation': True, 'return_to_annotation': True,
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
self.maxDiff = None self.maxDiff = None
@@ -1499,9 +1501,10 @@ class TestChoiceText(unittest.TestCase):
</{tag}> </{tag}>
""".format(tag=tag, choice_tag=choice_tag) """.format(tag=tag, choice_tag=choice_tag)
element = etree.fromstring(xml_str) element = etree.fromstring(xml_str)
prob_id = 'choicetext_input'
state = { state = {
'value': '{}', 'value': '{}',
'id': 'choicetext_input', 'id': prob_id,
'status': inputtypes.Status('answered'), 'status': inputtypes.Status('answered'),
'response_data': RESPONSE_DATA 'response_data': RESPONSE_DATA
} }
@@ -1516,7 +1519,6 @@ class TestChoiceText(unittest.TestCase):
('choiceinput_0', [first_choice_content, first_input]), ('choiceinput_0', [first_choice_content, first_input]),
('choiceinput_1', [second_choice_content, second_input, second_choice_text]) ('choiceinput_1', [second_choice_content, second_input, second_choice_text])
] ]
expected = { expected = {
'STATIC_URL': '/dummy-static/', 'STATIC_URL': '/dummy-static/',
'msg': '', 'msg': '',
@@ -1525,7 +1527,7 @@ class TestChoiceText(unittest.TestCase):
'show_correctness': 'always', 'show_correctness': 'always',
'submitted_message': 'Answer received.', 'submitted_message': 'Answer received.',
'response_data': RESPONSE_DATA, 'response_data': RESPONSE_DATA,
'describedby_html': DESCRIBEDBY 'describedby_html': DESCRIBEDBY.format(status_id=prob_id)
} }
expected.update(state) expected.update(state)
the_input = lookup_tag(tag)(test_capa_system(), element, state) the_input = lookup_tag(tag)(test_capa_system(), element, state)

View File

@@ -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
// ==================== // ====================
@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 {
@include status-icon($incorrect, $cross-icon); @include status-icon($incorrect, $cross-icon);
} }
&.unsubmitted,
&.unanswered {
.status-icon {
content: '';
}
}
} }
} }
} }
@@ -818,12 +823,14 @@ div.problem {
} }
.status { .status {
&:after { .status-icon {
content: ''; // clear out correct or incorrect icon &:after {
content: '';
}
} }
} }
} }
} }
.trailing_text { .trailing_text {

View File

@@ -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 @@
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) {

View File

@@ -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

View File

@@ -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):
@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,
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,
{'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
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
)) ))
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)']