Skip to content
Merged
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
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ Blades: Add view for field type Dict in Studio. BLD-658.

Blades: Refactor stub implementation of LTI Provider. BLD-601.

LMS: multiple choice features: shuffle, answer-pool, targeted-feedback,
choice name masking, submission timer

Studio: Added ability to edit course short descriptions that appear on the course catalog page.

LMS: In left accordion and progress page, due dates are now displayed in time
Expand Down
3 changes: 2 additions & 1 deletion cms/djangoapps/contentstore/features/problem-editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
PROBLEM_WEIGHT = "Problem Weight"
RANDOMIZATION = 'Randomization'
SHOW_ANSWER = "Show Answer"

TIMER_BETWEEN_ATTEMPTS = "Timer Between Attempts"

@step('I have created a Blank Common Problem$')
def i_created_blank_common_problem(step):
Expand Down Expand Up @@ -44,6 +44,7 @@ def i_see_advanced_settings_with_values(step):
[PROBLEM_WEIGHT, "", False],
[RANDOMIZATION, "Never", False],
[SHOW_ANSWER, "Finished", False],
[TIMER_BETWEEN_ATTEMPTS, "0", False]
])


Expand Down
82 changes: 82 additions & 0 deletions common/lib/capa/capa/capa_problem.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,14 @@ def __init__(self, problem_text, id, capa_system, state=None, seed=None):
# input_id string -> InputType object
self.inputs = {}

# Run response late_transforms last (see MultipleChoiceResponse)
# Sort the responses to be in *_1 *_2 ... order.
responses = self.responders.values()
responses = sorted(responses, key=lambda resp: int(resp.id[resp.id.rindex('_') + 1:]))
for response in responses:
if hasattr(response, 'late_transforms'):
response.late_transforms(self)

self.extracted_tree = self._extract_html(self.tree)

def do_reset(self):
Expand Down Expand Up @@ -419,10 +427,84 @@ def get_answer_ids(self):
answer_ids.append(results.keys())
return answer_ids

def do_targeted_feedback(self, tree):
"""
Implements the targeted-feedback=N in-place on <multiplechoiceresponse> --
choice-level explanations shown to a student after submission.
Does nothing if there is no targeted-feedback attribute.
"""
for mult_choice_response in tree.xpath('//multiplechoiceresponse[@targeted-feedback]'):
# Note that the modifications has been done, avoiding problems if called twice.
if hasattr(self, 'has_targeted'):
continue
self.has_targeted = True # pylint: disable=W0201

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 is not None and next_element.tag == 'solution':
solution_element = next_element
elif next_element is not None and 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.do_targeted_feedback(self.tree)
html = contextualize_text(etree.tostring(self._extract_html(self.tree)), self.context)
return html

Expand Down
40 changes: 40 additions & 0 deletions common/lib/capa/capa/customrender.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import logging
import re

from cgi import escape as cgi_escape
from lxml import etree
import xml.sax.saxutils as saxutils
from .registry import TagRegistry
Expand Down Expand Up @@ -98,3 +99,42 @@ 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>{}</span></section>'.format(etree.tostring(self.xml))
try:
xhtml = etree.XML(html)
except Exception as err: # pylint: disable=broad-except
if self.system.DEBUG:
msg = """
<html>
<div class="inline-error">
<p>Error {err}</p>
<p>Failed to construct targeted feedback from <pre>{html}</pre></p>
</div>
</html>
""".format(err=cgi_escape(err), html=cgi_escape(html))
log.error(msg)
return etree.XML(msg)
else:
raise
return xhtml

registry.register(TargetedFeedbackRenderer)
Loading