Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ client error are correctly passed through to the client.
LMS: Improve performance of page load and thread list load for
discussion tab

Studio: Support targeted feedback, which allows for authors to provide explanations for
incorrect choice selections for multiple choice question choices that will automatically
display. These are intended to help steer a student to the correct answer. Thus, they are
best used for quizzes that allow multiple attempts. To provide targeted feedback, add an
element called <targetedfeedbackset> right before your <solution> or <solutionset>, and in
this element, provide a <targetedfeedback> for each feedback. Within <targetedfeedback>
you can specify your text explanation. Both the <targetedfeedback> and <choice> should have
the same explanation-id attribute.

LMS: The wiki markup cheatsheet dialog is now accessible to screen readers.
(LMS-1303)

Expand Down
86 changes: 84 additions & 2 deletions common/lib/capa/capa/capa_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@

from pytz import UTC


# dict of tagname, Response Class -- this should come from auto-registering
response_tag_dict = dict([(x.response_tag, x) for x in responsetypes.__all__])

Expand Down Expand Up @@ -381,11 +382,93 @@ def get_answer_ids(self):
answer_ids.append(results.keys())
return answer_ids

def tree_using_targeted_feedback(self, tree):
"""
Allows for problem questions to show targeted feedback, which are choice-level explanations.
Targeted feedback is automatically visible after a student has submitted their answers.

The <multiplechoiceresponse> tag must have an attribute 'targeted-feedback':
- if so, this method will modify the tree
- if not, this method will not modify the tree
- if the value is 'alwaysShowCorrectChoiceExplanation', then the correct-choice
explanation will be automatically visible too after a student has submitted answers

Note if the value is 'alwaysShowCorrectChoiceExplanation', you probably want to set
the "Show Answer" setting to "Never" because now there's no need for a "Show Answer"
button because no solution will show up if you were to click the "Show Answer" button
"""

# Note that if there are no questions with targeted feedback, the body of the for loop is not executed
for mult_choice_response in tree.xpath('//multiplechoiceresponse[@targeted-feedback]'):
show_explanation = mult_choice_response.get('targeted-feedback') == 'alwaysShowCorrectChoiceExplanation'

# Grab the first choicegroup (there should only be one within each <multiplechoiceresponse> tag)
choicegroup = mult_choice_response.xpath('./choicegroup[@type="MultipleChoice"]')[0]
choices_list = list(choicegroup.iter('choice'))

# Find the student answer key that matches our <choicegroup> id
student_answer = self.student_answers.get(choicegroup.get('id'))
expl_id_for_student_answer = None

# Keep track of the explanation-id that corresponds to the student's answer
# Also, keep track of the solution-id
solution_id = None
for choice in choices_list:
if choice.get('name') == student_answer:
expl_id_for_student_answer = choice.get('explanation-id')
if choice.get('correct') == 'true':
solution_id = choice.get('explanation-id')

# Filter out targetedfeedback that doesn't correspond to the answer the student selected
# Note: following-sibling will grab all following siblings, so we just want the first in the list
targetedfeedbackset = mult_choice_response.xpath('./following-sibling::targetedfeedbackset')
if len(targetedfeedbackset) != 0:
targetedfeedbackset = targetedfeedbackset[0]
targetedfeedbacks = targetedfeedbackset.xpath('./targetedfeedback')
for targetedfeedback in targetedfeedbacks:
# Don't show targeted feedback if the student hasn't answer the problem
# or if the target feedback doesn't match the student's (incorrect) answer
if not self.done or targetedfeedback.get('explanation-id') != expl_id_for_student_answer:
targetedfeedbackset.remove(targetedfeedback)

# Do not displace the solution under these circumstances
if not show_explanation or not self.done:
continue

# The next element should either be <solution> or <solutionset>
next_element = targetedfeedbackset.getnext()
parent_element = tree
solution_element = None
if next_element.tag == 'solution':
solution_element = next_element
elif next_element.tag == 'solutionset':
solutions = next_element.xpath('./solution')
for solution in solutions:
if solution.get('explanation-id') == solution_id:
parent_element = next_element
solution_element = solution

# If could not find the solution element, then skip the remaining steps below
if solution_element is None:
continue

# Change our correct-choice explanation from a "solution explanation" to within
# the set of targeted feedback, which means the explanation will render on the page
# without the student clicking "Show Answer" or seeing a checkmark next to the correct choice
parent_element.remove(solution_element)

# Add our solution instead to the targetedfeedbackset and change its tag name
solution_element.tag = 'targetedfeedback'
targetedfeedbackset.append(solution_element)

def get_html(self):
'''
Main method called externally to get the HTML to be rendered for this capa Problem.
'''

self.tree_using_targeted_feedback(self.tree)
html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context)

return html

def handle_input_ajax(self, data):
Expand Down Expand Up @@ -562,8 +645,7 @@ def _extract_html(self, problemtree): # private
# other than to examine .tag to see if it's a string. :(
return

if (problemtree.tag == 'script' and problemtree.get('type')
and 'javascript' in problemtree.get('type')):
if (problemtree.tag == 'script' and problemtree.get('type') and 'javascript' in problemtree.get('type')):
# leave javascript intact.
return deepcopy(problemtree)

Expand Down
38 changes: 38 additions & 0 deletions common/lib/capa/capa/customrender.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,41 @@ def get_html(self):
return etree.XML(html)

registry.register(SolutionRenderer)

#-----------------------------------------------------------------------------


class TargetedFeedbackRenderer(object):
'''
A targeted feedback is just a <span>...</span> that is used for displaying an
extended piece of feedback to students if they incorrectly answered a question.
'''
tags = ['targetedfeedback']

def __init__(self, system, xml):
self.system = system
self.xml = xml

def get_html(self):
"""
Return the contents of this tag, rendered to html, as an etree element.
"""

html = '<section class="targeted-feedback-span"><span>%s</span></section>' % (
etree.tostring(self.xml))
try:
xhtml = etree.XML(html)
except Exception as err:
if self.system.DEBUG:
msg = '<html><div class="inline-error"><p>Error %s</p>' % (
str(err).replace('<', '&lt;'))
msg += ('<p>Failed to construct targeted feedback from <pre>%s</pre></p>' %
html.replace('<', '&lt;'))
msg += "</div></html>"
log.error(msg)
return etree.XML(msg)
else:
raise
return xhtml

registry.register(TargetedFeedbackRenderer)
Loading