Commit c3b6b104 by muhammad-ammar Committed by cahrens

Problem (capa) feedback UX revamp.

TNL-4877
parent 432f6cdf
...@@ -87,3 +87,7 @@ ...@@ -87,3 +87,7 @@
// +CodeMirror Overrides // +CodeMirror Overrides
// ==================== // ====================
@import 'elements/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 @@ ...@@ -399,7 +399,6 @@
margin: 0 auto; margin: 0 auto;
width: flex-grid(12); width: flex-grid(12);
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width;
strong { strong {
@extend %t-strong; @extend %t-strong;
......
...@@ -248,15 +248,7 @@ ...@@ -248,15 +248,7 @@
color: $color-visibility-set; 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 // +Messaging - Xblocks
......
...@@ -32,9 +32,8 @@ $headings-base-color: $gray-d2; ...@@ -32,9 +32,8 @@ $headings-base-color: $gray-d2;
%hd-2 { %hd-2 {
margin-bottom: 1em; font-size: 1.1125em;
font-size: 1.5em; font-weight: $headings-font-weight-bold;
font-weight: $headings-font-weight-normal;
line-height: 1.4em; line-height: 1.4em;
} }
......
...@@ -376,7 +376,7 @@ class LoncapaProblem(object): ...@@ -376,7 +376,7 @@ class LoncapaProblem(object):
def grade_answers(self, answers): 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 `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 "_"). of each key removed (the string before the first "_").
...@@ -496,6 +496,7 @@ class LoncapaProblem(object): ...@@ -496,6 +496,7 @@ class LoncapaProblem(object):
choice-level explanations shown to a student after submission. choice-level explanations shown to a student after submission.
Does nothing if there is no targeted-feedback attribute. 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. # Note that the modifications has been done, avoiding problems if called twice.
if hasattr(self, 'has_targeted'): if hasattr(self, 'has_targeted'):
return return
...@@ -515,9 +516,12 @@ class LoncapaProblem(object): ...@@ -515,9 +516,12 @@ class LoncapaProblem(object):
# Keep track of the explanation-id that corresponds to the student's answer # Keep track of the explanation-id that corresponds to the student's answer
# Also, keep track of the solution-id # Also, keep track of the solution-id
solution_id = None solution_id = None
choice_correctness_for_student_answer = _('Incorrect')
for choice in choices_list: for choice in choices_list:
if choice.get('name') == student_answer: if choice.get('name') == student_answer:
expl_id_for_student_answer = choice.get('explanation-id') 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': if choice.get('correct') == 'true':
solution_id = choice.get('explanation-id') solution_id = choice.get('explanation-id')
...@@ -527,7 +531,15 @@ class LoncapaProblem(object): ...@@ -527,7 +531,15 @@ class LoncapaProblem(object):
if len(targetedfeedbackset) != 0: if len(targetedfeedbackset) != 0:
targetedfeedbackset = targetedfeedbackset[0] targetedfeedbackset = targetedfeedbackset[0]
targetedfeedbacks = targetedfeedbackset.xpath('./targetedfeedback') 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: 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 # 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 # 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: if not self.done or targetedfeedback.get('explanation-id') != expl_id_for_student_answer:
...@@ -561,6 +573,7 @@ class LoncapaProblem(object): ...@@ -561,6 +573,7 @@ class LoncapaProblem(object):
# Add our solution instead to the targetedfeedbackset and change its tag name # Add our solution instead to the targetedfeedbackset and change its tag name
solution_element.tag = 'targetedfeedback' solution_element.tag = 'targetedfeedback'
targetedfeedbackset.append(solution_element) targetedfeedbackset.append(solution_element)
def get_html(self): def get_html(self):
......
...@@ -51,6 +51,7 @@ from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautifu ...@@ -51,6 +51,7 @@ from lxml.html.soupparser import fromstring as fromstring_bs # uses Beautifu
import capa.xqueue_interface as xqueue_interface import capa.xqueue_interface as xqueue_interface
import capa.safe_exec as safe_exec import capa.safe_exec as safe_exec
from openedx.core.djangolib.markup import HTML, Text
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
...@@ -352,9 +353,9 @@ class LoncapaResponse(object): ...@@ -352,9 +353,9 @@ class LoncapaResponse(object):
# Tricky: label None means output defaults, while '' means output empty label # Tricky: label None means output defaults, while '' means output empty label
if label is None: if label is None:
if correct: if correct:
label = _(u'Correct') label = _(u'Correct:')
else: else:
label = _(u'Incorrect') label = _(u'Incorrect:')
# self.runtime.track_function('get_demand_hint', event_info) # self.runtime.track_function('get_demand_hint', event_info)
# This this "feedback hint" event # This this "feedback hint" event
...@@ -372,15 +373,23 @@ class LoncapaResponse(object): ...@@ -372,15 +373,23 @@ class LoncapaResponse(object):
self.capa_module.runtime.track_function('edx.problem.hint.feedback_displayed', event_info) self.capa_module.runtime.track_function('edx.problem.hint.feedback_displayed', event_info)
# Form the div-wrapped hint texts # Form the div-wrapped hint texts
hints_wrap = u''.join( hints_wrap = HTML('').join(
[u'<div class="{0}">{1}</div>'.format(QUESTION_HINT_TEXT_STYLE, dct.get('text')) [HTML('<div class="{question_hint_text_style}">{hint_content}</div>').format(
for dct in hint_log] question_hint_text_style=QUESTION_HINT_TEXT_STYLE,
hint_content=HTML(dct.get('text'))
) for dct in hint_log]
) )
if multiline_mode: 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 = '' label_wrap = ''
if label: 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 # Establish the outer style
if correct: if correct:
...@@ -389,7 +398,12 @@ class LoncapaResponse(object): ...@@ -389,7 +398,12 @@ class LoncapaResponse(object):
style = QUESTION_HINT_INCORRECT_STYLE style = QUESTION_HINT_INCORRECT_STYLE
# Ready to go # 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): def get_extended_hints(self, student_answers, new_cmap):
""" """
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
<div class="block">${comment_prompt}</div> <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> <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"> <ul class="tags">
% for option in options: % for option in options:
<li> <li>
...@@ -53,12 +53,12 @@ ...@@ -53,12 +53,12 @@
<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="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> <p id="answer_${id}" class="answer answer-annotation"></p>
</div> </div>
</form> </form>
% if msg: % if msg:
<span class="message">${HTML(msg)}</span> <span class="message" aria-describedby="label_${id}" tabindex="-1">${HTML(msg)}</span>
% endif % endif
...@@ -11,8 +11,8 @@ ...@@ -11,8 +11,8 @@
/> />
<p class="status" aria-describedby="input_${id}"> <p class="status" aria-describedby="input_${id}">
${value|h} - ${value|h}
${status.display_name} <span class="sr">${status.display_name}</span>
</p> </p>
<div id="input_${id}_preview" class="equation"></div> <div id="input_${id}_preview" class="equation"></div>
......
...@@ -15,7 +15,7 @@ ...@@ -15,7 +15,7 @@
<p class="question-description" id="${description_id}">${description_text}</p> <p class="question-description" id="${description_id}">${description_text}</p>
% endfor % endfor
% for choice_id, choice_label in choices: % 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' label_class = 'response-label field-label label-inline'
%> %>
...@@ -60,7 +60,7 @@ ...@@ -60,7 +60,7 @@
</fieldset> </fieldset>
<div class="indicator-container"> <div class="indicator-container">
% if input_type == 'checkbox' or not value: % 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 class="sr">${status.display_tooltip}</span>
</span> </span>
% endif % endif
...@@ -69,6 +69,6 @@ ...@@ -69,6 +69,6 @@
<div class="capa_alert">${submitted_message}</div> <div class="capa_alert">${submitted_message}</div>
%endif %endif
% if msg: % if msg:
<span class="message">${HTML(msg)}</span> <span class="message" aria-describedby="${id}-legend" tabindex="-1">${HTML(msg)}</span>
% endif % endif
</form> </form>
<%! from capa.util import remove_markup %> <%! from capa.util import remove_markup
<%! from django.utils.translation import ugettext as _ %> from django.utils.translation import ugettext as _
from openedx.core.djangolib.markup import HTML
%>
<% element_checked = False %> <% element_checked = False %>
% for choice_id, _ in choices: % for choice_id, _ in choices:
<% choice_id = choice_id %> <% choice_id = choice_id %>
...@@ -63,7 +66,9 @@ ...@@ -63,7 +66,9 @@
<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}"></span> <span class="status ${status.classname}" id="status_${id}">
<span class="sr">${status.display_name}</span>
</span>
% endif % endif
</div> </div>
...@@ -71,7 +76,7 @@ ...@@ -71,7 +76,7 @@
<div class="capa_alert">${_(submitted_message)}</div> <div class="capa_alert">${_(submitted_message)}</div>
%endif %endif
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message" tabindex="-1">${HTML(msg)}</span>
% endif % endif
</form> </form>
</section> </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"> <section id="textbox_${id}" class="capa_inputtype textbox cminput">
<textarea rows="${rows}" cols="${cols}" name="input_${id}" <textarea rows="${rows}" cols="${cols}" name="input_${id}"
aria-label="${_("{programming_language} editor").format(programming_language=mode)}" aria-label="${_("{programming_language} editor").format(programming_language=mode)}"
...@@ -35,7 +38,7 @@ ...@@ -35,7 +38,7 @@
<span id="answer_${id}"></span> <span id="answer_${id}"></span>
<div class="external-grader-message" aria-live="polite"> <div class="external-grader-message">
${msg|n} ${HTML(msg)}
</div> </div>
</section> </section>
<%! from openedx.core.djangolib.markup import HTML %>
<section id="inputtype_${id}" class="capa_inputtype" > <section id="inputtype_${id}" class="capa_inputtype" >
<div class="crystalography_problem" style="width:${width};height:${height}"></div> <div class="crystalography_problem" style="width:${width};height:${height}"></div>
...@@ -16,13 +17,13 @@ ...@@ -16,13 +17,13 @@
<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}"> <p class="status" aria-describedby="input_${id}">
${status.display_name} <span class="sr">${status.display_name}</span>
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message" tabindex="-1">${HTML(msg)}</span>
% endif % endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
......
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
<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}"> <p class="status" aria-describedby="input_${id}">
${status.display_name} <span class="sr">${status.display_name}</span>
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
......
...@@ -17,14 +17,14 @@ ...@@ -17,14 +17,14 @@
<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" aria-describedby="input_${id}"> <p class="status drag-and-drop--status" aria-describedby="input_${id}">
${status.display_name} <span class="sr">${status.display_name}</span>
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
% if msg: % if msg:
<span class="message">${HTML(msg)}</span> <span class="message" tabindex="-1">${HTML(msg)}</span>
% endif % endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
......
...@@ -12,7 +12,7 @@ ...@@ -12,7 +12,7 @@
<input type="hidden" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}"/> <input type="hidden" name="input_${id}" aria-describedby="answer_${id}" id="input_${id}" value="${value|h}"/>
<p class="status" aria-describedby="input_${id}"> <p class="status" aria-describedby="input_${id}">
${status.display_name} <span class="sr">${status.display_name}</span>
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
......
...@@ -17,9 +17,8 @@ ...@@ -17,9 +17,8 @@
<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}">
${status.display_name} <span class="sr">${status.display_name}</span>
</p> </p>
<br/> <br/>
<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>
......
...@@ -10,5 +10,5 @@ ...@@ -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']}"/> <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>
<div class="message">${HTML(msg)}</div> <div class="message" tabindex="-1">${HTML(msg)}</div>
</section> </section>
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
<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}" id="status_${id}">
% if response_data['label']: % 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 % endif
% 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>
...@@ -18,7 +18,7 @@ ...@@ -18,7 +18,7 @@
/> />
<span class="trailing_text">${trailing_text}</span> <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 class="sr">${status.display_tooltip}</span>
</span> </span>
...@@ -33,6 +33,6 @@ ...@@ -33,6 +33,6 @@
<div class="script_placeholder" data-src="${previewer}"/> <div class="script_placeholder" data-src="${previewer}"/>
% if msg: % if msg:
<span class="message">${HTML(msg)}</span> <span class="message" aria-describedby="label_${id}" tabindex="-1">${HTML(msg)}</span>
% endif % endif
</div> </div>
<%! from openedx.core.djangolib.markup import HTML %>
<section id="inputtype_${id}" class="jsinput" <section id="inputtype_${id}" class="jsinput"
data="${gradefn}" data="${gradefn}"
% if saved_state: % if saved_state:
...@@ -41,9 +42,8 @@ ...@@ -41,9 +42,8 @@
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
<p class="status"> <p class="status">
${status.display_name} <span class="sr">${status.display_name}</span>
</p> </p>
<br/> <br/>
<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>
...@@ -52,6 +52,6 @@ ...@@ -52,6 +52,6 @@
% endif % endif
% if msg: % if msg:
<span class="message">${msg|n}</span> <span class="message" tabindex="-1">${HTML(msg)}</span>
% endif % endif
</section> </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 @@ ...@@ -4,7 +4,7 @@
<form class="inputtype option-input ${doinline}"> <form class="inputtype option-input ${doinline}">
% if response_data['label']: % 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 % endif
% for description_id, description_text in response_data['descriptions'].items(): % for description_id, description_text in response_data['descriptions'].items():
...@@ -23,12 +23,14 @@ ...@@ -23,12 +23,14 @@
</select> </select>
<div class="indicator-container"> <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 class="sr">${status.display_tooltip}</span>
</span> </span>
</div> </div>
<p class="answer" id="answer_${id}"></p> <p class="answer" id="answer_${id}"></p>
% if msg: % if msg:
<span class="message">${HTML(msg)}</span> <span class="message" aria-describedby="label_${id}" tabindex="-1">${HTML(msg)}</span>
% endif % endif
</form> </form>
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
% endif % endif
% if response_data['label']: % 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 % endif
% for description_id, description_text in response_data['descriptions'].items(): % for description_id, description_text in response_data['descriptions'].items():
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
/> />
<span class="trailing_text">${trailing_text}</span> <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 class="sr">${status.display_tooltip}</span>
</span> </span>
...@@ -51,8 +51,8 @@ ...@@ -51,8 +51,8 @@
</div> </div>
% endif % endif
% if msg: % if msg:
<span class="message">${HTML(msg)}</span> <span class="message" aria-describedby="label_${id}" tabindex="-1">${HTML(msg)}</span>
% endif % endif
</div> </div>
...@@ -20,14 +20,14 @@ ...@@ -20,14 +20,14 @@
style="display:none;" style="display:none;"
/> />
<p class="status" aria-describedby="input_${id}"> <p class="status">
${status.display_name} <span class="sr">${status.display_name}</span>
</p> </p>
<p id="answer_${id}" class="answer"></p> <p id="answer_${id}" class="answer"></p>
% if msg: % if msg:
<span class="message">${HTML(msg)}</span> <span class="message" tabindex="-1">${HTML(msg)}</span>
% endif % endif
% if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']: % if status in ['unsubmitted', 'correct', 'incorrect', 'partially-correct', 'incomplete']:
</div> </div>
......
...@@ -37,7 +37,7 @@ class ResponseXMLFactory(object): ...@@ -37,7 +37,7 @@ class ResponseXMLFactory(object):
For all response types, **kwargs can contain: For all response types, **kwargs can contain:
*question_text*: The text of the question to display, *question_text*: The text of the question to display,
wrapped in <p> tags. wrapped in <label> tags.
*explanation_text*: The detailed explanation that will *explanation_text*: The detailed explanation that will
be shown if the user answers incorrectly. be shown if the user answers incorrectly.
...@@ -72,10 +72,6 @@ class ResponseXMLFactory(object): ...@@ -72,10 +72,6 @@ class ResponseXMLFactory(object):
script_element.set("type", "loncapa/python") script_element.set("type", "loncapa/python")
script_element.text = str(script) 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) # Add the response(s)
for __ in range(int(num_responses)): for __ in range(int(num_responses)):
response_element = self.create_response_element(**kwargs) response_element = self.create_response_element(**kwargs)
...@@ -86,6 +82,10 @@ class ResponseXMLFactory(object): ...@@ -86,6 +82,10 @@ class ResponseXMLFactory(object):
root.append(response_element) root.append(response_element)
# Add the question label
question = etree.SubElement(response_element, "label")
question.text = question_text
# Add input elements # Add input elements
for __ in range(int(num_inputs)): for __ in range(int(num_inputs)):
input_element = self.create_input_element(**kwargs) input_element = self.create_input_element(**kwargs)
...@@ -113,9 +113,13 @@ class ResponseXMLFactory(object): ...@@ -113,9 +113,13 @@ class ResponseXMLFactory(object):
""" """
math_display = kwargs.get('math_display', False) math_display = kwargs.get('math_display', False)
size = kwargs.get('size', None) size = kwargs.get('size', None)
input_element_label = kwargs.get('input_element_label', '')
input_element = etree.Element('textline') input_element = etree.Element('textline')
if input_element_label:
input_element.set('label', input_element_label)
if math_display: if math_display:
input_element.set('math', '1') input_element.set('math', '1')
...@@ -267,9 +271,6 @@ class CustomResponseXMLFactory(ResponseXMLFactory): ...@@ -267,9 +271,6 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
*answer_attr*: The "answer" attribute on the tag itself (treated as an *answer_attr*: The "answer" attribute on the tag itself (treated as an
alias to "expect", though "expect" takes priority if both are given) 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 # Retrieve **kwargs
...@@ -279,7 +280,6 @@ class CustomResponseXMLFactory(ResponseXMLFactory): ...@@ -279,7 +280,6 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
answer = kwargs.get('answer', None) answer = kwargs.get('answer', None)
options = kwargs.get('options', None) options = kwargs.get('options', None)
cfn_extra_args = kwargs.get('cfn_extra_args', None) cfn_extra_args = kwargs.get('cfn_extra_args', None)
group_label = kwargs.get('group_label', None)
# Create the response element # Create the response element
response_element = etree.Element("customresponse") response_element = etree.Element("customresponse")
...@@ -297,10 +297,6 @@ class CustomResponseXMLFactory(ResponseXMLFactory): ...@@ -297,10 +297,6 @@ class CustomResponseXMLFactory(ResponseXMLFactory):
answer_element = etree.SubElement(response_element, "answer") answer_element = etree.SubElement(response_element, "answer")
answer_element.text = str(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: if options:
response_element.set('options', str(options)) response_element.set('options', str(options))
......
...@@ -151,10 +151,6 @@ class CapaHtmlRenderTest(unittest.TestCase): ...@@ -151,10 +151,6 @@ class CapaHtmlRenderTest(unittest.TestCase):
# Expect problem has been turned into a <div> # Expect problem has been turned into a <div>
self.assertEqual(rendered_html.tag, "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 # Expect that the response has been turned into a <section> with correct attributes
response_element = rendered_html.find("section") response_element = rendered_html.find("section")
self.assertEqual(response_element.tag, "section") self.assertEqual(response_element.tag, "section")
...@@ -185,7 +181,7 @@ class CapaHtmlRenderTest(unittest.TestCase): ...@@ -185,7 +181,7 @@ class CapaHtmlRenderTest(unittest.TestCase):
'id': '1_2_1', 'id': '1_2_1',
'trailing_text': '', 'trailing_text': '',
'size': None, 'size': None,
'response_data': {'label': '', 'descriptions': {}}, 'response_data': {'label': 'Test question', 'descriptions': {}},
'describedby_html': '' 'describedby_html': ''
} }
......
...@@ -930,7 +930,7 @@ class DragAndDropTemplateTest(TemplateTestCase): ...@@ -930,7 +930,7 @@ class DragAndDropTemplateTest(TemplateTestCase):
self.assert_has_xpath(xml, xpath, self.context) self.assert_has_xpath(xml, xpath, self.context)
# Expect a <p> with the status # 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) 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):
......
...@@ -96,8 +96,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase): ...@@ -96,8 +96,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
the_html = problem.get_html() the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "") without_new_lines = the_html.replace("\n", "")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback3\">.*3rd WRONG solution") 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") self.assertNotRegexpMatches(without_new_lines, r"feedback1|feedback2|feedbackC")
# Check that calling it multiple times yields the same thing # Check that calling it multiple times yields the same thing
the_html2 = problem.get_html() the_html2 = problem.get_html()
...@@ -110,11 +110,24 @@ class CapaTargetedFeedbackTest(unittest.TestCase): ...@@ -110,11 +110,24 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
the_html = problem.get_html() the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "") without_new_lines = the_html.replace("\n", "")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\">.*1st WRONG solution") 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.assertRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>")
self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3|feedbackC") 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): def test_targeted_feedback_id_typos(self):
"""Cases where the explanation-id's don't match anything.""" """Cases where the explanation-id's don't match anything."""
xml_str = textwrap.dedent(""" xml_str = textwrap.dedent("""
...@@ -280,8 +293,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase): ...@@ -280,8 +293,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
the_html = problem.get_html() the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "") without_new_lines = the_html.replace("\n", "")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\">.*1st WRONG solution") 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.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"<div>\{.*'1_solution_1'.*\}</div>")
self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3") self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3")
...@@ -350,8 +363,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase): ...@@ -350,8 +363,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
the_html = problem.get_html() the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "") without_new_lines = the_html.replace("\n", "")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\">.*1st WRONG solution") 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.assertNotRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedbackC\".*solution explanation")
self.assertRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>") self.assertRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>")
self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3|feedbackC") self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3|feedbackC")
...@@ -427,8 +440,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase): ...@@ -427,8 +440,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
the_html = problem.get_html() the_html = problem.get_html()
without_new_lines = the_html.replace("\n", "") without_new_lines = the_html.replace("\n", "")
# pylint: disable=line-too-long
self.assertRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedback1\">.*1st WRONG solution") 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.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"<div>\{.*'1_solution_1'.*\}</div>")
self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3") self.assertNotRegexpMatches(without_new_lines, r"feedback2|feedback3")
......
...@@ -69,7 +69,7 @@ class CapaModule(CapaMixin, XModule): ...@@ -69,7 +69,7 @@ class CapaModule(CapaMixin, XModule):
handlers = { handlers = {
'hint_button': self.hint_button, 'hint_button': self.hint_button,
'problem_get': self.get_problem, 'problem_get': self.get_problem,
'problem_check': self.check_problem, 'problem_check': self.submit_problem,
'problem_reset': self.reset_problem, 'problem_reset': self.reset_problem,
'problem_save': self.save_problem, 'problem_save': self.save_problem,
'problem_show': self.get_answer, 'problem_show': self.get_answer,
...@@ -212,7 +212,6 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -212,7 +212,6 @@ class CapaDescriptor(CapaFields, RawDescriptor):
CapaDescriptor.graceperiod, CapaDescriptor.graceperiod,
CapaDescriptor.force_save_button, CapaDescriptor.force_save_button,
CapaDescriptor.markdown, CapaDescriptor.markdown,
CapaDescriptor.text_customization,
CapaDescriptor.use_latex_compiler, CapaDescriptor.use_latex_compiler,
]) ])
return non_editable_fields return non_editable_fields
...@@ -276,9 +275,9 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -276,9 +275,9 @@ class CapaDescriptor(CapaFields, RawDescriptor):
# Proxy to CapaModule for access to any of its attributes # Proxy to CapaModule for access to any of its attributes
answer_available = module_attr('answer_available') answer_available = module_attr('answer_available')
check_button_name = module_attr('check_button_name') submit_button_name = module_attr('submit_button_name')
check_button_checking_name = module_attr('check_button_checking_name') submit_button_submitting_name = module_attr('submit_button_submitting_name')
check_problem = module_attr('check_problem') submit_problem = module_attr('submit_problem')
choose_new_seed = module_attr('choose_new_seed') choose_new_seed = module_attr('choose_new_seed')
closed = module_attr('closed') closed = module_attr('closed')
get_answer = module_attr('get_answer') get_answer = module_attr('get_answer')
...@@ -301,7 +300,7 @@ class CapaDescriptor(CapaFields, RawDescriptor): ...@@ -301,7 +300,7 @@ class CapaDescriptor(CapaFields, RawDescriptor):
reset_problem = module_attr('reset_problem') reset_problem = module_attr('reset_problem')
save_problem = module_attr('save_problem') save_problem = module_attr('save_problem')
set_state_from_lcp = module_attr('set_state_from_lcp') 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_reset_button = module_attr('should_show_reset_button')
should_show_save_button = module_attr('should_show_save_button') should_show_save_button = module_attr('should_show_save_button')
update_score = module_attr('update_score') update_score = module_attr('update_score')
<div class="problem"> <div class="problem">
<div aria-live="polite"> <div>
<div> <span>
<span> <section id="textbox_test_matlab_plot1_2_1" class="capa_inputtype cminput">
<p> <textarea rows="10" cols="80" name="input_i4x-MITx-2_01x-problem-test_matlab_plot1_2_1"
<p></p> aria-describedby="answer_i4x-MITx-2_01x-problem-test_matlab_plot1_2_1"
</span> id="input_i4x-MITx-2_01x-problem-test_matlab_plot1_2_1" data-tabsize="4" data-mode="octave"
<span><section id="textbox_test_matlab_plot1_2_1" class="capa_inputtype cminput"> data-linenums="true" style="display: none;">This is the MATLAB input, whatever that may be.
<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> </textarea>
<div class="grader-status" tabindex="-1"> <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 id="status_test_matlab_plot1_2_1" class="processing" aria-describedby="input_test_matlab_plot1_2_1">
<span class="status sr">processing</span> <span class="status sr">processing</span>
</span> </span>
<span style="display:none;" class="xqueue" id="test_matlab_plot1_2_1">1</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> </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"> <div class="action">
<input type="hidden" name="problem_id" value="Plot a straight line"> <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="reset" data-value="Reset">Reset<span class="sr"> your answer</span></button>
<button class="show"><span class="show-label">Show Answer</span> </button> <button class="show"><span class="show-label">Show Answer</span> </button>
</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> </div>
...@@ -12,11 +12,29 @@ ...@@ -12,11 +12,29 @@
<span id="display_example_1"></span> <span id="display_example_1"></span>
<span id="input_example_1_dynamath"></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> <div class="problem-action-buttons-wrapper">
<button class="reset">Reset</button> <span class="problem-action-button-wrapper">
<button class="save">Save</button> <button class="reset btn-default btn-small">Reset</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> </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> <a href="/courseware/6.002_Spring_2012/${ explain }" class="new-page">Explanation</a>
<div class="submission_feedback"></div> <div class="submission_feedback"></div>
</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> </div>
...@@ -17,9 +17,7 @@ var options = { ...@@ -17,9 +17,7 @@ var options = {
// Avoid adding files to this list. Use RequireJS. // Avoid adding files to this list. Use RequireJS.
libraryFilesToInclude: [ libraryFilesToInclude: [
{pattern: 'common_static/js/vendor/requirejs/require.js', included: true}, // Load the core JavaScript dependencies
{pattern: 'RequireJS-namespace-undefine.js', included: true},
{pattern: 'common_static/coffee/src/ajax_prefix.js', included: true}, {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/underscore.js', included: true},
{pattern: 'common_static/common/js/vendor/backbone.js', included: true}, {pattern: 'common_static/common/js/vendor/backbone.js', included: true},
...@@ -45,11 +43,20 @@ var options = { ...@@ -45,11 +43,20 @@ var options = {
{pattern: 'public/js/split_test_staff.js', included: true}, {pattern: 'public/js/split_test_staff.js', included: true},
{pattern: 'src/word_cloud/d3.min.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/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-waituntil.js', included: true},
{pattern: 'common_static/common/js/spec_helpers/jasmine-extensions.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}, {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} {pattern: 'spec/main_requirejs.js', included: true}
], ],
......
...@@ -125,11 +125,6 @@ class InheritanceMixin(XBlockMixin): ...@@ -125,11 +125,6 @@ class InheritanceMixin(XBlockMixin):
scope=Scope.settings, scope=Scope.settings,
default='', default='',
) )
text_customization = Dict(
display_name=_("Text Customization"),
help=_("Enter string customization substitutions for particular locations."),
scope=Scope.settings,
)
use_latex_compiler = Boolean( use_latex_compiler = Boolean(
display_name=_("Enable LaTeX Compiler"), 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."), 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: | ...@@ -43,7 +43,7 @@ data: |
par is a dictionary that contains two keys, "answer" and "state". 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 "answer" is the JSON string that "getGrade" returns.
The value of "state" is the JSON string that "getState" 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) par = json.loads(ans)
......
...@@ -3,7 +3,7 @@ Tests the logic of problems with a delay between attempt submissions. ...@@ -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 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 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 submissions" setting is set to different values
""" """
...@@ -128,7 +128,7 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): ...@@ -128,7 +128,7 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase):
last_submission_time=None, last_submission_time=None,
submission_wait_seconds=None, submission_wait_seconds=None,
considered_now=None, considered_now=None,
skip_check_problem=False): skip_submit_problem=False):
"""Unified create and check code for the tests here.""" """Unified create and check code for the tests here."""
module = CapaFactoryWithDelay.create( module = CapaFactoryWithDelay.create(
attempts=num_attempts, attempts=num_attempts,
...@@ -138,12 +138,12 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): ...@@ -138,12 +138,12 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase):
) )
module.done = False module.done = False
get_request_dict = {CapaFactoryWithDelay.input_key(): "3.14"} get_request_dict = {CapaFactoryWithDelay.input_key(): "3.14"}
if skip_check_problem: if skip_submit_problem:
return (module, None) return (module, None)
if considered_now is not 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: else:
result = module.check_problem(get_request_dict) result = module.submit_problem(get_request_dict)
return (module, result) return (module, result)
def test_first_submission(self): def test_first_submission(self):
...@@ -251,13 +251,13 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase): ...@@ -251,13 +251,13 @@ class XModuleQuizAttemptsDelayTest(unittest.TestCase):
considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0, tzinfo=UTC) 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( (module, unused_result) = self.create_and_check(
num_attempts=num_attempts, num_attempts=num_attempts,
last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC), last_submission_time=datetime.datetime(2013, 12, 6, 0, 17, 36, tzinfo=UTC),
submission_wait_seconds=180, submission_wait_seconds=180,
considered_now=datetime.datetime(2013, 12, 6, 0, 24, 0, tzinfo=UTC), 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 # Expect that number of attempts NOT incremented
self.assertEqual(module.attempts, num_attempts) self.assertEqual(module.attempts, num_attempts)
......
...@@ -290,6 +290,7 @@ class XBlockWrapperTestMixin(object): ...@@ -290,6 +290,7 @@ class XBlockWrapperTestMixin(object):
# pylint: disable=no-member # pylint: disable=no-member
descriptor.runtime.id_reader.get_definition_id = Mock(return_value='a') descriptor.runtime.id_reader.get_definition_id = Mock(return_value='a')
descriptor.runtime.modulestore = modulestore descriptor.runtime.modulestore = modulestore
descriptor._xmodule.graded = 'False'
self.check_property(descriptor) self.check_property(descriptor)
# Test that when an xmodule is generated from descriptor_cls # 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() { ...@@ -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() { ...@@ -147,28 +147,52 @@ $(function() {
SRAlert = (function() { SRAlert = (function() {
function SRAlert() { function SRAlert() {
$('body').append('<div id="reader-feedback" class="sr" style="display:none" aria-hidden="false" aria-atomic="true" aria-live="assertive"></div>'); // This initialization sometimes gets done twice, so take to only create a single reader-feedback div.
this.el = $('#reader-feedback'); 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() { SRAlert.prototype.clear = function() {
return this.el.html(' '); edx.HtmlUtils.setHtml(this.el, '');
}; };
SRAlert.prototype.readElts = function(elts) { SRAlert.prototype.readElts = function(elts) {
var feedback = ''; var texts = [];
$.each(elts, function(idx, value) { $.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) { 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; 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): ...@@ -72,7 +72,7 @@ class AnnotationComponentPage(PageObject):
# Wait for the click to take effect, which is after the class is applied. # 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') self.wait_for(lambda: 'selected' in self.q(css=answer_css).attrs('class')[0], description='answer selected')
# Click the "Check" button. # 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. # This will trigger a POST to problem_check so wait until the response is returned.
self.wait_for_ajax() self.wait_for_ajax()
......
...@@ -2,6 +2,8 @@ ...@@ -2,6 +2,8 @@
Problem Page. Problem Page.
""" """
from bok_choy.page_object import PageObject 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): class ProblemPage(PageObject):
...@@ -20,6 +22,7 @@ class ProblemPage(PageObject): ...@@ -20,6 +22,7 @@ class ProblemPage(PageObject):
""" """
Return the current problem name. 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] return self.q(css='.problem-header').text[0]
@property @property
...@@ -48,14 +51,15 @@ class ProblemPage(PageObject): ...@@ -48,14 +51,15 @@ class ProblemPage(PageObject):
""" """
Return the "hint" text of the problem from html 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 @property
def hint_text(self): def hint_text(self):
""" """
Return the "hint" text of the problem from its div. 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): def verify_mathjax_rendered_in_problem(self):
""" """
...@@ -108,32 +112,113 @@ class ProblemPage(PageObject): ...@@ -108,32 +112,113 @@ class ProblemPage(PageObject):
self.wait_for_element_invisibility('.loading', 'wait for loading icon to disappear') self.wait_for_element_invisibility('.loading', 'wait for loading icon to disappear')
self.wait_for_ajax() 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() click_css(self, '.problem .submit', require_notification=False)
self.wait_for_ajax()
def click_save(self): def click_save(self):
""" """
Click the Save button. Click the Save button.
""" """
self.q(css='div.problem button.save').click() click_css(self, '.problem .save', require_notification=False)
self.wait_for_ajax()
def click_reset(self): def click_reset(self):
""" """
Click the Reset button. Click the Reset button.
""" """
self.q(css='div.problem button.reset').click() click_css(self, '.problem .reset', require_notification=False)
self.wait_for_ajax()
def click_show_hide_button(self): def click_show(self):
""" Click the Show/Hide button. """ """
self.q(css='div.problem div.action .show').click() Click the Show Answer button.
"""
self.q(css='.problem .show').click()
self.wait_for_ajax() 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): def wait_for_status_icon(self):
""" """
wait for status icon wait for status icon
...@@ -151,12 +236,67 @@ class ProblemPage(PageObject): ...@@ -151,12 +236,67 @@ class ProblemPage(PageObject):
msg = "Wait for status to be {}".format(message) msg = "Wait for status to be {}".format(message)
self.wait_for_element_visibility(status_selector, msg) 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): def click_hint(self):
""" """
Click the Hint button. Click the Hint button.
""" """
self.q(css='div.problem button.hint-button').click() click_css(self, '.problem .hint-button', require_notification=False)
self.wait_for_ajax() 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): def click_choice(self, choice_value):
""" """
...@@ -235,3 +375,11 @@ class ProblemPage(PageObject): ...@@ -235,3 +375,11 @@ class ProblemPage(PageObject):
Return a list of question descriptions of the problem. Return a list of question descriptions of the problem.
""" """
return self.q(css="div.problem .wrapper-problem-response .question-description").text 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): ...@@ -215,7 +215,6 @@ class AdvancedSettingsPage(CoursePage):
'show_reset_button', 'show_reset_button',
'static_asset_path', 'static_asset_path',
'teams_configuration', 'teams_configuration',
'text_customization',
'annotation_storage_url', 'annotation_storage_url',
'social_sharing_url', 'social_sharing_url',
'video_bumper', 'video_bumper',
......
...@@ -26,7 +26,7 @@ class CrowdsourcehinterProblemPage(PageObject): ...@@ -26,7 +26,7 @@ class CrowdsourcehinterProblemPage(PageObject):
Submit an answer to the problem block Submit an answer to the problem block
""" """
self.q(css='input[type="text"]').fill(text) 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() self.wait_for_ajax()
def get_hint_text(self): def get_hint_text(self):
......
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
<optionresponse> <optionresponse>
<optioninput options="('yellow','blue','green')" correct="blue"/> <optioninput options="('yellow','blue','green')" correct="blue"/>
</optionresponse> </optionresponse>
<p>Which piece of furniture is built for sitting?</p>
<multiplechoiceresponse> <multiplechoiceresponse>
<label>Which piece of furniture is built for sitting?</label>
<choicegroup type="MultipleChoice"> <choicegroup type="MultipleChoice">
<choice correct="false">a table</choice> <choice correct="false">a table</choice>
<choice correct="false">a desk</choice> <choice correct="false">a desk</choice>
...@@ -15,8 +15,8 @@ ...@@ -15,8 +15,8 @@
<choice correct="false">a bookshelf</choice> <choice correct="false">a bookshelf</choice>
</choicegroup> </choicegroup>
</multiplechoiceresponse> </multiplechoiceresponse>
<p>Which of the following are musical instruments?</p>
<choiceresponse> <choiceresponse>
<label>Which of the following are musical instruments?</label>
<checkboxgroup> <checkboxgroup>
<choice correct="true">a piano</choice> <choice correct="true">a piano</choice>
<choice correct="false">a tree</choice> <choice correct="false">a tree</choice>
......
...@@ -218,14 +218,14 @@ class CertificateProgressPageTest(UniqueCourseTest): ...@@ -218,14 +218,14 @@ class CertificateProgressPageTest(UniqueCourseTest):
self.course_nav.q(css='select option[value="{}"]'.format('blue')).first.click() self.course_nav.q(css='select option[value="{}"]'.format('blue')).first.click()
# Select correct radio button for the answer # 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 # 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(2) 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(4) input').nth(1).click()
# Submit the answer # 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() self.course_nav.wait_for_ajax()
# Navigate to the 'Test Subsection 2' of 'Test Section 2' # Navigate to the 'Test Subsection 2' of 'Test Section 2'
...@@ -238,5 +238,5 @@ class CertificateProgressPageTest(UniqueCourseTest): ...@@ -238,5 +238,5 @@ class CertificateProgressPageTest(UniqueCourseTest):
self.course_nav.q(css='input[id^=input_][id$=_2_1]').fill('A*x^2 + sqrt(y)') self.course_nav.q(css='input[id^=input_][id$=_2_1]').fill('A*x^2 + sqrt(y)')
# Submit the answer # 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() self.course_nav.wait_for_ajax()
...@@ -109,7 +109,7 @@ class ConditionalTest(UniqueCourseTest): ...@@ -109,7 +109,7 @@ class ConditionalTest(UniqueCourseTest):
# Answer the problem # Answer the problem
problem_page = ProblemPage(self.browser) problem_page = ProblemPage(self.browser)
problem_page.fill_answer('correct string') 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. # The conditional does not update on its own, so we need to reload the page.
self.courseware_page.visit() self.courseware_page.visit()
# Verify that we can see the content. # Verify that we can see the content.
......
...@@ -1085,12 +1085,12 @@ class ProblemExecutionTest(UniqueCourseTest): ...@@ -1085,12 +1085,12 @@ class ProblemExecutionTest(UniqueCourseTest):
# Fill in the answer correctly. # Fill in the answer correctly.
problem_page.fill_answer("20") problem_page.fill_answer("20")
problem_page.click_check() problem_page.click_submit()
self.assertTrue(problem_page.is_correct()) self.assertTrue(problem_page.is_correct())
# Fill in the answer incorrectly. # Fill in the answer incorrectly.
problem_page.fill_answer("4") problem_page.fill_answer("4")
problem_page.click_check() problem_page.click_submit()
self.assertFalse(problem_page.is_correct()) self.assertFalse(problem_page.is_correct())
......
...@@ -714,12 +714,12 @@ class ProblemStateOnNavigationTest(UniqueCourseTest): ...@@ -714,12 +714,12 @@ class ProblemStateOnNavigationTest(UniqueCourseTest):
) )
self.assertEqual(self.problem_page.problem_name, problem_name) 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: Scenario:
I go to sequential position 1 I go to sequential position 1
Facing problem1, I select 'choice_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 go to sequential position 2
Then I came back to sequential position 1 again Then I came back to sequential position 1 again
Facing problem1, I observe the problem1 content is not Facing problem1, I observe the problem1 content is not
...@@ -730,7 +730,7 @@ class ProblemStateOnNavigationTest(UniqueCourseTest): ...@@ -730,7 +730,7 @@ class ProblemStateOnNavigationTest(UniqueCourseTest):
# Update problem 1's content state by clicking check button. # Update problem 1's content state by clicking check button.
self.problem_page.click_choice('choice_choice_1') 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.wait_for_expected_status('label.choicegroup_incorrect', 'incorrect')
# Save problem 1's content state as we're about to switch units in the sequence. # Save problem 1's content state as we're about to switch units in the sequence.
...@@ -761,7 +761,7 @@ class ProblemStateOnNavigationTest(UniqueCourseTest): ...@@ -761,7 +761,7 @@ class ProblemStateOnNavigationTest(UniqueCourseTest):
# Update problem 1's content state by clicking save button. # Update problem 1's content state by clicking save button.
self.problem_page.click_choice('choice_choice_1') self.problem_page.click_choice('choice_choice_1')
self.problem_page.click_save() 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. # 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 problem1_content_before_switch = self.problem_page.problem_content
...@@ -790,7 +790,7 @@ class ProblemStateOnNavigationTest(UniqueCourseTest): ...@@ -790,7 +790,7 @@ class ProblemStateOnNavigationTest(UniqueCourseTest):
# Update problem 1's content state – by performing reset operation. # Update problem 1's content state – by performing reset operation.
self.problem_page.click_choice('choice_choice_1') 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.wait_for_expected_status('label.choicegroup_incorrect', 'incorrect')
self.problem_page.click_reset() self.problem_page.click_reset()
self.problem_page.wait_for_expected_status('span.unanswered', 'unanswered') self.problem_page.wait_for_expected_status('span.unanswered', 'unanswered')
......
...@@ -99,7 +99,7 @@ class EntranceExamPassTest(EntranceExamTest): ...@@ -99,7 +99,7 @@ class EntranceExamPassTest(EntranceExamTest):
self.assertTrue(self.courseware_page.has_entrance_exam_message()) self.assertTrue(self.courseware_page.has_entrance_exam_message())
self.assertFalse(self.courseware_page.has_passed_message()) self.assertFalse(self.courseware_page.has_passed_message())
problem_page.click_choice('choice_1') problem_page.click_choice('choice_1')
problem_page.click_check() problem_page.click_submit()
self.courseware_page.wait_for_page() self.courseware_page.wait_for_page()
self.assertTrue(self.courseware_page.has_passed_message()) self.assertTrue(self.courseware_page.has_passed_message())
self.assertEqual(self.courseware_page.chapter_count_in_navigation, 2) self.assertEqual(self.courseware_page.chapter_count_in_navigation, 2)
...@@ -114,7 +114,7 @@ class GatingTest(UniqueCourseTest): ...@@ -114,7 +114,7 @@ class GatingTest(UniqueCourseTest):
problem_page = ProblemPage(self.browser) problem_page = ProblemPage(self.browser)
self.assertEqual(problem_page.wait_for_page().problem_name, 'HEIGHT OF EIFFEL TOWER') self.assertEqual(problem_page.wait_for_page().problem_name, 'HEIGHT OF EIFFEL TOWER')
problem_page.click_choice('choice_1') problem_page.click_choice('choice_1')
problem_page.click_check() problem_page.click_submit()
def test_subsection_gating_in_studio(self): def test_subsection_gating_in_studio(self):
""" """
......
...@@ -234,7 +234,6 @@ class StaffDebugTest(CourseWithoutContentGroupsTest): ...@@ -234,7 +234,6 @@ class StaffDebugTest(CourseWithoutContentGroupsTest):
'for user {}'.format(self.USERNAME), msg) 'for user {}'.format(self.USERNAME), msg)
@attr(shard=3)
class CourseWithContentGroupsTest(StaffViewTest): class CourseWithContentGroupsTest(StaffViewTest):
""" """
Verifies that changing the "View this course as" selector works properly for content groups. Verifies that changing the "View this course as" selector works properly for content groups.
...@@ -265,8 +264,8 @@ class CourseWithContentGroupsTest(StaffViewTest): ...@@ -265,8 +264,8 @@ class CourseWithContentGroupsTest(StaffViewTest):
""" """
problem_data = dedent(""" problem_data = dedent("""
<problem markdown="Simple Problem" max_attempts="" weight=""> <problem markdown="Simple Problem" max_attempts="" weight="">
<p>Choose Yes.</p>
<choiceresponse> <choiceresponse>
<label>Choose Yes.</label>
<checkboxgroup> <checkboxgroup>
<choice correct="true">Yes</choice> <choice correct="true">Yes</choice>
</checkboxgroup> </checkboxgroup>
...@@ -294,6 +293,7 @@ class CourseWithContentGroupsTest(StaffViewTest): ...@@ -294,6 +293,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
) )
) )
@attr(shard=3)
def test_staff_sees_all_problems(self): def test_staff_sees_all_problems(self):
""" """
Scenario: Staff see all problems Scenario: Staff see all problems
...@@ -305,6 +305,7 @@ class CourseWithContentGroupsTest(StaffViewTest): ...@@ -305,6 +305,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
course_page = self._goto_staff_page() course_page = self._goto_staff_page()
verify_expected_problem_visibility(self, course_page, [self.alpha_text, self.beta_text, self.everyone_text]) 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): def test_student_not_in_content_group(self):
""" """
Scenario: When previewing as a student, only content visible to all is shown Scenario: When previewing as a student, only content visible to all is shown
...@@ -318,6 +319,7 @@ class CourseWithContentGroupsTest(StaffViewTest): ...@@ -318,6 +319,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
course_page.set_staff_view_mode('Student') course_page.set_staff_view_mode('Student')
verify_expected_problem_visibility(self, course_page, [self.everyone_text]) verify_expected_problem_visibility(self, course_page, [self.everyone_text])
@attr(shard=3)
def test_as_student_in_alpha(self): def test_as_student_in_alpha(self):
""" """
Scenario: When previewing as a student in group alpha, only content visible to alpha is shown Scenario: When previewing as a student in group alpha, only content visible to alpha is shown
...@@ -331,6 +333,7 @@ class CourseWithContentGroupsTest(StaffViewTest): ...@@ -331,6 +333,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
course_page.set_staff_view_mode('Student in alpha') course_page.set_staff_view_mode('Student in alpha')
verify_expected_problem_visibility(self, course_page, [self.alpha_text, self.everyone_text]) verify_expected_problem_visibility(self, course_page, [self.alpha_text, self.everyone_text])
@attr(shard=3)
def test_as_student_in_beta(self): def test_as_student_in_beta(self):
""" """
Scenario: When previewing as a student in group beta, only content visible to beta is shown Scenario: When previewing as a student in group beta, only content visible to beta is shown
...@@ -366,6 +369,7 @@ class CourseWithContentGroupsTest(StaffViewTest): ...@@ -366,6 +369,7 @@ class CourseWithContentGroupsTest(StaffViewTest):
add_cohort_with_student("Cohort Beta", "beta", student_b_username) add_cohort_with_student("Cohort Beta", "beta", student_b_username)
cohort_management_page.wait_for_ajax() cohort_management_page.wait_for_ajax()
@attr(shard=3)
def test_as_specific_student(self): def test_as_specific_student(self):
student_a_username = 'tass_student_a' student_a_username = 'tass_student_a'
student_b_username = 'tass_student_b' student_b_username = 'tass_student_b'
......
...@@ -76,7 +76,7 @@ class ProgressPageBaseTest(UniqueCourseTest): ...@@ -76,7 +76,7 @@ class ProgressPageBaseTest(UniqueCourseTest):
""" """
self.courseware_page.go_to_sequential_position(1) self.courseware_page.go_to_sequential_position(1)
self.problem_page.click_choice('choice_choice_2') self.problem_page.click_choice('choice_choice_2')
self.problem_page.click_check() self.problem_page.click_submit()
def _get_section_score(self): def _get_section_score(self):
""" """
......
...@@ -74,7 +74,7 @@ def answer_problem_step(step, problem_type, correctness): ...@@ -74,7 +74,7 @@ def answer_problem_step(step, problem_type, correctness):
input_problem_answer(step, problem_type, correctness) input_problem_answer(step, problem_type, correctness)
# Submit the problem # Submit the problem
check_problem(step) submit_problem(step)
@step(u'I input an answer on a "([^"]*)" problem "([^"]*)ly"') @step(u'I input an answer on a "([^"]*)" problem "([^"]*)ly"')
...@@ -87,26 +87,18 @@ def input_problem_answer(_, problem_type, correctness): ...@@ -87,26 +87,18 @@ def input_problem_answer(_, problem_type, correctness):
answer_problem(world.scenario_dict['COURSE'].number, problem_type, correctness) answer_problem(world.scenario_dict['COURSE'].number, problem_type, correctness)
@step(u'I check a problem') @step(u'I submit a problem')
def check_problem(step): # pylint: disable=unused-argument
def submit_problem(step):
# first scroll down so the loading mathjax button does not # 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)") world.browser.execute_script("window.scrollTo(0,1024)")
assert world.is_css_not_present("button.check.is-disabled") world.css_click("button.submit")
world.css_click("button.check")
# Wait for the problem to finish re-rendering # Wait for the problem to finish re-rendering
world.wait_for_ajax_complete() 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') @step(u'The "([^"]*)" problem displays a "([^"]*)" answer')
def assert_problem_has_answer(step, problem_type, answer_class): def assert_problem_has_answer(step, problem_type, answer_class):
''' '''
...@@ -147,21 +139,13 @@ def action_button_present(_step, buttonname, doesnt_appear): ...@@ -147,21 +139,13 @@ def action_button_present(_step, buttonname, doesnt_appear):
assert world.is_css_present(button_css) 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 "([^"]*)"$') @step(u'I should see a score of "([^"]*)"$')
def see_score(_step, score): def see_score(_step, score):
# The problem progress is changed by # The problem progress is changed by
# cms/static/xmodule_js/src/capa/display.js # cms/static/xmodule_js/src/capa/display.js
# so give it some time to render on the page. # so give it some time to render on the page.
score_css = 'div.problem-progress' 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)) 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): ...@@ -295,27 +295,27 @@ def problem_has_answer(course, problem_type, answer_class):
elif problem_type == "multiple choice": elif problem_type == "multiple choice":
if answer_class == 'correct': if answer_class == 'correct':
assert_checked(course, 'multiple choice', ['choice_2']) assert_submitted(course, 'multiple choice', ['choice_2'])
elif answer_class == 'incorrect': elif answer_class == 'incorrect':
assert_checked(course, 'multiple choice', ['choice_1']) assert_submitted(course, 'multiple choice', ['choice_1'])
else: else:
assert_checked(course, 'multiple choice', []) assert_submitted(course, 'multiple choice', [])
elif problem_type == "checkbox": elif problem_type == "checkbox":
if answer_class == 'correct': if answer_class == 'correct':
assert_checked(course, 'checkbox', ['choice_0', 'choice_2']) assert_submitted(course, 'checkbox', ['choice_0', 'choice_2'])
elif answer_class == 'incorrect': elif answer_class == 'incorrect':
assert_checked(course, 'checkbox', ['choice_3']) assert_submitted(course, 'checkbox', ['choice_3'])
else: else:
assert_checked(course, 'checkbox', []) assert_submitted(course, 'checkbox', [])
elif problem_type == "radio": elif problem_type == "radio":
if answer_class == 'correct': if answer_class == 'correct':
assert_checked(course, 'radio', ['choice_2']) assert_submitted(course, 'radio', ['choice_2'])
elif answer_class == 'incorrect': elif answer_class == 'incorrect':
assert_checked(course, 'radio', ['choice_1']) assert_submitted(course, 'radio', ['choice_1'])
else: else:
assert_checked(course, 'radio', []) assert_submitted(course, 'radio', [])
elif problem_type == 'string': elif problem_type == 'string':
if answer_class == 'blank': if answer_class == 'blank':
...@@ -410,23 +410,23 @@ def inputfield(course, problem_type, choice=None, input_num=1): ...@@ -410,23 +410,23 @@ def inputfield(course, problem_type, choice=None, input_num=1):
return sel 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 Assert that choice names given in *choices* are the only
ones checked. ones submitted.
Works for both radio and checkbox problems Works for both radio and checkbox problems
''' '''
all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3'] all_choices = ['choice_0', 'choice_1', 'choice_2', 'choice_3']
for this_choice in all_choices: for this_choice in all_choices:
def check_problem(): def submit_problem():
element = world.css_find(inputfield(course, problem_type, choice=this_choice)) element = world.css_find(inputfield(course, problem_type, choice=this_choice))
if this_choice in choices: if this_choice in choices:
assert element.checked assert element.checked
else: else:
assert not element.checked 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): def assert_textfield(course, problem_type, expected_text, input_num=1):
......
...@@ -90,3 +90,6 @@ ...@@ -90,3 +90,6 @@
// overrides // overrides
@import 'developer'; // used for any developer-created scss that needs further polish/refactoring @import 'developer'; // used for any developer-created scss that needs further polish/refactoring
@import 'shame'; // used for any bad-form/orphaned scss @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; ...@@ -32,9 +32,8 @@ $headings-base-color: $gray-d2;
%hd-2 { %hd-2 {
margin-bottom: 1em; font-size: em(18);
font-size: 1.5em; font-weight: $headings-font-weight-bold;
font-weight: $headings-font-weight-normal;
line-height: 1.4em; line-height: 1.4em;
} }
...@@ -118,7 +117,7 @@ $headings-base-color: $gray-d2; ...@@ -118,7 +117,7 @@ $headings-base-color: $gray-d2;
h3 { h3 {
@extend %hd-2; @extend %hd-2;
font-weight: $headings-font-weight-normal; font-weight: $headings-font-weight-bold;
// override external modules and xblocks that use inline CSS // override external modules and xblocks that use inline CSS
text-transform: initial; text-transform: initial;
......
../../../common/static/sass/edx-pattern-library-shims
\ No newline at end of file
...@@ -547,7 +547,6 @@ ...@@ -547,7 +547,6 @@
margin: 0 auto; margin: 0 auto;
width: flex-grid(12); width: flex-grid(12);
max-width: $fg-max-width; max-width: $fg-max-width;
min-width: $fg-min-width;
strong { strong {
@extend %t-strong; @extend %t-strong;
......
...@@ -211,14 +211,13 @@ $shadow-d1: rgba(0,0,0,0.4) !default; ...@@ -211,14 +211,13 @@ $shadow-d1: rgba(0,0,0,0.4) !default;
$shadow-d2: rgba($black, 0.6) !default; $shadow-d2: rgba($black, 0.6) !default;
// system feedback-based colors // system feedback-based colors
$error-color: rgb(253, 87, 87) !default; $error-color: rgb(203, 7, 18) !default;
$warning-color: rgb(181,42,103) !default; $warning-color: rgb(255, 192, 31) !default;
$confirm-color: rgb(0, 132, 1) !default; $confirm-color: rgb(0, 132, 1) !default;
$active-color: $blue !default; $active-color: $blue !default;
$highlight-color: rgb(255,255,0) !default; $highlight-color: rgb(255,255,0) !default;
$alert-color: rgb(212, 64, 64) !default; $alert-color: rgb(212, 64, 64) !default;
$warning-color: rgb(237, 189, 60) !default; $success-color: rgb(0, 155, 0) !default;
$success-color: rgb(37, 184, 90) !default;
// ---------------------------- // ----------------------------
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
%> %>
<div class='exam-text'> <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')}) %> <%= 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>
<div id="turn_in_exam_id" class="pull-right turn_in_exam"> <div id="turn_in_exam_id" class="pull-right turn_in_exam">
<span> <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 ...@@ -5,38 +5,97 @@ from openedx.core.djangolib.markup import HTML
%> %>
<%namespace name='static' file='static_content.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'] } ${ problem['name'] }
</h3> </h3>
<div class="problem-progress"></div> <div class="problem-progress" id="${ id }-problem-progress"></div>
<div class="problem"> <div class="problem">
${ HTML(problem['html']) } ${ HTML(problem['html']) }
<div class="action"> <div class="action">
<input type="hidden" name="problem_id" value="${ problem['name'] }" /> <input type="hidden" name="problem_id" value="${ problem['name'] }" />
% if demand_hint_possible: % if demand_hint_possible:
<div class="problem-hint" aria-live="polite"></div> <div class="problem-hint">
% endif <%include file="problem_notifications.html" args="
% if check_button: notification_name='hint',
<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> notification_type='problem-hint',
notification_icon='fa-question',
notification_message=''"
/>
</div>
% endif % endif
<div class="problem-action-buttons-wrapper">
% if demand_hint_possible: % if demand_hint_possible:
<button class="hint-button" data-value="${_('Hint')}">${_('Hint')}</button> <span class="problem-action-button-wrapper">
% endif <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>
% if reset_button: </span>
<button class="reset" data-value="${_('Reset')}">${_('Reset')}<span class="sr"> ${_("your answer")}</span></button>
% endif % endif
% if save_button: % 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 % endif
% if answer_available: % 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 % 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> </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 % endif
</div>
</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>
<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 ...@@ -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 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/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/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/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 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 -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