Commit c3b6b104 by muhammad-ammar Committed by cahrens

Problem (capa) feedback UX revamp.

TNL-4877
parent 432f6cdf
......@@ -87,3 +87,7 @@
// +CodeMirror Overrides
// ====================
@import 'elements/codemirror-overrides';
// CAPA Problem Feedback
@import 'edx-pattern-library-shims/buttons';
../../../common/static/sass/edx-pattern-library-shims
\ No newline at end of file
......@@ -399,7 +399,6 @@
margin: 0 auto;
width: flex-grid(12);
max-width: $fg-max-width;
min-width: $fg-min-width;
strong {
@extend %t-strong;
......
......@@ -248,15 +248,7 @@
color: $color-visibility-set;
}
}
.action {
.save {
// taking styles from LMS for these Save buttons to maintain consistency
// there is no studio-specific style for these LMS-styled buttons
@extend %btn-lms-style;
}
}
}
// +Messaging - Xblocks
......
......@@ -32,9 +32,8 @@ $headings-base-color: $gray-d2;
%hd-2 {
margin-bottom: 1em;
font-size: 1.5em;
font-weight: $headings-font-weight-normal;
font-size: 1.1125em;
font-weight: $headings-font-weight-bold;
line-height: 1.4em;
}
......
......@@ -376,7 +376,7 @@ class LoncapaProblem(object):
def grade_answers(self, answers):
"""
Grade student responses. Called by capa_module.check_problem.
Grade student responses. Called by capa_module.submit_problem.
`answers` is a dict of all the entries from request.POST, but with the first part
of each key removed (the string before the first "_").
......@@ -496,6 +496,7 @@ class LoncapaProblem(object):
choice-level explanations shown to a student after submission.
Does nothing if there is no targeted-feedback attribute.
"""
_ = self.capa_system.i18n.ugettext
# Note that the modifications has been done, avoiding problems if called twice.
if hasattr(self, 'has_targeted'):
return
......@@ -515,9 +516,12 @@ class LoncapaProblem(object):
# Keep track of the explanation-id that corresponds to the student's answer
# Also, keep track of the solution-id
solution_id = None
choice_correctness_for_student_answer = _('Incorrect')
for choice in choices_list:
if choice.get('name') == student_answer:
expl_id_for_student_answer = choice.get('explanation-id')
if choice.get('correct') == 'true':
choice_correctness_for_student_answer = _('Correct')
if choice.get('correct') == 'true':
solution_id = choice.get('explanation-id')
......@@ -527,7 +531,15 @@ class LoncapaProblem(object):
if len(targetedfeedbackset) != 0:
targetedfeedbackset = targetedfeedbackset[0]
targetedfeedbacks = targetedfeedbackset.xpath('./targetedfeedback')
# find the legend by id in choicegroup.html for aria-describedby
problem_legend_id = str(choicegroup.get('id')) + '-legend'
for targetedfeedback in targetedfeedbacks:
screenreadertext = etree.Element("span")
targetedfeedback.insert(0, screenreadertext)
screenreadertext.set('class', 'sr')
screenreadertext.text = choice_correctness_for_student_answer
targetedfeedback.set('role', 'group')
targetedfeedback.set('aria-describedby', problem_legend_id)
# Don't show targeted feedback if the student hasn't answer the problem
# or if the target feedback doesn't match the student's (incorrect) answer
if not self.done or targetedfeedback.get('explanation-id') != expl_id_for_student_answer:
......@@ -561,6 +573,7 @@ class LoncapaProblem(object):
# Add our solution instead to the targetedfeedbackset and change its tag name
solution_element.tag = 'targetedfeedback'
targetedfeedbackset.append(solution_element)
def get_html(self):
......
......@@ -51,6 +51,7 @@ from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautifu
import capa.xqueue_interface as xqueue_interface
import capa.safe_exec as safe_exec
from openedx.core.djangolib.markup import HTML, Text
log = logging.getLogger(__name__)
......@@ -352,9 +353,9 @@ class LoncapaResponse(object):
# Tricky: label None means output defaults, while '' means output empty label
if label is None:
if correct:
label = _(u'Correct')
label = _(u'Correct:')
else:
label = _(u'Incorrect')
label = _(u'Incorrect:')
# self.runtime.track_function('get_demand_hint', event_info)
# This this "feedback hint" event
......@@ -372,15 +373,23 @@ class LoncapaResponse(object):
self.capa_module.runtime.track_function('edx.problem.hint.feedback_displayed', event_info)
# Form the div-wrapped hint texts
hints_wrap = u''.join(
[u'<div class="{0}">{1}</div>'.format(QUESTION_HINT_TEXT_STYLE, dct.get('text'))
for dct in hint_log]
hints_wrap = HTML('').join(
[HTML('<div class="{question_hint_text_style}">{hint_content}</div>').format(
question_hint_text_style=QUESTION_HINT_TEXT_STYLE,
hint_content=HTML(dct.get('text'))
) for dct in hint_log]
)
if multiline_mode:
hints_wrap = u'<div class="{0}">{1}</div>'.format(QUESTION_HINT_MULTILINE, hints_wrap)
hints_wrap = HTML('<div class="{question_hint_multiline}">{hints_wrap}</div>').format(
question_hint_multiline=QUESTION_HINT_MULTILINE,
hints_wrap=hints_wrap
)
label_wrap = ''
if label:
label_wrap = u'<div class="{0}">{1}: </div>'.format(QUESTION_HINT_LABEL_STYLE, label)
label_wrap = HTML('<span class="{question_hint_label_style}">{label} </span>').format(
question_hint_label_style=QUESTION_HINT_LABEL_STYLE,
label=Text(label)
)
# Establish the outer style
if correct:
......@@ -389,7 +398,12 @@ class LoncapaResponse(object):
style = QUESTION_HINT_INCORRECT_STYLE
# Ready to go
return u'<div class="{0}">{1}{2}</div>'.format(style, label_wrap, hints_wrap)
return HTML('<div class="{st}"><div class="explanation-title">{text}</div>{lwrp}{hintswrap}</div>').format(
st=style,
text=Text(_("Answer")),
lwrp=label_wrap,
hintswrap=hints_wrap
)
def get_extended_hints(self, student_answers, new_cmap):
"""
......
......@@ -17,7 +17,7 @@
<div class="block">${comment_prompt}</div>
<textarea class="comment" id="input_${id}_comment" name="input_${id}_comment" aria-describedby="answer_${id}">${comment_value|h}</textarea>
<div class="block">${tag_prompt}</div>
<div class="block" id="label_${id}">${tag_prompt}</div>
<ul class="tags">
% for option in options:
<li>
......@@ -53,12 +53,12 @@
<input type="hidden" class="value" name="input_${id}" id="input_${id}" value="${value|h}" />
% endif
<span class="status ${status.classname}" id="status_${id}" aria-describedby="input_${id}"><span class="sr">${status.display_name}</span></span>
<span class="status ${status.classname}" id="status_${id}" aria-describedby="label_${id}"><span class="sr">${status.display_name}</span></span>
<p id="answer_${id}" class="answer answer-annotation"></p>
</div>
</form>
% if msg:
<span class="message">${HTML(msg)}</span>
<span class="message" aria-describedby="label_${id}" tabindex="-1">${HTML(msg)}</span>
% endif
......@@ -11,8 +11,8 @@
/>
<p class="status" aria-describedby="input_${id}">
${value|h} -
${status.display_name}
${value|h}
<span class="sr">${status.display_name}</span>
</p>
<div id="input_${id}_preview" class="equation"></div>
......
......@@ -15,7 +15,7 @@
<p class="question-description" id="${description_id}">${description_text}</p>
% endfor
% for choice_id, choice_label in choices:
<div class="field" aria-live="polite" aria-atomic="true">
<div class="field">
<%
label_class = 'response-label field-label label-inline'
%>
......@@ -60,7 +60,7 @@
</fieldset>
<div class="indicator-container">
% if input_type == 'checkbox' or not value:
<span class="status ${status.classname if show_correctness != 'never' else 'unanswered'}" id="status_${id}" data-tooltip="${status.display_tooltip}">
<span class="status ${status.classname if show_correctness != 'never' else 'unanswered'}" id="status_${id}" aria-describedby="${id}-legend" data-tooltip="${status.display_tooltip}">
<span class="sr">${status.display_tooltip}</span>
</span>
% endif
......@@ -69,6 +69,6 @@
<div class="capa_alert">${submitted_message}</div>
%endif
% if msg:
<span class="message">${HTML(msg)}</span>
<span class="message" aria-describedby="${id}-legend" tabindex="-1">${HTML(msg)}</span>
% endif
</form>
<%! from capa.util import remove_markup %>
<%! from django.utils.translation import ugettext as _ %>
<%! from capa.util import remove_markup
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML
%>
<% element_checked = False %>
% for choice_id, _ in choices:
<% choice_id = choice_id %>
......@@ -63,7 +66,9 @@
<div class="indicator-container">
% if input_type == 'checkbox' or not element_checked:
<span class="status ${status.classname}" id="status_${id}"></span>
<span class="status ${status.classname}" id="status_${id}">
<span class="sr">${status.display_name}</span>
</span>
% endif
</div>
......@@ -71,7 +76,7 @@
<div class="capa_alert">${_(submitted_message)}</div>
%endif
% if msg:
<span class="message">${msg|n}</span>
<span class="message" tabindex="-1">${HTML(msg)}</span>
% endif
</form>
</section>
<%! from django.utils.translation import ugettext as _ %>
<%!
from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML
%>
<section id="textbox_${id}" class="capa_inputtype textbox cminput">
<textarea rows="${rows}" cols="${cols}" name="input_${id}"
aria-label="${_("{programming_language} editor").format(programming_language=mode)}"
......@@ -35,7 +38,7 @@
<span id="answer_${id}"></span>
<div class="external-grader-message" aria-live="polite">
${msg|n}
<div class="external-grader-message">
${HTML(msg)}
</div>
</section>
<%! from openedx.core.djangolib.markup import HTML %>
<section id="inputtype_${id}" class="capa_inputtype" >
<div class="crystalography_problem" style="width:${width};height:${height}"></div>
......@@ -16,13 +17,13 @@
<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}">
${status.display_name}
<span class="sr">${status.display_name}</span>
</p>
<p id="answer_${id}" class="answer"></p>
% if msg:
<span class="message">${msg|n}</span>
<span class="message" tabindex="-1">${HTML(msg)}</span>
% endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
......
......@@ -11,7 +11,7 @@
<input type="hidden" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"/>
<p class="status" aria-describedby="input_${id}">
${status.display_name}
<span class="sr">${status.display_name}</span>
</p>
<p id="answer_${id}" class="answer"></p>
......
......@@ -17,14 +17,14 @@
<input type="text" name="input_${id}" id="input_${id}" aria-describedby="answer_${id}" value="${value|h}"
style="display:none;"/>
<p class="status" aria-describedby="input_${id}">
${status.display_name}
<p class="status drag-and-drop--status" aria-describedby="input_${id}">
<span class="sr">${status.display_name}</span>
</p>
<p id="answer_${id}" class="answer"></p>
% if msg:
<span class="message">${HTML(msg)}</span>
<span class="message" tabindex="-1">${HTML(msg)}</span>
% endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
......
......@@ -12,7 +12,7 @@
<input type="hidden" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}"/>
<p class="status" aria-describedby="input_${id}">
${status.display_name}
<span class="sr">${status.display_name}</span>
</p>
<p id="answer_${id}" class="answer"></p>
......
......@@ -17,9 +17,8 @@
<p id="answer_${id}" class="answer"></p>
<p class="status" aria-describedby="input_${id}">
${status.display_name}
<span class="sr">${status.display_name}</span>
</p>
<br/> <br/>
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
......
......@@ -10,5 +10,5 @@
<input type="file" name="input_${id}" id="input_${id}" value="${value}" multiple="multiple" data-required_files="${required_files|h}" data-allowed_files="${allowed_files|h}" aria-label="${response_data['label']}"/>
</div>
<div class="message">${HTML(msg)}</div>
<div class="message" tabindex="-1">${HTML(msg)}</div>
</section>
......@@ -4,7 +4,7 @@
<div id="formulaequationinput_${id}" class="inputtype formulaequationinput" ${doinline | n, decode.utf8}>
<div class="${status.classname}" id="status_${id}">
% if response_data['label']:
<label class="problem-group-label" for="input_${id}">${response_data['label']}</label>
<label class="problem-group-label" for="input_${id}" id="label_${id}">${response_data['label']}</label>
% endif
% for description_id, description_text in response_data['descriptions'].items():
<p class="question-description" id="${description_id}">${description_text}</p>
......@@ -18,7 +18,7 @@
/>
<span class="trailing_text">${trailing_text}</span>
<span class="status" id="${id}_status" data-tooltip="${status.display_tooltip}">
<span class="status" id="${id}_status" aria-describedby="label_${id}" data-tooltip="${status.display_tooltip}">
<span class="sr">${status.display_tooltip}</span>
</span>
......@@ -33,6 +33,6 @@
<div class="script_placeholder" data-src="${previewer}"/>
% if msg:
<span class="message">${HTML(msg)}</span>
<span class="message" aria-describedby="label_${id}" tabindex="-1">${HTML(msg)}</span>
% endif
</div>
<%! from openedx.core.djangolib.markup import HTML %>
<section id="inputtype_${id}" class="jsinput"
data="${gradefn}"
% if saved_state:
......@@ -41,9 +42,8 @@
<p id="answer_${id}" class="answer"></p>
<p class="status">
${status.display_name}
<span class="sr">${status.display_name}</span>
</p>
<br/> <br/>
<div class="error_message" style="padding: 5px 5px 5px 5px; background-color:#FA6666; height:60px;width:400px; display: none"></div>
......@@ -52,6 +52,6 @@
% endif
% if msg:
<span class="message">${msg|n}</span>
<span class="message" tabindex="-1">${HTML(msg)}</span>
% endif
</section>
<section id="jstextline_${id}" class="jstextline">
<input type="text" name="input_${id}" id="input_${id}" value="${value}"
% if size:
size="${size}"
% endif
% if dojs == 'math':
onkeyup="DoUpdateMath('${id}')"
% endif
/>
% if dojs == 'math':
<span id="display_${id}">`{::}`</span>
% endif
<span id="answer_${id}"></span>
% if dojs == 'math':
<textarea style="display:none" id="input_${id}_fromjs" name="input_${id}_fromjs"></textarea>
% endif
<span class="status ${status.classname}" id="status_${id}" aria-describedby="input_${id}">
<span class="sr">${status.display_name}</span>
</span>
% if msg:
<br/>
<span class="debug">${msg|n}</span>
% endif
</section>
......@@ -4,7 +4,7 @@
<form class="inputtype option-input ${doinline}">
% if response_data['label']:
<label class="problem-group-label" for="input_${id}">${response_data['label']}</label>
<label class="problem-group-label" for="input_${id}" id="label_${id}">${response_data['label']}</label>
% endif
% for description_id, description_text in response_data['descriptions'].items():
......@@ -23,12 +23,14 @@
</select>
<div class="indicator-container">
<span class="status ${status.classname}" id="status_${id}" data-tooltip="${status.display_tooltip}">
<span class="status ${status.classname}"
id="status_${id}"
aria-describedby="label_${id}" data-tooltip="${status.display_tooltip}">
<span class="sr">${status.display_tooltip}</span>
</span>
</div>
<p class="answer" id="answer_${id}"></p>
% if msg:
<span class="message">${HTML(msg)}</span>
<span class="message" aria-describedby="label_${id}" tabindex="-1">${HTML(msg)}</span>
% endif
</form>
......@@ -17,7 +17,7 @@
% endif
% if response_data['label']:
<label class="problem-group-label" for="input_${id}">${response_data['label']}</label>
<label class="problem-group-label" for="input_${id}" id="label_${id}">${response_data['label']}</label>
% endif
% for description_id, description_text in response_data['descriptions'].items():
......@@ -36,7 +36,7 @@
/>
<span class="trailing_text">${trailing_text}</span>
<span class="status" data-tooltip="${status.display_tooltip}">
<span class="status" aria-describedby="label_${id}" data-tooltip="${status.display_tooltip}">
<span class="sr">${status.display_tooltip}</span>
</span>
......@@ -51,8 +51,8 @@
</div>
% endif
% if msg:
<span class="message">${HTML(msg)}</span>
% endif
% if msg:
<span class="message" aria-describedby="label_${id}" tabindex="-1">${HTML(msg)}</span>
% endif
</div>
......@@ -20,14 +20,14 @@
style="display:none;"
/>
<p class="status" aria-describedby="input_${id}">
${status.display_name}
<p class="status">
<span class="sr">${status.display_name}</span>
</p>
<p id="answer_${id}" class="answer"></p>
% if msg:
<span class="message">${HTML(msg)}</span>
<span class="message" tabindex="-1">${HTML(msg)}</span>
% endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
</div>
......
......@@ -37,7 +37,7 @@ class ResponseXMLFactory(object):
For all response types, **kwargs can contain:
*question_text*: The text of the question to display,
wrapped in <p> tags.
wrapped in <label> tags.
*explanation_text*: The detailed explanation that will
be shown if the user answers incorrectly.
......@@ -72,10 +72,6 @@ class ResponseXMLFactory(object):
script_element.set("type", "loncapa/python")
script_element.text = str(script)
# The problem has a child <p> with question text
question = etree.SubElement(root, "p")
question.text = question_text
# Add the response(s)
for __ in range(int(num_responses)):
response_element = self.create_response_element(**kwargs)
......@@ -86,6 +82,10 @@ class ResponseXMLFactory(object):
root.append(response_element)
# Add the question label
question = etree.SubElement(response_element, "label")
question.text = question_text
# Add input elements
for __ in range(int(num_inputs)):
input_element = self.create_input_element(**kwargs)
......@@ -113,9 +113,13 @@ class ResponseXMLFactory(object):
"""
math_display = kwargs.get('math_display', False)
size = kwargs.get('size', None)
input_element_label = kwargs.get('input_element_label', '')
input_element = etree.Element('textline')
if input_element_label:
input_element.set('label', input_element_label)
if math_display:
input_element.set('math', '1')
......@@ -267,9 +271,6 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
*answer_attr*: The "answer" attribute on the tag itself (treated as an
alias to "expect", though "expect" takes priority if both are given)
*group_label*: Text to represent group of inputs when there are
multiple inputs.
"""
# Retrieve **kwargs
......@@ -279,7 +280,6 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
answer = kwargs.get('answer', None)
options = kwargs.get('options', None)
cfn_extra_args = kwargs.get('cfn_extra_args', None)
group_label = kwargs.get('group_label', None)
# Create the response element
response_element = etree.Element("customresponse")
......@@ -297,10 +297,6 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
answer_element = etree.SubElement(response_element, "answer")
answer_element.text = str(answer)
if group_label:
group_label_element = etree.SubElement(response_element, "label")
group_label_element.text = group_label
if options:
response_element.set('options', str(options))
......
......@@ -151,10 +151,6 @@ class CapaHtmlRenderTest(unittest.TestCase):
# Expect problem has been turned into a <div>
self.assertEqual(rendered_html.tag, "div")
# Expect question text is in a <p> child
question_element = rendered_html.find("p")
self.assertEqual(question_element.text, "Test question")
# Expect that the response has been turned into a <section> with correct attributes
response_element = rendered_html.find("section")
self.assertEqual(response_element.tag, "section")
......@@ -185,7 +181,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
'id': '1_2_1',
'trailing_text': '',
'size': None,
'response_data': {'label': '', 'descriptions': {}},
'response_data': {'label': 'Test question', 'descriptions': {}},
'describedby_html': ''
}
......
......@@ -930,7 +930,7 @@ class DragAndDropTemplateTest(TemplateTestCase):
self.assert_has_xpath(xml, xpath, self.context)
# Expect a <p> with the status
xpath = "//p[@class='status']"
xpath = "//p[@class='status drag-and-drop--status']/span[@class='sr']"
self.assert_has_text(xml, xpath, expected_text, exact=False)
def test_drag_and_drop_json_html(self):
......
......@@ -96,8 +96,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "")
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback3\">.*3rd WRONG solution")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback3\" role=\"group\" aria-describedby=\"1_2_1-legend\">\s*<span class=\"sr\">Incorrect</span>.*3rd WRONG solution")
self.assertNotRegexpMatches(without_new_lines, r"feedback1|feedback2|feedbackC")
# Check that calling it multiple times yields the same thing
the_html2 = problem.get_html()
......@@ -110,11 +110,24 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "")
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\">.*1st WRONG solution")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\" role=\"group\" aria-describedby=\"1_2_1-legend\">\s*<span class=\"sr\">Incorrect</span>.*1st WRONG solution")
self.assertRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>")
self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3|feedbackC")
def test_targeted_feedback_correct_answer(self):
""" Test the case of targeted feedback for a correct answer. """
problem = new_loncapa_problem(load_fixture('targeted_feedback.xml'))
problem.done = True
problem.student_answers = {'1_2_1': 'choice_2'}
the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines,
r"<targetedfeedback explanation-id=\"feedbackC\" role=\"group\" aria-describedby=\"1_2_1-legend\">\s*<span class=\"sr\">Correct</span>.*Feedback on your correct solution...")
self.assertNotRegexpMatches(without_new_lines, r"feedback1|feedback2|feedback3")
def test_targeted_feedback_id_typos(self):
"""Cases where the explanation-id's don't match anything."""
xml_str = textwrap.dedent("""
......@@ -280,8 +293,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "")
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\">.*1st WRONG solution")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\" role=\"group\" aria-describedby=\"1_2_1-legend\">.*1st WRONG solution")
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedbackC\".*solution explanation")
self.assertNotRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>")
self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3")
......@@ -350,8 +363,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "")
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\">.*1st WRONG solution")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\" role=\"group\" aria-describedby=\"1_2_1-legend\">.*1st WRONG solution")
self.assertNotRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedbackC\".*solution explanation")
self.assertRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>")
self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3|feedbackC")
......@@ -427,8 +440,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "")
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\">.*1st WRONG solution")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\" role=\"group\" aria-describedby=\"1_2_1-legend\">.*1st WRONG solution")
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedbackC2\".*other solution explanation")
self.assertNotRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>")
self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3")
......
......@@ -69,7 +69,7 @@ class CapaModule(CapaMixin, XModule):
handlers = {
'hint_button': self.hint_button,
'problem_get': self.get_problem,
'problem_check': self.check_problem,
'problem_check': self.submit_problem,
'problem_reset': self.reset_problem,
'problem_save': self.save_problem,
'problem_show': self.get_answer,
......@@ -212,7 +212,6 @@ class CapaDescriptor(CapaFields, RawDescriptor):
CapaDescriptor.graceperiod,
CapaDescriptor.force_save_button,
CapaDescriptor.markdown,
CapaDescriptor.text_customization,
CapaDescriptor.use_latex_compiler,
])
return non_editable_fields
......@@ -276,9 +275,9 @@ class CapaDescriptor(CapaFields, RawDescriptor):
# Proxy to CapaModule for access to any of its attributes
answer_available = module_attr('answer_available')
check_button_name = module_attr('check_button_name')
check_button_checking_name = module_attr('check_button_checking_name')
check_problem = module_attr('check_problem')
submit_button_name = module_attr('submit_button_name')
submit_button_submitting_name = module_attr('submit_button_submitting_name')
submit_problem = module_attr('submit_problem')
choose_new_seed = module_attr('choose_new_seed')
closed = module_attr('closed')
get_answer = module_attr('get_answer')
......@@ -301,7 +300,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
reset_problem = module_attr('reset_problem')
save_problem = module_attr('save_problem')
set_state_from_lcp = module_attr('set_state_from_lcp')
should_show_check_button = module_attr('should_show_check_button')
should_show_submit_button = module_attr('should_show_submit_button')
should_show_reset_button = module_attr('should_show_reset_button')
should_show_save_button = module_attr('should_show_save_button')
update_score = module_attr('update_score')
<div class="problem">
<div aria-live="polite">
<div>
<span>
<p>
<p></p>
</span>
<span><section id="textbox_test_matlab_plot1_2_1" class="capa_inputtype cminput">
<textarea rows="10" cols="80" name="input_i4x-MITx-2_01x-problem-test_matlab_plot1_2_1" aria-describedby="answer_i4x-MITx-2_01x-problem-test_matlab_plot1_2_1" id="input_i4x-MITx-2_01x-problem-test_matlab_plot1_2_1" data-tabsize="4" data-mode="octave" data-linenums="true" style="display: none;">This is the MATLAB input, whatever that may be.</textarea>
<div>
<span>
<section id="textbox_test_matlab_plot1_2_1" class="capa_inputtype cminput">
<textarea rows="10" cols="80" name="input_i4x-MITx-2_01x-problem-test_matlab_plot1_2_1"
aria-describedby="answer_i4x-MITx-2_01x-problem-test_matlab_plot1_2_1"
id="input_i4x-MITx-2_01x-problem-test_matlab_plot1_2_1" data-tabsize="4" data-mode="octave"
data-linenums="true" style="display: none;">This is the MATLAB input, whatever that may be.
</textarea>
<div class="grader-status" tabindex="-1">
<span id="status_test_matlab_plot1_2_1" class="processing" aria-describedby="input_test_matlab_plot1_2_1">
<span class="status sr">processing</span>
</span>
<span style="display:none;" class="xqueue" id="test_matlab_plot1_2_1">1</span>
<div class="grader-status" tabindex="-1">
<span id="status_test_matlab_plot1_2_1" class="processing" aria-describedby="input_test_matlab_plot1_2_1">
<span class="status sr">processing</span>
</span>
<span style="display:none;" class="xqueue" id="test_matlab_plot1_2_1">1</span>
<p class="debug">processing</p>
</div>
<span id="answer_test_matlab_plot1_2_1"></span>
<div class="external-grader-message" aria-live="polite">
Submitted. As soon as a response is returned, this message will be replaced by that feedback.
</div>
<div class="ungraded-matlab-result" aria-live="polite">
</div>
<p class="debug">processing</p>
<div class="plot-button">
<input type="button" class="save" name="plot-button" id="plot_test_matlab_plot1_2_1" value="Run Code">
</div>
</section>
</span>
</div>
<span id="answer_test_matlab_plot1_2_1"></span>
<div class="external-grader-message" aria-live="polite">
Submitted. As soon as a response is returned, this message will be replaced by that feedback.
</div>
<div class="ungraded-matlab-result" aria-live="polite">
</div>
<div class="plot-button">
<input type="button" class="save" name="plot-button" id="plot_test_matlab_plot1_2_1" value="Run Code">
</div>
</section></span>
</div>
</div>
<div class="action">
<input type="hidden" name="problem_id" value="Plot a straight line">
<button class="reset" data-value="Reset">Reset<span class="sr"> your answer</span></button>
<button class="show"><span class="show-label">Show Answer</span> </button>
</div>
<div class="notification warning notification-gentle-alert is-hidden" tabindex="-1">
<span class="icon fa fa-exclamation-circle" aria-hidden="true"></span>
<span class="notification-message" aria-describedby="title">
</span>
<div class="notification-btn-wrapper">
<button class="btn btn-default btn-small notification-btn review-btn sr">Review</button>
</div>
</div>
</div>
......@@ -12,11 +12,29 @@
<span id="display_example_1"></span>
<span id="input_example_1_dynamath"></span>
<button class="check Check" data-checking="Checking..." data-value="Check"><span class="check-label">Check</span><span class="sr"> your answer</span></button>
<button class="reset">Reset</button>
<button class="save">Save</button>
<button class="show"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>
<div class="problem-action-buttons-wrapper">
<span class="problem-action-button-wrapper">
<button class="reset btn-default btn-small">Reset</button>
</span>
<span class="problem-action-button-wrapper">
<button class="save btn-default btn-small">Save</button>
</span>
<span class="problem-action-button-wrapper">
<button class="show btn-default btn-small"><span class="show-label">Show Answer(s)</span> <span class="sr">(for question(s) above - adjacent to each field)</span></button>
</span>
</div>
<button class="submit btn-brand" data-submitting="Submitting" data-value="Submit" data-should-enable-submit-button="True"><span class="submit-label">Submit</span><span class="sr"> your answer</span></button>
<a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a>
<div class="submission_feedback"></div>
</div>
<div class="notification warning notification-gentle-alert is-hidden" tabindex="-1">
<span class="icon fa fa-exclamation-circle" aria-hidden="true"></span>
<span class="notification-message" aria-describedby="title">
</span>
<div class="notification-btn-wrapper">
<button class="btn btn-default btn-small notification-btn review-btn sr">Review</button>
</div>
</div>
</div>
......@@ -17,9 +17,7 @@ var options = {
// Avoid adding files to this list. Use RequireJS.
libraryFilesToInclude: [
{pattern: 'common_static/js/vendor/requirejs/require.js', included: true},
{pattern: 'RequireJS-namespace-undefine.js', included: true},
// Load the core JavaScript dependencies
{pattern: 'common_static/coffee/src/ajax_prefix.js', included: true},
{pattern: 'common_static/common/js/vendor/underscore.js', included: true},
{pattern: 'common_static/common/js/vendor/backbone.js', included: true},
......@@ -45,11 +43,20 @@ var options = {
{pattern: 'public/js/split_test_staff.js', included: true},
{pattern: 'src/word_cloud/d3.min.js', included: true},
// Load test utilities
{pattern: 'common_static/js/vendor/jasmine-imagediff.js', included: true},
{pattern: 'common_static/common/js/spec_helpers/jasmine-waituntil.js', included: true},
{pattern: 'common_static/common/js/spec_helpers/jasmine-extensions.js', included: true},
{pattern: 'common_static/js/vendor/sinon-1.17.0.js', included: true},
// Load the edX global namespace before RequireJS is installed
{pattern: 'common_static/edx-ui-toolkit/js/utils/global-loader.js', included: true},
{pattern: 'common_static/edx-ui-toolkit/js/utils/string-utils.js', included: true},
{pattern: 'common_static/edx-ui-toolkit/js/utils/html-utils.js', included: true},
// Load RequireJS and move it into the RequireJS namespace
{pattern: 'common_static/js/vendor/requirejs/require.js', included: true},
{pattern: 'RequireJS-namespace-undefine.js', included: true},
{pattern: 'spec/main_requirejs.js', included: true}
],
......
......@@ -125,11 +125,6 @@ class InheritanceMixin(XBlockMixin):
scope=Scope.settings,
default='',
)
text_customization = Dict(
display_name=_("Text Customization"),
help=_("Enter string customization substitutions for particular locations."),
scope=Scope.settings,
)
use_latex_compiler = Boolean(
display_name=_("Enable LaTeX Compiler"),
help=_("Enter true or false. If true, you can use the LaTeX templates for HTML components and advanced Problem components."),
......
......@@ -43,7 +43,7 @@ data: |
par is a dictionary that contains two keys, "answer" and "state".
The value of "answer" is the JSON string that "getGrade" returns.
The value of "state" is the JSON string that "getState" returns.
Clicking either "Check" or "Save" registers the current state.
Clicking either "Submit" or "Save" registers the current state.
'''
par = json.loads(ans)
......
......@@ -3,7 +3,7 @@ Tests the logic of problems with a delay between attempt submissions.
Note that this test file is based off of test_capa_module.py and as
such, uses the same CapaFactory problem setup to test the functionality
of the check_problem method of a capa module when the "delay between quiz
of the submit_problem method of a capa module when the "delay between quiz
submissions" setting is set to different values
"""
......@@ -128,7 +128,7 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase):
last_submission_time=None,
submission_wait_seconds=None,
considered_now=None,
skip_check_problem=False):
skip_submit_problem=False):
"""Unified create and check code for the tests here."""
module = CapaFactoryWithDelay.create(
attempts=num_attempts,
......@@ -138,12 +138,12 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase):
)
module.done = False
get_request_dict = {CapaFactoryWithDelay.input_key(): "3.14"}
if skip_check_problem:
if skip_submit_problem:
return (module, None)
if considered_now is not None:
result = module.check_problem(get_request_dict, considered_now)
result = module.submit_problem(get_request_dict, considered_now)
else:
result = module.check_problem(get_request_dict)
result = module.submit_problem(get_request_dict)
return (module, result)
def test_first_submission(self):
......@@ -251,13 +251,13 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase):
considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0, tzinfo=UTC)
)
# Now try it without the check_problem
# Now try it without the submit_problem
(module, unused_result) = self.create_and_check(
num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=180,
considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0, tzinfo=UTC),
skip_check_problem=True
skip_submit_problem=True
)
# Expect that number of attempts NOT incremented
self.assertEqual(module.attempts, num_attempts)
......
......@@ -290,6 +290,7 @@ class XBlockWrapperTestMixin(object):
# pylint: disable=no-member
descriptor.runtime.id_reader.get_definition_id = Mock(return_value='a')
descriptor.runtime.modulestore = modulestore
descriptor._xmodule.graded = 'False'
self.check_property(descriptor)
# Test that when an xmodule is generated from descriptor_cls
......
<div>
<span class="status">Yes!<span>Your answer is correct!</span></span>
<span class="status">No!<span>Your answer is wrong!</span></span>
</div>
......@@ -91,4 +91,38 @@ describe('Tests for accessibility_tools.js', function() {
});
});
});
describe('Tests for SR region', function() {
var getSRText = function() {
return $('#reader-feedback').html();
};
beforeEach(function() {
loadFixtures('js/fixtures/sr-fixture.html');
});
it('has the sr class and is aria-live', function() {
var $reader = $('#reader-feedback');
expect($reader.hasClass('sr')).toBe(true);
expect($reader.attr('aria-live')).toBe('polite');
});
it('supports the setting of simple text', function() {
window.SR.readText('Simple Text');
expect(getSRText()).toContain('<p>Simple Text</p>');
});
it('supports the setting of an array of text', function() {
window.SR.readTexts(['One', 'Two']);
expect(getSRText()).toContain('<p>One</p>\n<p>Two</p>');
});
it('supports setting an array of elements', function() {
window.SR.readElts($('.status'));
expect(getSRText()).toContain(
'<p>Yes!<span>Your answer is correct!</span></p>\n<p>No!<span>Your answer is wrong!</span></p>'
);
});
});
});
......@@ -147,28 +147,52 @@ $(function() {
SRAlert = (function() {
function SRAlert() {
$('body').append('<div id="reader-feedback" class="sr" style="display:none" aria-hidden="false" aria-atomic="true" aria-live="assertive"></div>');
this.el = $('#reader-feedback');
// This initialization sometimes gets done twice, so take to only create a single reader-feedback div.
var readerFeedbackID = 'reader-feedback',
$readerFeedbackSelector = $('#' + readerFeedbackID);
if ($readerFeedbackSelector.length === 0) {
edx.HtmlUtils.append(
$('body'),
edx.HtmlUtils.interpolateHtml(
edx.HtmlUtils.HTML('<div id="{readerFeedbackID}" class="sr" aria-live="polite"></div>'),
{readerFeedbackID: readerFeedbackID}
)
);
}
this.el = $('#' + readerFeedbackID);
}
SRAlert.prototype.clear = function() {
return this.el.html(' ');
edx.HtmlUtils.setHtml(this.el, '');
};
SRAlert.prototype.readElts = function(elts) {
var feedback = '';
var texts = [];
$.each(elts, function(idx, value) {
return feedback += '<p>' + $(value).html() + '</p>\n';
texts.push($(value).html());
});
return this.el.html(feedback);
return this.readTexts(texts);
};
SRAlert.prototype.readText = function(text) {
return this.el.text(text);
return this.readTexts([text]);
};
SRAlert.prototype.readTexts = function(texts) {
var htmlFeedback = edx.HtmlUtils.HTML('');
$.each(texts, function(idx, value) {
htmlFeedback = edx.HtmlUtils.interpolateHtml(
edx.HtmlUtils.HTML('{previous_feedback}<p>{value}</p>\n'),
// "value" may be HTML, if an element is being passed
{previous_feedback: htmlFeedback, value: edx.HtmlUtils.HTML(value)}
);
});
edx.HtmlUtils.setHtml(this.el, htmlFeedback);
};
return SRAlert;
})();
window.SR = new SRAlert;
window.SR = new SRAlert();
});
// ------------------------------
// LMS Problem Feedback Revamp styling
// Mirror styles from the Pattern Library
@import 'base/variables';
// ----------------------------
// #GLOBALS
// ----------------------------
%btn {
display: inline-block;
border-style: $btn-border-style;
border-radius: $btn-border-radius;
border-width: $btn-border-size;
box-shadow: none;
padding: 0.625rem 1.25rem;
font-size: 16px;
font-weight: normal;
text-shadow: none;
text-transform: capitalize;
// Display: block, one button per line, full width
&.block {
display: block;
width: 100%;
}
// STATE: is disabled
&:disabled,
&.is-disabled {
@extend %state-disabled;
}
.icon {
display: inline-block;
vertical-align: baseline;
&:only-child,
.sr-only + & {
@include margin-right(0);
}
}
&.btn-small {
@extend %btn-small;
}
}
// ----------------------------
// #DEFAULT
// ----------------------------
.btn-default {
@extend %btn;
border-color: $btn-default-border-color;
background: $btn-default-background;
color: $btn-default-color;
// STATE: hover and focus
&:hover,
&.is-hovered,
&:focus,
&.is-focused {
border-color: $btn-default-focus-border-color;
background-color: $btn-default-background;
color: $btn-default-focus-color;
}
// STATE: is pressed or active
&:active,
&.is-pressed,
&.is-active {
border-color: $btn-default-active-border-color;
color: $btn-default-active-color;
}
// STATE: is disabled
&:disabled,
&.is-disabled {
border-color: $btn-disabled-border-color;
color: $btn-disabled-color;
}
}
// ----------------------------
// #BRAND
// ----------------------------
.btn-brand {
@extend %btn;
border-color: $btn-brand-border-color;
background: $btn-brand-background;
color: $btn-brand-color;
// STATE: hover and focus
&:hover,
&.is-hovered,
&:focus,
&.is-focused {
border-color: $btn-brand-focus-border-color;
background-color: $btn-brand-focus-background;
color: $btn-brand-focus-color;
}
// STATE: is pressed or active
&:active,
&.is-pressed,
&.is-active {
border-color: $btn-brand-active-border-color;
background: $btn-brand-active-background;
}
// STATE: is disabled
&:disabled,
&.is-disabled {
border-color: $btn-disabled-border-color;
background: $btn-brand-disabled-background;
color: $btn-brand-disabled-color;
}
}
// COLORS
$light-gray1: rgb(221, 221, 221);
// Font Sizes in em
$small-font-size: 0.85em !default;
$medium-font-size: 0.9em !default;
$base-font-size: 1em !default;
// Line height
$base-line-height: 1.5em !default;
$component-border-radius: 3px !default;
// grid - breakpoints
$bp-screen-sm: 480px !default;
$bp-screen-md: 768px !default;
$bp-screen-lg: 1024px !default;
$bp-screen-xl: 1280px !default;
// #SPACING
// ----------------------------
// spacing - baseline
$baseline: 20px !default;
// vertical spacing
$baseline-vertical: ($baseline*2) !default;
$spacing-vertical: (
base: $baseline-vertical,
mid-small: ($baseline-vertical*0.75),
small: ($baseline-vertical/2),
x-small: ($baseline-vertical/4),
xx-small: ($baseline-vertical/8),
xxx-small: ($baseline-vertical/10),
mid-large: ($baseline-vertical*1.5),
large: ($baseline-vertical*2),
x-large: ($baseline-vertical*4)
);
// horizontal spacing
$baseline-horizontal: $baseline !default;
$spacing-horizontal: (
base: $baseline-horizontal,
mid-small: ($baseline-horizontal*0.75),
small: ($baseline-horizontal/2),
x-small: ($baseline-horizontal/4),
xx-small: ($baseline-horizontal/8),
mid-large: ($baseline-horizontal*1.5),
large: ($baseline-horizontal*2),
x-large: ($baseline-horizontal*4)
);
// get vertical spacings from defined map values
@function spacing-vertical($key) {
@if map-has-key($spacing-vertical, $key) {
@return rem(map-get($spacing-vertical, $key));
}
@warn "Unknown `#{$key}` in $spacing-vertical.";
@return null;
}
// get horizontal spacings from defined map values
@function spacing-horizontal($key) {
@if map-has-key($spacing-horizontal, $key) {
@return rem(map-get($spacing-horizontal, $key));
}
@warn "Unknown `#{$key}` in $spacing-horizontal.";
@return null;
}
// typography: weights
$font-weights: (
normal: 400,
light: 300,
x-light: 200,
semi-bold: 600,
bold: 700
);
// typography: sizes
$font-sizes: (
xxxx-large: 38,
xxx-large: 28,
xx-large: 24,
x-large: 21,
large: 18,
base: 16,
small: 14,
x-small: 12,
xx-small: 11,
xxx-small: 10,
);
// get font sizes from defined map values
@function font-size($key) {
@if map-has-key($font-sizes, $key) {
@return rem(map-get($font-sizes, $key));
}
@warn "Unknown `#{$key}` in $font-sizes.";
@return null;
}
// get font weight from defined map values
@function font-weight($key) {
@if map-has-key($font-weights, $key) {
@return map-get($font-weights, $key);
}
@warn "Unknown `#{$key}` in $font-weights.";
@return null;
}
// visual disabled
%state-disabled {
pointer-events: none;
outline: none;
cursor: not-allowed;
}
// +Colors - UXPL new pattern library colors
// ====================
$uxpl-blue-base: rgba(0, 116, 180, 1); // wcag2a compliant
$uxpl-blue-hover-active: darken($uxpl-blue-base, 7%); // wcag2a compliant
$uxpl-green-base: rgba(0, 129, 0, 1); // wcag2a compliant
$uxpl-green-hover-active: lighten($uxpl-green-base, 7%); // wcag2a compliant
$uxpl-gray-dark: rgb(17, 17, 17);
$uxpl-gray-base: rgb(65, 65, 65);
$uxpl-gray-background: rgb(217, 217, 217);
// Alert styles
$error-color: rgb(203, 7, 18) !default;
$success-color: rgb(0, 155, 0) !default;
$warning-color: rgb(255, 192, 31) !default;
$warning-color-accent: rgb(255, 252, 221) !default;
// BUTTONS
// disabled button
$btn-disabled-border-color: #d2d0d0 !default;
$btn-disabled-color: #6b6969 !default;
// base button
$btn-default-border-color: transparent !default;
$btn-default-background: transparent !default;
$btn-default-color: $uxpl-blue-base !default;
$btn-default-focus-border-color: $uxpl-blue-base !default;
$btn-default-focus-color: $uxpl-blue-base !default;
$btn-default-active-border-color: $uxpl-blue-base !default;
$btn-default-active-color: $uxpl-blue-base !default;
// brand button
$btn-brand-border-color: $uxpl-blue-base !default;
$btn-brand-background: $uxpl-blue-base !default;
$btn-brand-color: #fcfcfc !default;
$btn-brand-focus-color: $btn-brand-color !default;
$btn-brand-focus-border-color: $uxpl-blue-hover-active !default;
$btn-brand-focus-background: $uxpl-blue-hover-active !default;
$btn-brand-active-border-color: $uxpl-blue-base !default;
$btn-brand-active-background: $uxpl-blue-base !default;
$btn-brand-disabled-background: #f2f3f3 !default;
$btn-brand-disabled-color: #676666 !default;
// ----------------------------
// #SETTINGS
// ----------------------------
$btn-border-style: solid !default;
$btn-border-size: 1px !default;
$btn-shadow: 3px !default;
$btn-font-weight: font-weight(semi-bold) !default;
$btn-border-radius: $component-border-radius !default;
// sizes
$btn-large-padding-vertical: spacing-vertical(small);
$btn-large-padding-horizontal: spacing-horizontal(mid-large);
$btn-base-padding-vertical: spacing-vertical(x-small);
$btn-base-padding-horizontal: spacing-horizontal(base);
$btn-base-font-size: font-size(base);
$btn-small-padding-vertical: spacing-vertical(x-small);
$btn-small-padding-horizontal: spacing-horizontal(small);
// ----------------------------
// #SIZES
// ----------------------------
// large
%btn-large {
padding: 1.25rem 1.875rem;
font-size: font-size(large);
}
// small
%btn-small {
padding: 0.625rem 0.625rem;
font-size: 14px;
}
// ----------------------------
// Problem Notifications
// ----------------------------
@mixin notification-by-type($color) {
border-top: 3px solid $color;
.icon {
@include margin-right(3 * $baseline/ 4);
color: $color;
}
}
......@@ -72,7 +72,7 @@ class AnnotationComponentPage(PageObject):
# Wait for the click to take effect, which is after the class is applied.
self.wait_for(lambda: 'selected' in self.q(css=answer_css).attrs('class')[0], description='answer selected')
# Click the "Check" button.
self.q(css=self.active_problem_selector('.check')).click()
self.q(css=self.active_problem_selector('.submit')).click()
# This will trigger a POST to problem_check so wait until the response is returned.
self.wait_for_ajax()
......
......@@ -2,6 +2,8 @@
Problem Page.
"""
from bok_choy.page_object import PageObject
from common.test.acceptance.pages.common.utils import click_css
from selenium.webdriver.common.keys import Keys
class ProblemPage(PageObject):
......@@ -20,6 +22,7 @@ class ProblemPage(PageObject):
"""
Return the current problem name.
"""
self.wait_for_element_visibility(self.CSS_PROBLEM_HEADER, 'wait for problem header')
return self.q(css='.problem-header').text[0]
@property
......@@ -48,14 +51,15 @@ class ProblemPage(PageObject):
"""
Return the "hint" text of the problem from html
"""
return self.q(css="div.problem div.problem-hint").html[0].split(' <', 1)[0]
hints_html = self.q(css="div.problem .notification-hint .notification-message li").html
return [hint_html.split(' <span', 1)[0] for hint_html in hints_html]
@property
def hint_text(self):
"""
Return the "hint" text of the problem from its div.
"""
return self.q(css="div.problem div.problem-hint").text[0]
return self.q(css="div.problem .notification-hint .notification-message").text[0]
def verify_mathjax_rendered_in_problem(self):
"""
......@@ -108,32 +112,113 @@ class ProblemPage(PageObject):
self.wait_for_element_invisibility('.loading', 'wait for loading icon to disappear')
self.wait_for_ajax()
def click_check(self):
def click_submit(self):
"""
Click the Check button.
Click the Submit button.
"""
self.q(css='div.problem button.check').click()
self.wait_for_ajax()
click_css(self, '.problem .submit', require_notification=False)
def click_save(self):
"""
Click the Save button.
"""
self.q(css='div.problem button.save').click()
self.wait_for_ajax()
click_css(self, '.problem .save', require_notification=False)
def click_reset(self):
"""
Click the Reset button.
"""
self.q(css='div.problem button.reset').click()
self.wait_for_ajax()
click_css(self, '.problem .reset', require_notification=False)
def click_show_hide_button(self):
""" Click the Show/Hide button. """
self.q(css='div.problem div.action .show').click()
def click_show(self):
"""
Click the Show Answer button.
"""
self.q(css='.problem .show').click()
self.wait_for_ajax()
def is_hint_notification_visible(self):
"""
Is the Hint Notification visible?
"""
return self.q(css='.notification.notification-hint').visible
def is_save_notification_visible(self):
"""
Is the Save Notification Visible?
"""
return self.q(css='.notification.warning.notification-save').visible
def is_success_notification_visible(self):
"""
Is the Submit Notification Visible?
"""
return self.q(css='.notification.success.notification-submit').visible
def wait_for_save_notification(self):
"""
Wait for the Save Notification to be present
"""
self.wait_for_element_visibility('.notification.warning.notification-save',
'Waiting for Save notification to be visible')
self.wait_for(lambda: self.q(css='.notification.warning.notification-save').focused,
'Waiting for the focus to be on the save notification')
def wait_for_gentle_alert_notification(self):
"""
Wait for the Gentle Alert Notification to be present
"""
self.wait_for_element_visibility('.notification.warning.notification-gentle-alert',
'Waiting for Gentle Alert notification to be visible')
self.wait_for(lambda: self.q(css='.notification.warning.notification-gentle-alert').focused,
'Waiting for the focus to be on the gentle alert notification')
def is_gentle_alert_notification_visible(self):
"""
Is the Gentle Alert Notification visible?
"""
return self.q(css='.notification.warning.notification-gentle-alert').visible
def is_reset_button_present(self):
""" Check for the presence of the reset button. """
return self.q(css='.problem .reset').present
def is_save_button_enabled(self):
""" Is the Save button enabled """
return self.q(css='.action .save').attrs('disabled') == [None]
def is_focus_on_problem_meta(self):
"""
Check for focus problem meta.
"""
return self.q(css='.problem-header').focused
def is_submit_disabled(self):
"""
Checks if the submit button is disabled
"""
disabled_attr = self.q(css='.problem .submit').attrs('disabled')[0]
return disabled_attr == 'true'
def wait_for_submit_disabled(self):
"""
Waits until the Submit button becomes disabled.
"""
self.wait_for(self.is_submit_disabled, 'Waiting for submit to be enabled')
def wait_for_focus_on_submit_notification(self):
"""
Check for focus submit notification.
"""
def focus_check():
"""
Checks whether or not the focus is on the notification-submit
"""
return self.q(css='.notification-submit').focused
self.wait_for(promise_check_func=focus_check, description='Waiting for the notification-submit to gain focus')
def wait_for_status_icon(self):
"""
wait for status icon
......@@ -151,12 +236,67 @@ class ProblemPage(PageObject):
msg = "Wait for status to be {}".format(message)
self.wait_for_element_visibility(status_selector, msg)
def wait_success_notification(self):
"""
Check for visibility of the success notification and icon.
"""
msg = "Wait for success notification to be visible"
self.wait_for_element_visibility('.notification.success.notification-submit', msg)
self.wait_for_element_visibility('.fa-check', "Waiting for success icon")
self.wait_for_focus_on_submit_notification()
def wait_incorrect_notification(self):
"""
Check for visibility of the incorrect notification and icon.
"""
msg = "Wait for error notification to be visible"
self.wait_for_element_visibility('.notification.error.notification-submit', msg)
self.wait_for_element_visibility('.fa-close', "Waiting for incorrect notification icon")
self.wait_for_focus_on_submit_notification()
def wait_partial_notification(self):
"""
Check for visibility of the partially visible notification and icon.
"""
msg = "Wait for partial correct notification to be visible"
self.wait_for_element_visibility('.notification.success.notification-submit', msg)
self.wait_for_element_visibility('.fa-asterisk', "Waiting for asterisk notification icon")
self.wait_for_focus_on_submit_notification()
def click_hint(self):
"""
Click the Hint button.
"""
self.q(css='div.problem button.hint-button').click()
self.wait_for_ajax()
click_css(self, '.problem .hint-button', require_notification=False)
self.wait_for_focus_on_hint_notification()
def wait_for_focus_on_hint_notification(self):
"""
Wait for focus to be on the hint notification.
"""
self.wait_for(
lambda: self.q(css='.notification-hint').focused,
'Waiting for the focus to be on the hint notification'
)
def click_review_in_notification(self):
"""
Click on the "Review" button within the visible notification.
"""
# The review button cannot be clicked on until it is tabbed to, so first tab to it.
# Multiple tabs may be required depending on the content (for instance, hints with links).
def tab_until_review_focused():
""" Tab until the review button is focused """
self.browser.switch_to_active_element().send_keys(Keys.TAB)
return self.q(css='.notification .review-btn').focused
self.wait_for(tab_until_review_focused, 'Waiting for the Review button to become focused')
click_css(self, '.notification .review-btn', require_notification=False)
def get_hint_button_disabled_attr(self):
""" Return the disabled attribute of all hint buttons (once hints are visible, there will be two). """
return self.q(css='.problem .hint-button').attrs('disabled')
def click_choice(self, choice_value):
"""
......@@ -235,3 +375,11 @@ class ProblemPage(PageObject):
Return a list of question descriptions of the problem.
"""
return self.q(css="div.problem .wrapper-problem-response .question-description").text
@property
def problem_progress_graded_value(self):
"""
Return problem progress text which contains weight of problem, if it is graded, and the student's current score.
"""
self.wait_for_element_visibility('.problem-progress', "Problem progress is visible")
return self.q(css='.problem-progress').text[0]
......@@ -215,7 +215,6 @@ class AdvancedSettingsPage(CoursePage):
'show_reset_button',
'static_asset_path',
'teams_configuration',
'text_customization',
'annotation_storage_url',
'social_sharing_url',
'video_bumper',
......
......@@ -26,7 +26,7 @@ class CrowdsourcehinterProblemPage(PageObject):
Submit an answer to the problem block
"""
self.q(css='input[type="text"]').fill(text)
self.q(css='.action [data-value="Check"]').click()
self.q(css='.action [data-value="Submit"]').click()
self.wait_for_ajax()
def get_hint_text(self):
......
......@@ -6,8 +6,8 @@
<optionresponse>
<optioninput options="('yellow','blue','green')" correct="blue"/>
</optionresponse>
<p>Which piece of furniture is built for sitting?</p>
<multiplechoiceresponse>
<label>Which piece of furniture is built for sitting?</label>
<choicegroup type="MultipleChoice">
<choice correct="false">a table</choice>
<choice correct="false">a desk</choice>
......@@ -15,8 +15,8 @@
<choice correct="false">a bookshelf</choice>
</choicegroup>
</multiplechoiceresponse>
<p>Which of the following are musical instruments?</p>
<choiceresponse>
<label>Which of the following are musical instruments?</label>
<checkboxgroup>
<choice correct="true">a piano</choice>
<choice correct="false">a tree</choice>
......
......@@ -218,14 +218,14 @@ class CertificateProgressPageTest(UniqueCourseTest):
self.course_nav.q(css='select option[value="{}"]'.format('blue')).first.click()
# Select correct radio button for the answer
self.course_nav.q(css='fieldset div.field:nth-child(3) input').nth(0).click()
self.course_nav.q(css='fieldset div.field:nth-child(4) input').nth(0).click()
# Select correct radio buttons for the answer
self.course_nav.q(css='fieldset div.field:nth-child(1) input').nth(1).click()
self.course_nav.q(css='fieldset div.field:nth-child(3) input').nth(1).click()
self.course_nav.q(css='fieldset div.field:nth-child(2) input').nth(1).click()
self.course_nav.q(css='fieldset div.field:nth-child(4) input').nth(1).click()
# Submit the answer
self.course_nav.q(css='button.check.Check').click()
self.course_nav.q(css='button.submit').click()
self.course_nav.wait_for_ajax()
# Navigate to the 'Test Subsection 2' of 'Test Section 2'
......@@ -238,5 +238,5 @@ class CertificateProgressPageTest(UniqueCourseTest):
self.course_nav.q(css='input[id^=input_][id$=_2_1]').fill('A*x^2 + sqrt(y)')
# Submit the answer
self.course_nav.q(css='button.check.Check').click()
self.course_nav.q(css='button.submit').click()
self.course_nav.wait_for_ajax()
......@@ -109,7 +109,7 @@ class ConditionalTest(UniqueCourseTest):
# Answer the problem
problem_page = ProblemPage(self.browser)
problem_page.fill_answer('correct string')
problem_page.click_check()
problem_page.click_submit()
# The conditional does not update on its own, so we need to reload the page.
self.courseware_page.visit()
# Verify that we can see the content.
......
......@@ -1085,12 +1085,12 @@ class ProblemExecutionTest(UniqueCourseTest):
# Fill in the answer correctly.
problem_page.fill_answer("20")
problem_page.click_check()
problem_page.click_submit()
self.assertTrue(problem_page.is_correct())
# Fill in the answer incorrectly.
problem_page.fill_answer("4")
problem_page.click_check()
problem_page.click_submit()
self.assertFalse(problem_page.is_correct())
......
......@@ -714,12 +714,12 @@ class ProblemStateOnNavigationTest(UniqueCourseTest):
)
self.assertEqual(self.problem_page.problem_name, problem_name)
def test_perform_problem_check_and_navigate(self):
def test_perform_problem_submit_and_navigate(self):
"""
Scenario:
I go to sequential position 1
Facing problem1, I select 'choice_1'
Then I click check button
Then I click submit button
Then I go to sequential position 2
Then I came back to sequential position 1 again
Facing problem1, I observe the problem1 content is not
......@@ -730,7 +730,7 @@ class ProblemStateOnNavigationTest(UniqueCourseTest):
# Update problem 1's content state by clicking check button.
self.problem_page.click_choice('choice_choice_1')
self.problem_page.click_check()
self.problem_page.click_submit()
self.problem_page.wait_for_expected_status('label.choicegroup_incorrect', 'incorrect')
# Save problem 1's content state as we're about to switch units in the sequence.
......@@ -761,7 +761,7 @@ class ProblemStateOnNavigationTest(UniqueCourseTest):
# Update problem 1's content state by clicking save button.
self.problem_page.click_choice('choice_choice_1')
self.problem_page.click_save()
self.problem_page.wait_for_expected_status('div.capa_alert', 'saved')
self.problem_page.wait_for_save_notification()
# Save problem 1's content state as we're about to switch units in the sequence.
problem1_content_before_switch = self.problem_page.problem_content
......@@ -790,7 +790,7 @@ class ProblemStateOnNavigationTest(UniqueCourseTest):
# Update problem 1's content state – by performing reset operation.
self.problem_page.click_choice('choice_choice_1')
self.problem_page.click_check()
self.problem_page.click_submit()
self.problem_page.wait_for_expected_status('label.choicegroup_incorrect', 'incorrect')
self.problem_page.click_reset()
self.problem_page.wait_for_expected_status('span.unanswered', 'unanswered')
......
......@@ -99,7 +99,7 @@ class EntranceExamPassTest(EntranceExamTest):
self.assertTrue(self.courseware_page.has_entrance_exam_message())
self.assertFalse(self.courseware_page.has_passed_message())
problem_page.click_choice('choice_1')
problem_page.click_check()
problem_page.click_submit()
self.courseware_page.wait_for_page()
self.assertTrue(self.courseware_page.has_passed_message())
self.assertEqual(self.courseware_page.chapter_count_in_navigation, 2)
......@@ -114,7 +114,7 @@ class GatingTest(UniqueCourseTest):
problem_page = ProblemPage(self.browser)
self.assertEqual(problem_page.wait_for_page().problem_name, 'HEIGHT OF EIFFEL TOWER')
problem_page.click_choice('choice_1')
problem_page.click_check()
problem_page.click_submit()
def test_subsection_gating_in_studio(self):
"""
......
......@@ -234,7 +234,6 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
'for user {}'.format(self.USERNAME), msg)
@attr(shard=3)
class CourseWithContentGroupsTest(StaffViewTest):
"""
Verifies that changing the "View this course as" selector works properly for content groups.
......@@ -265,8 +264,8 @@ class CourseWithContentGroupsTest(StaffViewTest):
"""
problem_data = dedent("""
<problem markdown="Simple Problem" max_attempts="" weight="">
<p>Choose Yes.</p>
<choiceresponse>
<label>Choose Yes.</label>
<checkboxgroup>
<choice correct="true">Yes</choice>
</checkboxgroup>
......@@ -294,6 +293,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
)
)
@attr(shard=3)
def test_staff_sees_all_problems(self):
"""
Scenario: Staff see all problems
......@@ -305,6 +305,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
course_page = self._goto_staff_page()
verify_expected_problem_visibility(self, course_page, [self.alpha_text, self.beta_text, self.everyone_text])
@attr(shard=3)
def test_student_not_in_content_group(self):
"""
Scenario: When previewing as a student, only content visible to all is shown
......@@ -318,6 +319,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
course_page.set_staff_view_mode('Student')
verify_expected_problem_visibility(self, course_page, [self.everyone_text])
@attr(shard=3)
def test_as_student_in_alpha(self):
"""
Scenario: When previewing as a student in group alpha, only content visible to alpha is shown
......@@ -331,6 +333,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
course_page.set_staff_view_mode('Student in alpha')
verify_expected_problem_visibility(self, course_page, [self.alpha_text, self.everyone_text])
@attr(shard=3)
def test_as_student_in_beta(self):
"""
Scenario: When previewing as a student in group beta, only content visible to beta is shown
......@@ -366,6 +369,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
add_cohort_with_student("Cohort Beta", "beta", student_b_username)
cohort_management_page.wait_for_ajax()
@attr(shard=3)
def test_as_specific_student(self):
student_a_username = 'tass_student_a'
student_b_username = 'tass_student_b'
......
......@@ -76,7 +76,7 @@ class ProgressPageBaseTest(UniqueCourseTest):
"""
self.courseware_page.go_to_sequential_position(1)
self.problem_page.click_choice('choice_choice_2')
self.problem_page.click_check()
self.problem_page.click_submit()
def _get_section_score(self):
"""
......
......@@ -74,7 +74,7 @@ def answer_problem_step(step, problem_type, correctness):
input_problem_answer(step, problem_type, correctness)
# Submit the problem
check_problem(step)
submit_problem(step)
@step(u'I input an answer on a "([^"]*)" problem "([^"]*)ly"')
......@@ -87,26 +87,18 @@ def input_problem_answer(_, problem_type, correctness):
answer_problem(world.scenario_dict['COURSE'].number, problem_type, correctness)
@step(u'I check a problem')
def check_problem(step):
@step(u'I submit a problem')
# pylint: disable=unused-argument
def submit_problem(step):
# first scroll down so the loading mathjax button does not
# cover up the Check button
# cover up the Submit button
world.browser.execute_script("window.scrollTo(0,1024)")
assert world.is_css_not_present("button.check.is-disabled")
world.css_click("button.check")
world.css_click("button.submit")
# Wait for the problem to finish re-rendering
world.wait_for_ajax_complete()
@step(u"I can't check a problem")
def assert_cant_check_problem(step): # pylint: disable=unused-argument
# first scroll down so the loading mathjax button does not
# cover up the Check button
world.browser.execute_script("window.scrollTo(0,1024)")
assert world.is_css_present("button.check.is-disabled")
@step(u'The "([^"]*)" problem displays a "([^"]*)" answer')
def assert_problem_has_answer(step, problem_type, answer_class):
'''
......@@ -147,21 +139,13 @@ def action_button_present(_step, buttonname, doesnt_appear):
assert world.is_css_present(button_css)
@step(u'the Show/Hide button label is "([^"]*)"$')
def show_hide_label_is(_step, label_name):
# The label text is changed by static/xmodule_js/src/capa/display.js
# so give it some time to change on the page.
label_css = 'button.show span.show-label'
world.wait_for(lambda _: world.css_has_text(label_css, label_name))
@step(u'I should see a score of "([^"]*)"$')
def see_score(_step, score):
# The problem progress is changed by
# cms/static/xmodule_js/src/capa/display.js
# so give it some time to render on the page.
score_css = 'div.problem-progress'
expected_text = '({})'.format(score)
expected_text = '{}'.format(score)
world.wait_for(lambda _: world.css_has_text(score_css, expected_text))
......
......@@ -295,27 +295,27 @@ def problem_has_answer(course, problem_type, answer_class):
elif problem_type == "multiple choice":
if answer_class == 'correct':
assert_checked(course, 'multiple choice', ['choice_2'])
assert_submitted(course, 'multiple choice', ['choice_2'])
elif answer_class == 'incorrect':
assert_checked(course, 'multiple choice', ['choice_1'])
assert_submitted(course, 'multiple choice', ['choice_1'])
else:
assert_checked(course, 'multiple choice', [])
assert_submitted(course, 'multiple choice', [])
elif problem_type == "checkbox":
if answer_class == 'correct':
assert_checked(course, 'checkbox', ['choice_0', 'choice_2'])
assert_submitted(course, 'checkbox', ['choice_0', 'choice_2'])
elif answer_class == 'incorrect':
assert_checked(course, 'checkbox', ['choice_3'])
assert_submitted(course, 'checkbox', ['choice_3'])
else:
assert_checked(course, 'checkbox', [])
assert_submitted(course, 'checkbox', [])
elif problem_type == "radio":
if answer_class == 'correct':
assert_checked(course, 'radio', ['choice_2'])
assert_submitted(course, 'radio', ['choice_2'])
elif answer_class == 'incorrect':
assert_checked(course, 'radio', ['choice_1'])
assert_submitted(course, 'radio', ['choice_1'])
else:
assert_checked(course, 'radio', [])
assert_submitted(course, 'radio', [])
elif problem_type == 'string':
if answer_class == 'blank':
......@@ -410,23 +410,23 @@ def inputfield(course, problem_type, choice=None, input_num=1):
return sel
def assert_checked(course, problem_type, choices):
def assert_submitted(course, problem_type, choices):
'''
Assert that choice names given in *choices* are the only
ones checked.
ones submitted.
Works for both radio and checkbox problems
'''
all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3']
for this_choice in all_choices:
def check_problem():
def submit_problem():
element = world.css_find(inputfield(course, problem_type, choice=this_choice))
if this_choice in choices:
assert element.checked
else:
assert not element.checked
world.retry_on_exception(check_problem)
world.retry_on_exception(submit_problem)
def assert_textfield(course, problem_type, expected_text, input_num=1):
......
......@@ -90,3 +90,6 @@
// overrides
@import 'developer'; // used for any developer-created scss that needs further polish/refactoring
@import 'shame'; // used for any bad-form/orphaned scss
// CAPA Problem Feedback
@import 'edx-pattern-library-shims/buttons';
......@@ -32,9 +32,8 @@ $headings-base-color: $gray-d2;
%hd-2 {
margin-bottom: 1em;
font-size: 1.5em;
font-weight: $headings-font-weight-normal;
font-size: em(18);
font-weight: $headings-font-weight-bold;
line-height: 1.4em;
}
......@@ -118,7 +117,7 @@ $headings-base-color: $gray-d2;
h3 {
@extend %hd-2;
font-weight: $headings-font-weight-normal;
font-weight: $headings-font-weight-bold;
// override external modules and xblocks that use inline CSS
text-transform: initial;
......
../../../common/static/sass/edx-pattern-library-shims
\ No newline at end of file
......@@ -547,7 +547,6 @@
margin: 0 auto;
width: flex-grid(12);
max-width: $fg-max-width;
min-width: $fg-min-width;
strong {
@extend %t-strong;
......
......@@ -211,14 +211,13 @@ $shadow-d1: rgba(0,0,0,0.4) !default;
$shadow-d2: rgba($black, 0.6) !default;
// system feedback-based colors
$error-color: rgb(253, 87, 87) !default;
$warning-color: rgb(181,42,103) !default;
$error-color: rgb(203, 7, 18) !default;
$warning-color: rgb(255, 192, 31) !default;
$confirm-color: rgb(0, 132, 1) !default;
$active-color: $blue !default;
$highlight-color: rgb(255,255,0) !default;
$alert-color: rgb(212, 64, 64) !default;
$warning-color: rgb(237, 189, 60) !default;
$success-color: rgb(37, 184, 90) !default;
$success-color: rgb(0, 155, 0) !default;
// ----------------------------
......
......@@ -8,7 +8,7 @@
%>
<div class='exam-text'>
<%= interpolate_text('You are taking "{exam_link}" as a {exam_type} exam. The timer on the right shows the time remaining in the exam.', {exam_link: "<a href='" + exam_url_path + "'>"+gtLtEscape(exam_display_name)+"</a>", exam_type: (!_.isUndefined(arguments[0].exam_type)) ? exam_type : gettext('timed')}) %>
<%- gettext('To receive credit on a problem, you must click "Check" or "Final Check" on it before you select "End My Exam".') %>
<%- gettext('To receive credit for problems, you must select "Submit" for each problem before you select "End My Exam".') %>
</div>
<div id="turn_in_exam_id" class="pull-right turn_in_exam">
<span>
......
<h1> ${ homework['name']} Test </h1>
<ol>
% for problem in homework['problems']:
<li>
<h2>${ problem['name'] }</h2>
${ problem['html'] }
<section>
<input type="hidden" name="problem_id" value="${ problem['name'] }">
<input type="submit" value="Check">
</section>
</li>
% endfor
</ol>
......@@ -5,38 +5,97 @@ from openedx.core.djangolib.markup import HTML
%>
<%namespace name='static' file='static_content.html'/>
<h3 class="hd hd-2 problem-header">
<h3 class="hd hd-2 problem-header" id="${ short_id }-problem-title" aria-describedby="${ id }-problem-progress" tabindex="-1">
${ problem['name'] }
</h3>
<div class="problem-progress"></div>
<div class="problem-progress" id="${ id }-problem-progress"></div>
<div class="problem">
${ HTML(problem['html']) }
<div class="action">
<input type="hidden" name="problem_id" value="${ problem['name'] }" />
% if demand_hint_possible:
<div class="problem-hint" aria-live="polite"></div>
% endif
% if check_button:
<button class="check ${ check_button }" data-checking="${ check_button_checking }" data-value="${ check_button }"><span class="check-label">${ check_button }</span><span class="sr"> ${_("your answer")}</span></button>
<div class="problem-hint">
<%include file="problem_notifications.html" args="
notification_name='hint',
notification_type='problem-hint',
notification_icon='fa-question',
notification_message=''"
/>
</div>
% endif
<div class="problem-action-buttons-wrapper">
% if demand_hint_possible:
<button class="hint-button" data-value="${_('Hint')}">${_('Hint')}</button>
% endif
% if reset_button:
<button class="reset" data-value="${_('Reset')}">${_('Reset')}<span class="sr"> ${_("your answer")}</span></button>
<span class="problem-action-button-wrapper">
<button type="button" class="hint-button problem-action-btn btn-default btn-small" data-value="${_('Hint')}" ${'' if should_enable_next_hint else 'disabled'}><span class="icon fa fa-question" aria-hidden="true"></span>${_('Hint')}</button>
</span>
% endif
% if save_button:
<button class="save" data-value="${_('Save')}">${_('Save')}<span class="sr"> ${_("your answer")}</span></button>
<span class="problem-action-button-wrapper">
<button type="button" class="save problem-action-btn btn-default btn-small" data-value="${_('Save')}">
<span class="icon fa fa-floppy-o" aria-hidden="true"></span>
<span aria-hidden="true">${_('Save')}</span>
<span class="sr">${_("Save your answer")}</span>
</button>
</span>
% endif
% if reset_button:
<span class="problem-action-button-wrapper">
<button type="button" class="reset problem-action-btn btn-default btn-small" data-value="${_('Reset')}"><span class="icon fa fa-refresh" aria-hidden="true"></span><span aria-hidden="true">${_('Reset')}</span><span class="sr">${_("Reset your answer")}</span></button>
</span>
% endif
% if answer_available:
<button class="show"><span class='sr'>${_('Toggle Answer Visibility')}</span><span class="show-label">${_('Show Answer')}</span></button>
<span class="problem-action-button-wrapper">
<button type="button" class="show problem-action-btn btn-default btn-small" aria-describedby="${ short_id }-problem-title"><span class="icon fa fa-info-circle" aria-hidden="true"></span><span class="show-label">${_('Show Answer')}</span></button>
</span>
% endif
% if attempts_allowed :
<div class="submission_feedback" aria-live="polite">
${_("You have used {num_used} of {num_total} submissions").format(num_used=attempts_used, num_total=attempts_allowed)}
</div>
<button type="button" class="submit btn-brand" data-submitting="${ submit_button_submitting }" data-value="${ submit_button }" data-should-enable-submit-button="${ should_enable_submit_button }" aria-describedby="submission_feedback_${short_id}" ${'' if should_enable_submit_button else 'disabled'}>
<span class="submit-label" aria-hidden="true">${ submit_button }</span><span class="sr">${_("Submit your answer")}</span>
</button>
<div class="submission_feedback" id="submission_feedback_${short_id}">
% if attempts_allowed:
${_("You have used {num_used} of {num_total} attempts").format(num_used=attempts_used, num_total=attempts_allowed)}
% endif
</div>
</div>
<%include file="problem_notifications.html" args="
notification_type='warning',
notification_icon='fa-exclamation-circle',
notification_name='gentle-alert',
notification_message=''"
/>
% if answer_notification_type:
% if 'correct' == answer_notification_type:
<%include file="problem_notifications.html" args="
notification_type='success',
notification_icon='fa-check',
notification_name='submit',
notification_message=answer_notification_message"
/>
% endif
% if 'incorrect' == answer_notification_type:
<%include file="problem_notifications.html" args="
notification_type='error',
notification_icon='fa-close',
notification_name='submit',
notification_message=answer_notification_message"
/>
% endif
% if 'partially-correct' == answer_notification_type:
<%include file="problem_notifications.html" args="
notification_type='success',
notification_icon='fa-asterisk',
notification_name='submit',
notification_message=answer_notification_message"
/>
% endif
% endif
<%include file="problem_notifications.html" args="
notification_type='warning',
notification_icon='fa-save',
notification_name='save',
notification_message=''"
/>
</div>
<div id="problem_${element_id}" class="problems-wrapper" data-problem-id="${id}" data-url="${ajax_url}" data-progress_status="${progress_status}" data-progress_detail="${progress_detail}" data-content="${content | h}"></div>
<div id="problem_${element_id}" class="problems-wrapper" data-problem-id="${id}" data-url="${ajax_url}" data-progress_status="${progress_status}" data-progress_detail="${progress_detail}" data-content="${content | h}" data-graded="${graded}"></div>
<%page expression_filter="h" args="notification_name, notification_type, notification_icon,
notification_message, should_enable_next_hint"/>
<%! from django.utils.translation import ugettext as _ %>
<div class="notification ${notification_type} ${'notification-'}${notification_name}
${'' if notification_name == 'submit' else 'is-hidden' }"
tabindex="-1">
<span class="icon fa ${notification_icon}" aria-hidden="true"></span>
<span class="notification-message" aria-describedby="${ id }-problem-title">${notification_message}
</span>
<div class="notification-btn-wrapper">
% if notification_name is 'hint':
<button type="button" class="btn btn-default btn-small notification-btn hint-button">
${_('Next Hint')}
</button>
% endif
<button type="button" class="btn btn-default btn-small notification-btn review-btn sr">${_('Review')}</button>
</div>
</div>
......@@ -53,7 +53,7 @@ git+https://github.com/edx/MongoDBProxy.git@25b99097615bda06bd7cdfe5669ed80dc2a7
git+https://github.com/edx/nltk.git@2.0.6#egg=nltk==2.0.6
-e git+https://github.com/dementrock/pystache_custom.git@776973740bdaad83a3b029f96e415a7d1e8bec2f#egg=pystache_custom-dev
-e git+https://github.com/appliedsec/pygeoip.git@95e69341cebf5a6a9fbf7c4f5439d458898bdc3b#egg=pygeoip
-e git+https://github.com/jazkarta/edx-jsme.git@c5bfa5d361d6685d8c643838fc0055c25f8b7999#egg=edx-jsme
-e git+https://github.com/jazkarta/edx-jsme.git@0908b4db16168382be5685e7e9b7b4747ac410e0#egg=edx-jsme
git+https://github.com/edx/django-pyfs.git@1.0.3#egg=django-pyfs==1.0.3
git+https://github.com/mitodl/django-cas.git@v2.1.1#egg=django-cas
-e git+https://github.com/dgrtwo/ParsePy.git@7949b9f754d1445eff8e8f20d0e967b9a6420639#egg=parse_rest
......
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