Merge pull request #3677 from edx/nick/fix-feedback-bug
Fix targeted-feedback bug
This commit is contained in:
@@ -429,16 +429,16 @@ class LoncapaProblem(object):
|
||||
|
||||
def do_targeted_feedback(self, tree):
|
||||
"""
|
||||
Implements the targeted-feedback=N in-place on <multiplechoiceresponse> --
|
||||
Implements targeted-feedback 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
|
||||
# Note that the modifications has been done, avoiding problems if called twice.
|
||||
if hasattr(self, 'has_targeted'):
|
||||
return
|
||||
self.has_targeted = True # pylint: disable=W0201
|
||||
|
||||
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)
|
||||
|
||||
@@ -57,3 +57,15 @@ def test_capa_system():
|
||||
def new_loncapa_problem(xml, capa_system=None, seed=723):
|
||||
"""Construct a `LoncapaProblem` suitable for unit tests."""
|
||||
return LoncapaProblem(xml, id='1', seed=seed, capa_system=capa_system or test_capa_system())
|
||||
|
||||
|
||||
def load_fixture(relpath):
|
||||
"""
|
||||
Return a `unicode` object representing the contents
|
||||
of the fixture file at the given path within a test_files directory
|
||||
in the same directory as the test file.
|
||||
"""
|
||||
abspath = os.path.join(os.path.dirname(__file__), 'test_files', relpath)
|
||||
with open(abspath) as fixture_file:
|
||||
contents = fixture_file.read()
|
||||
return contents.decode('utf8')
|
||||
|
||||
50
common/lib/capa/capa/tests/test_files/targeted_feedback.xml
Normal file
50
common/lib/capa/capa/tests/test_files/targeted_feedback.xml
Normal file
@@ -0,0 +1,50 @@
|
||||
<problem>
|
||||
<p>What is the correct answer?</p>
|
||||
<multiplechoiceresponse targeted-feedback="">
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false" explanation-id="feedback1">wrong-1</choice>
|
||||
<choice correct="false" explanation-id="feedback2">wrong-2</choice>
|
||||
<choice correct="true" explanation-id="feedbackC">correct-1</choice>
|
||||
<choice correct="false" explanation-id="feedback3">wrong-3</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<targetedfeedbackset>
|
||||
<targetedfeedback explanation-id="feedback1">
|
||||
<div class="detailed-targeted-feedback">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>This is the 1st WRONG solution</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
<targetedfeedback explanation-id="feedback2">
|
||||
<div class="detailed-targeted-feedback">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>This is the 2nd WRONG solution</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
<targetedfeedback explanation-id="feedback3">
|
||||
<div class="detailed-targeted-feedback">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>This is the 3rd WRONG solution</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
<targetedfeedback explanation-id="feedbackC">
|
||||
<div class="detailed-targeted-feedback-correct">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>Feedback on your correct solution...</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
</targetedfeedbackset>
|
||||
|
||||
<solution explanation-id="feedbackC">
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>This is the solution explanation</p>
|
||||
<p>Not much to explain here, sorry!</p>
|
||||
</div>
|
||||
</solution>
|
||||
</problem>
|
||||
@@ -0,0 +1,91 @@
|
||||
<problem>
|
||||
<p>Q1</p>
|
||||
<multiplechoiceresponse targeted-feedback="">
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false" explanation-id="feedback1">wrong-1</choice>
|
||||
<choice correct="false" explanation-id="feedback2">wrong-2</choice>
|
||||
<choice correct="true" explanation-id="feedbackC">correct-1</choice>
|
||||
<choice correct="false" explanation-id="feedback3">wrong-3</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<targetedfeedbackset>
|
||||
<targetedfeedback explanation-id="feedback1">
|
||||
<div class="detailed-targeted-feedback">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>This is the 1st WRONG solution</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
<targetedfeedback explanation-id="feedback3">
|
||||
<div class="detailed-targeted-feedback">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>This is the 3rd WRONG solution</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
<targetedfeedback explanation-id="feedbackC">
|
||||
<div class="detailed-targeted-feedback-correct">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>Feedback on your correct solution...</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
</targetedfeedbackset>
|
||||
|
||||
<solutionset>
|
||||
<solution explanation-id="feedbackC">
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>This is the solution explanation</p>
|
||||
<p>Not much to explain here, sorry!</p>
|
||||
</div>
|
||||
</solution>
|
||||
</solutionset>
|
||||
|
||||
<hr/>
|
||||
|
||||
<p>Q2</p>
|
||||
<multiplechoiceresponse targeted-feedback="">
|
||||
<choicegroup type="MultipleChoice" answer-pool="3">
|
||||
<choice correct="false" explanation-id="feedback1">wrong-1</choice>
|
||||
<choice correct="false" explanation-id="feedback2">wrong-2</choice>
|
||||
<choice correct="true" explanation-id="feedbackC">correct-1</choice>
|
||||
<choice correct="false" explanation-id="feedback3">wrong-3</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<targetedfeedbackset>
|
||||
<targetedfeedback explanation-id="feedback1">
|
||||
<div class="detailed-targeted-feedback">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>This is the 1st WRONG solution</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
<targetedfeedback explanation-id="feedback3">
|
||||
<div class="detailed-targeted-feedback">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>This is the 3rd WRONG solution</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
<targetedfeedback explanation-id="feedbackC">
|
||||
<div class="detailed-targeted-feedback-correct">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>Feedback on your correct solution...</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
</targetedfeedbackset>
|
||||
|
||||
<solutionset>
|
||||
<solution explanation-id="feedbackC">
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>This is the solution explanation</p>
|
||||
<p>Not much to explain here, sorry!</p>
|
||||
</div>
|
||||
</solution>
|
||||
</solutionset>
|
||||
</problem>
|
||||
@@ -13,7 +13,7 @@ import textwrap
|
||||
import requests
|
||||
import mock
|
||||
|
||||
from . import new_loncapa_problem, test_capa_system
|
||||
from . import new_loncapa_problem, test_capa_system, load_fixture
|
||||
import calc
|
||||
|
||||
from capa.responsetypes import LoncapaProblemError, \
|
||||
@@ -224,7 +224,7 @@ class SymbolicResponseTest(ResponseTest):
|
||||
|
||||
for (input_str, input_mathml, server_fixture) in correct_inputs:
|
||||
print "Testing input: {0}".format(input_str)
|
||||
server_resp = self._load_fixture(server_fixture)
|
||||
server_resp = load_fixture(server_fixture)
|
||||
self._assert_symbolic_grade(
|
||||
problem, input_str, input_mathml,
|
||||
'correct', snuggletex_resp=server_resp
|
||||
@@ -253,8 +253,8 @@ class SymbolicResponseTest(ResponseTest):
|
||||
options=["matrix", "imaginary"]
|
||||
)
|
||||
|
||||
correct_snuggletex = self._load_fixture('snuggletex_correct.html')
|
||||
dynamath_input = self._load_fixture('dynamath_input.txt')
|
||||
correct_snuggletex = load_fixture('snuggletex_correct.html')
|
||||
dynamath_input = load_fixture('dynamath_input.txt')
|
||||
student_response = "cos(theta)*[[1,0],[0,1]] + i*sin(theta)*[[0,1],[1,0]]"
|
||||
|
||||
self._assert_symbolic_grade(
|
||||
@@ -269,7 +269,7 @@ class SymbolicResponseTest(ResponseTest):
|
||||
expect="[[cos(theta),i*sin(theta)],[i*sin(theta),cos(theta)]]",
|
||||
options=["matrix", "imaginary"])
|
||||
|
||||
wrong_snuggletex = self._load_fixture('snuggletex_wrong.html')
|
||||
wrong_snuggletex = load_fixture('snuggletex_wrong.html')
|
||||
dynamath_input = textwrap.dedent("""
|
||||
<math xmlns="http://www.w3.org/1998/Math/MathML">
|
||||
<mstyle displaystyle="true"><mn>2</mn></mstyle>
|
||||
@@ -315,18 +315,6 @@ class SymbolicResponseTest(ResponseTest):
|
||||
correct_map.get_correctness('1_2_1'), expected_correctness
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _load_fixture(relpath):
|
||||
"""
|
||||
Return a `unicode` object representing the contents
|
||||
of the fixture file at `relpath` (relative to the test files dir)
|
||||
"""
|
||||
abspath = os.path.join(os.path.dirname(__file__), 'test_files', relpath)
|
||||
with open(abspath) as fixture_file:
|
||||
contents = fixture_file.read()
|
||||
|
||||
return contents.decode('utf8')
|
||||
|
||||
|
||||
class OptionResponseTest(ResponseTest):
|
||||
from capa.tests.response_xml_factory import OptionResponseXMLFactory
|
||||
|
||||
@@ -5,7 +5,7 @@ i.e. those with the <multiplechoiceresponse> element
|
||||
|
||||
import unittest
|
||||
import textwrap
|
||||
from . import test_capa_system, new_loncapa_problem
|
||||
from . import test_capa_system, new_loncapa_problem, load_fixture
|
||||
|
||||
|
||||
class CapaTargetedFeedbackTest(unittest.TestCase):
|
||||
@@ -80,62 +80,8 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
|
||||
self.assertRegexpMatches(without_new_lines, r"<div>.*'wrong-1'.*'wrong-2'.*'correct-1'.*'wrong-3'.*</div>")
|
||||
self.assertRegexpMatches(without_new_lines, r"feedback1|feedback2|feedback3|feedbackC")
|
||||
|
||||
# A targeted-feedback problem shared for a few tests
|
||||
common_targeted_xml = textwrap.dedent("""
|
||||
<problem>
|
||||
<p>What is the correct answer?</p>
|
||||
<multiplechoiceresponse targeted-feedback="">
|
||||
<choicegroup type="MultipleChoice">
|
||||
<choice correct="false" explanation-id="feedback1">wrong-1</choice>
|
||||
<choice correct="false" explanation-id="feedback2">wrong-2</choice>
|
||||
<choice correct="true" explanation-id="feedbackC">correct-1</choice>
|
||||
<choice correct="false" explanation-id="feedback3">wrong-3</choice>
|
||||
</choicegroup>
|
||||
</multiplechoiceresponse>
|
||||
|
||||
<targetedfeedbackset>
|
||||
<targetedfeedback explanation-id="feedback1">
|
||||
<div class="detailed-targeted-feedback">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>This is the 1st WRONG solution</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
<targetedfeedback explanation-id="feedback2">
|
||||
<div class="detailed-targeted-feedback">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>This is the 2nd WRONG solution</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
<targetedfeedback explanation-id="feedback3">
|
||||
<div class="detailed-targeted-feedback">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>This is the 3rd WRONG solution</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
<targetedfeedback explanation-id="feedbackC">
|
||||
<div class="detailed-targeted-feedback-correct">
|
||||
<p>Targeted Feedback</p>
|
||||
<p>Feedback on your correct solution...</p>
|
||||
</div>
|
||||
</targetedfeedback>
|
||||
|
||||
</targetedfeedbackset>
|
||||
|
||||
<solution explanation-id="feedbackC">
|
||||
<div class="detailed-solution">
|
||||
<p>Explanation</p>
|
||||
<p>This is the solution explanation</p>
|
||||
<p>Not much to explain here, sorry!</p>
|
||||
</div>
|
||||
</solution>
|
||||
</problem>
|
||||
""")
|
||||
|
||||
def test_targeted_feedback_not_finished(self):
|
||||
problem = new_loncapa_problem(self.common_targeted_xml)
|
||||
problem = new_loncapa_problem(load_fixture('targeted_feedback.xml'))
|
||||
the_html = problem.get_html()
|
||||
without_new_lines = the_html.replace("\n", "")
|
||||
|
||||
@@ -144,7 +90,7 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
|
||||
self.assertEquals(the_html, problem.get_html(), "Should be able to call get_html() twice")
|
||||
|
||||
def test_targeted_feedback_student_answer1(self):
|
||||
problem = new_loncapa_problem(self.common_targeted_xml)
|
||||
problem = new_loncapa_problem(load_fixture('targeted_feedback.xml'))
|
||||
problem.done = True
|
||||
problem.student_answers = {'1_2_1': 'choice_3'}
|
||||
|
||||
@@ -158,7 +104,7 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
|
||||
self.assertEquals(the_html, the_html2)
|
||||
|
||||
def test_targeted_feedback_student_answer2(self):
|
||||
problem = new_loncapa_problem(self.common_targeted_xml)
|
||||
problem = new_loncapa_problem(load_fixture('targeted_feedback.xml'))
|
||||
problem.done = True
|
||||
problem.student_answers = {'1_2_1': 'choice_0'}
|
||||
|
||||
@@ -611,3 +557,41 @@ class CapaTargetedFeedbackTest(unittest.TestCase):
|
||||
self.assertNotRegexpMatches(without_new_lines, r"<targetedfeedback explanation-id=\"feedbackC\".*solution explanation")
|
||||
self.assertRegexpMatches(without_new_lines, r"<div>\{.*'1_solution_1'.*\}</div>")
|
||||
self.assertNotRegexpMatches(without_new_lines, r"feedback1|feedback3|feedbackC")
|
||||
|
||||
def test_targeted_feedback_multiple_not_answered(self):
|
||||
# Not answered -> empty targeted feedback
|
||||
problem = new_loncapa_problem(load_fixture('targeted_feedback_multiple.xml'))
|
||||
the_html = problem.get_html()
|
||||
without_new_lines = the_html.replace("\n", "")
|
||||
# Q1 and Q2 have no feedback
|
||||
self.assertRegexpMatches(
|
||||
without_new_lines,
|
||||
r'<targetedfeedbackset.*?>\s*</targetedfeedbackset>.*' +
|
||||
r'<targetedfeedbackset.*?>\s*</targetedfeedbackset>'
|
||||
)
|
||||
|
||||
def test_targeted_feedback_multiple_answer_1(self):
|
||||
problem = new_loncapa_problem(load_fixture('targeted_feedback_multiple.xml'))
|
||||
problem.done = True
|
||||
problem.student_answers = {'1_2_1': 'choice_0'} # feedback1
|
||||
the_html = problem.get_html()
|
||||
without_new_lines = the_html.replace("\n", "")
|
||||
# Q1 has feedback1 and Q2 has nothing
|
||||
self.assertRegexpMatches(
|
||||
without_new_lines,
|
||||
r'<targetedfeedbackset.*?>.*?explanation-id="feedback1".*?</targetedfeedbackset>.*' +
|
||||
r'<targetedfeedbackset.*?>\s*</targetedfeedbackset>'
|
||||
)
|
||||
|
||||
def test_targeted_feedback_multiple_answer_2(self):
|
||||
problem = new_loncapa_problem(load_fixture('targeted_feedback_multiple.xml'))
|
||||
problem.done = True
|
||||
problem.student_answers = {'1_2_1': 'choice_0', '1_3_1': 'mask_1'} # Q1 wrong, Q2 correct
|
||||
the_html = problem.get_html()
|
||||
without_new_lines = the_html.replace("\n", "")
|
||||
# Q1 has feedback1 and Q2 has feedbackC
|
||||
self.assertRegexpMatches(
|
||||
without_new_lines,
|
||||
r'<targetedfeedbackset.*?>.*?explanation-id="feedback1".*?</targetedfeedbackset>.*' +
|
||||
r'<targetedfeedbackset.*?>.*explanation-id="feedbackC".*?</targetedfeedbackset>'
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user