Merge pull request #22954 from edx/dsheraz/PROD-347

add multiple correct values check for Dropdown problems
This commit is contained in:
Syed Muhammad Dawoud Sheraz Ali
2020-02-10 12:26:32 +05:00
committed by GitHub
6 changed files with 159 additions and 12 deletions

View File

@@ -245,6 +245,22 @@ class LoncapaProblem(object):
This translation takes in the new format and synthesizes the old option= attribute
so all downstream logic works unchanged with the new <option> tag format.
"""
def is_optioninput_valid(optioninput):
"""
Verifies if a given optioninput xml is valid or not.
A given optioninput(Dropdown) problem is invalid if it has more than one correct answer.
Argument:
optioninput: dropdown specification tree
Returns:
boolean: signifying if the optioninput is valid or not.
"""
correct_options = [
option.get('correct').upper() == 'TRUE' for option in optioninput.findall('./option')
]
return correct_options.count(True) in (0, 1)
additionals = tree.xpath('//stringresponse/additional_answer')
for additional in additionals:
answer = additional.get('answer')
@@ -252,8 +268,9 @@ class LoncapaProblem(object):
if not answer and text: # trigger of old->new conversion
additional.set('answer', text)
additional.text = ''
for optioninput in tree.xpath('//optioninput'):
if not is_optioninput_valid(optioninput):
raise responsetypes.LoncapaProblemError("Dropdown questions can only have one correct answer.")
correct_option = None
child_options = []
for option_element in optioninput.findall('./option'):

View File

@@ -12,6 +12,7 @@ from lxml import etree
from markupsafe import Markup
from mock import patch
from capa.responsetypes import LoncapaProblemError
from capa.tests.helpers import new_loncapa_problem
from openedx.core.djangolib.markup import HTML
@@ -460,6 +461,37 @@ class CAPAProblemTest(unittest.TestCase):
self.assert_question_tag(question1, question2, tag='label', label_attr=False)
self.assert_question_tag(question1, question2, tag='p', label_attr=True)
def test_optionresponse_xml_compatibility(self):
"""
Verify that an optionresponse problem with multiple correct answers is not instantiated.
Scenario:
Given an optionresponse/Dropdown problem
If there are multiple correct answers
Then the problem is not instantiated
And Loncapa problem error exception is raised
If the problem is corrected by including only one correct answer
Then the problem is created successfully
"""
xml = """
<problem>
<optionresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<optioninput>
<option correct="False">an incorrect answer</option>
<option correct="True">the correct answer</option>
<option correct="{correctness}">an incorrect answer</option>
</optioninput>
</optionresponse>
</problem>
"""
with self.assertRaises(LoncapaProblemError):
new_loncapa_problem(xml.format(correctness=True))
problem = new_loncapa_problem(xml.format(correctness=False))
self.assertIsNotNone(problem)
@ddt.ddt
class CAPAMultiInputProblemTest(unittest.TestCase):

View File

@@ -344,7 +344,7 @@ class ProblemBlock(
def max_score(self):
"""
Return the problem's max score
Return the problem's max score if problem is instantiated successfully, else return max score of 0.
"""
from capa.capa_problem import LoncapaProblem, LoncapaSystem
capa_system = LoncapaSystem(
@@ -363,16 +363,22 @@ class ProblemBlock(
xqueue=None,
matlab_api_key=None,
)
lcp = LoncapaProblem(
problem_text=self.data,
id=self.location.html_id(),
capa_system=capa_system,
capa_module=self,
state={},
seed=1,
minimal_init=True,
)
return lcp.get_max_score()
try:
lcp = LoncapaProblem(
problem_text=self.data,
id=self.location.html_id(),
capa_system=capa_system,
capa_module=self,
state={},
seed=1,
minimal_init=True,
)
except responsetypes.LoncapaProblemError:
log.exception(u"LcpFatalError for block {} while getting max score".format(str(self.location)))
maximum_score = 0
else:
maximum_score = lcp.get_max_score()
return maximum_score
def generate_report_data(self, user_state_iterator, limit_responses=None):
"""

View File

@@ -2871,6 +2871,27 @@ class ProblemBlockXMLTest(unittest.TestCase):
with self.assertRaises(etree.XMLSyntaxError):
self._create_descriptor(sample_invalid_xml, name="Invalid XML")
def test_invalid_dropdown_xml(self):
"""
Verify the capa problem cannot be created from dropdown xml with multiple correct answers.
"""
problem_xml = textwrap.dedent("""
<problem>
<optionresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<optioninput>
<option correct="False">an incorrect answer</option>
<option correct="True">the correct answer</option>
<option correct="True">an incorrect answer</option>
</optioninput>
</optionresponse>
</problem>
""")
with self.assertRaises(Exception):
CapaFactory.create(xml=problem_xml)
class ComplexEncoderTest(unittest.TestCase):

View File

@@ -6,6 +6,7 @@ Test for lms courseware app, module render unit
import itertools
import json
import textwrap
from datetime import datetime
from functools import partial
@@ -1880,6 +1881,47 @@ class TestStaffDebugInfo(SharedModuleStoreTestCase):
result_fragment = module.render(STUDENT_VIEW)
self.assertIn('Staff Debug', result_fragment.content)
def test_staff_debug_info_score_for_invalid_dropdown(self):
"""
Verifies that for an invalid drop down problem, the max score is set
to zero in the html.
"""
problem_xml = """
<problem>
<optionresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<optioninput>
<option correct="False">an incorrect answer</option>
<option correct="True">the correct answer</option>
<option correct="True">an incorrect answer</option>
</optioninput>
</optionresponse>
</problem>
"""
problem_descriptor = ItemFactory.create(
category='problem',
data=problem_xml
)
module = render.get_module(
self.user,
self.request,
problem_descriptor.location,
self.field_data_cache
)
html_fragment = module.render(STUDENT_VIEW)
expected_score_override_html = textwrap.dedent("""<div>
<label for="sd_fs_{block_id}">Score (for override only):</label>
<input type="text" tabindex="0" id="sd_fs_{block_id}" placeholder="0"/>
<label for="sd_fs_{block_id}"> / 0</label>
</div>""")
self.assertIn(
expected_score_override_html.format(block_id=problem_descriptor.location.block_id),
html_fragment.content
)
@XBlock.register_temp_plugin(DetachedXBlock, identifier='detached-block')
def test_staff_debug_info_disabled_for_detached_blocks(self):
"""Staff markup should not be present on detached blocks."""

View File

@@ -347,6 +347,35 @@ class GradesTransformerTestCase(CourseStructureTestCase):
max_score=2,
)
def test_max_score_for_invalid_dropdown_problem(self):
"""
Verify that for an invalid dropdown problem, the max score is set to zero.
"""
problem_data = u'''
<problem>
<optionresponse>
<p>You can use this template as a guide to the simple editor markdown and OLX markup to use for dropdown problems. Edit this component to replace this template with your own assessment.</p>
<label>Add the question text, or prompt, here. This text is required.</label>
<description>You can add an optional tip or note related to the prompt like this. </description>
<optioninput>
<option correct="False">an incorrect answer</option>
<option correct="True">the correct answer</option>
<option correct="True">an incorrect answer</option>
</optioninput>
</optionresponse>
</problem>
'''
blocks = self.build_course_with_problems(problem_data)
block_structure = get_course_blocks(self.student, blocks['course'].location, self.transformers)
self.assert_collected_transformer_block_fields(
block_structure,
blocks['problem'].location,
self.TRANSFORMER_CLASS_TO_TEST,
max_score=0,
)
def test_course_version_not_collected_in_old_mongo(self):
blocks = self.build_course_with_problems()
block_structure = get_course_blocks(self.student, blocks[u'course'].location, self.transformers)