Add support for additional answers for Numerical Input problems
TNL-5581
This commit is contained in:
@@ -1710,6 +1710,8 @@ class NumericalResponse(LoncapaResponse):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.correct_answer = ''
|
||||
self.additional_answers = []
|
||||
self.additional_answer_index = -1
|
||||
self.tolerance = default_tolerance
|
||||
self.range_tolerance = False
|
||||
self.answer_range = self.inclusion = None
|
||||
@@ -1720,6 +1722,10 @@ class NumericalResponse(LoncapaResponse):
|
||||
context = self.context
|
||||
answer = xml.get('answer')
|
||||
|
||||
self.additional_answers = (
|
||||
[element.get('answer') for element in xml.findall('additional_answer')]
|
||||
)
|
||||
|
||||
if answer.startswith(('[', '(')) and answer.endswith((']', ')')): # range tolerance case
|
||||
self.range_tolerance = True
|
||||
self.inclusion = (
|
||||
@@ -1930,6 +1936,20 @@ class NumericalResponse(LoncapaResponse):
|
||||
if compare_with_tolerance(student_float, correct_float, expanded_tolerance):
|
||||
is_correct = 'partially-correct'
|
||||
|
||||
# Reset self.additional_answer_index to -1 so that we always have a fresh index to look up.
|
||||
self.additional_answer_index = -1
|
||||
|
||||
# Compare with additional answers.
|
||||
if is_correct == 'incorrect':
|
||||
temp_additional_answer_idx = 0
|
||||
for additional_answer in self.additional_answers:
|
||||
staff_answer = self.get_staff_ans(additional_answer)
|
||||
if complex(student_float) == staff_answer:
|
||||
is_correct = 'correct'
|
||||
self.additional_answer_index = temp_additional_answer_idx
|
||||
break
|
||||
temp_additional_answer_idx += 1
|
||||
|
||||
if is_correct == 'partially-correct':
|
||||
return CorrectMap(self.answer_id, is_correct, npoints=partial_score)
|
||||
else:
|
||||
@@ -1957,7 +1977,38 @@ class NumericalResponse(LoncapaResponse):
|
||||
return False
|
||||
|
||||
def get_answers(self):
|
||||
return {self.answer_id: self.correct_answer}
|
||||
_ = self.capa_system.i18n.ugettext
|
||||
# Example: "Answer: Answer_1 or Answer_2 or Answer_3".
|
||||
separator = Text(' {b_start}{or_separator}{b_end} ').format(
|
||||
# Translators: Separator used in NumericalResponse to display multiple answers.
|
||||
or_separator=_('or'),
|
||||
b_start=HTML('<b>'),
|
||||
b_end=HTML('</b>'),
|
||||
)
|
||||
return {self.answer_id: separator.join([self.correct_answer] + self.additional_answers)}
|
||||
|
||||
def set_cmap_msg(self, student_answers, new_cmap, hint_type, hint_index):
|
||||
"""
|
||||
Sets feedback to correct hint node in correct map.
|
||||
|
||||
Arguments:
|
||||
student_answers (dict): Dict containing student input.
|
||||
new_cmap (dict): Dict containing correct map properties.
|
||||
hint_type (str): Hint type, either `correcthint` or `additional_answer`
|
||||
hint_index (int): Index of the hint node
|
||||
"""
|
||||
# Note: using self.id here, not the more typical self.answer_id
|
||||
hint_nodes = self.xml.xpath('//numericalresponse[@id=$id]/' + hint_type, id=self.id)
|
||||
if hint_nodes:
|
||||
hint_node = hint_nodes[hint_index]
|
||||
if hint_type == 'additional_answer':
|
||||
hint_node = hint_nodes[hint_index].find('./correcthint')
|
||||
new_cmap[self.answer_id]['msg'] += self.make_hint_div(
|
||||
hint_node,
|
||||
True,
|
||||
[student_answers[self.answer_id]],
|
||||
self.tags[0]
|
||||
)
|
||||
|
||||
def get_extended_hints(self, student_answers, new_cmap):
|
||||
"""
|
||||
@@ -1966,16 +2017,11 @@ class NumericalResponse(LoncapaResponse):
|
||||
"""
|
||||
if self.answer_id in student_answers:
|
||||
if new_cmap.cmap[self.answer_id]['correctness'] == 'correct': # if the grader liked the student's answer
|
||||
# Note: using self.id here, not the more typical self.answer_id
|
||||
hints = self.xml.xpath('//numericalresponse[@id=$id]/correcthint', id=self.id)
|
||||
if hints:
|
||||
hint_node = hints[0]
|
||||
new_cmap[self.answer_id]['msg'] += self.make_hint_div(
|
||||
hint_node,
|
||||
True,
|
||||
[student_answers[self.answer_id]],
|
||||
self.tags[0]
|
||||
)
|
||||
# Answer is not an additional answer.
|
||||
if self.additional_answer_index == -1:
|
||||
self.set_cmap_msg(student_answers, new_cmap, 'correcthint', 0)
|
||||
else:
|
||||
self.set_cmap_msg(student_answers, new_cmap, 'additional_answer', self.additional_answer_index)
|
||||
|
||||
#-----------------------------------------------------------------------------
|
||||
|
||||
|
||||
@@ -204,6 +204,10 @@ class NumericalResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
*answer*: The correct answer (e.g. "5")
|
||||
|
||||
*correcthint*: The feedback describing correct answer.
|
||||
|
||||
*additional_answers*: A dict of additional answers along with their correcthint.
|
||||
|
||||
*tolerance*: The tolerance within which a response
|
||||
is considered correct. Can be a decimal (e.g. "0.01")
|
||||
or percentage (e.g. "2%")
|
||||
@@ -219,6 +223,8 @@ class NumericalResponseXMLFactory(ResponseXMLFactory):
|
||||
"""
|
||||
|
||||
answer = kwargs.get('answer', None)
|
||||
correcthint = kwargs.get('correcthint', '')
|
||||
additional_answers = kwargs.get('additional_answers', {})
|
||||
tolerance = kwargs.get('tolerance', None)
|
||||
credit_type = kwargs.get('credit_type', None)
|
||||
partial_range = kwargs.get('partial_range', None)
|
||||
@@ -232,6 +238,13 @@ class NumericalResponseXMLFactory(ResponseXMLFactory):
|
||||
else:
|
||||
response_element.set('answer', str(answer))
|
||||
|
||||
for additional_answer, additional_correcthint in additional_answers.items():
|
||||
additional_element = etree.SubElement(response_element, 'additional_answer')
|
||||
additional_element.set('answer', str(additional_answer))
|
||||
if additional_correcthint:
|
||||
correcthint_element = etree.SubElement(additional_element, 'correcthint')
|
||||
correcthint_element.text = str(additional_correcthint)
|
||||
|
||||
if tolerance:
|
||||
responseparam_element = etree.SubElement(response_element, 'responseparam')
|
||||
responseparam_element.set('type', 'tolerance')
|
||||
@@ -244,6 +257,10 @@ class NumericalResponseXMLFactory(ResponseXMLFactory):
|
||||
responseparam_element = etree.SubElement(response_element, 'responseparam')
|
||||
responseparam_element.set('partial_answers', partial_answers)
|
||||
|
||||
if correcthint:
|
||||
correcthint_element = etree.SubElement(response_element, 'correcthint')
|
||||
correcthint_element.text = str(correcthint)
|
||||
|
||||
return response_element
|
||||
|
||||
def create_input_element(self, **kwargs):
|
||||
@@ -732,7 +749,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
|
||||
|
||||
*regexp*: Whether the response is regexp
|
||||
|
||||
*additional_answers*: list of additional asnwers.
|
||||
*additional_answers*: list of additional answers.
|
||||
|
||||
*non_attribute_answers*: list of additional answers to be coded in the
|
||||
non-attribute format
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<label>What value when squared is approximately equal to 2 (give your answer to 2 decimal places)?</label>
|
||||
<responseparam default=".01" type="tolerance"/>
|
||||
<formulaequationinput/>
|
||||
<additional_answer answer="10">
|
||||
<correcthint>This is an additional hint.</correcthint>
|
||||
</additional_answer>
|
||||
|
||||
<correcthint label="Nice">
|
||||
The square root of two turns up in the strangest places.
|
||||
|
||||
@@ -236,6 +236,9 @@ class NumericInputHintsTest(HintTest):
|
||||
@data(
|
||||
{'problem_id': u'1_2_1', 'choice': '1.141',
|
||||
'expected_string': u'<div class="feedback-hint-correct"><div class="explanation-title">Answer</div><span class="hint-label">Nice </span><div class="hint-text">The square root of two turns up in the strangest places.</div></div>'},
|
||||
# additional answer
|
||||
{'problem_id': u'1_2_1', 'choice': '10',
|
||||
'expected_string': u'<div class="feedback-hint-correct"><div class="explanation-title">Answer</div><span class="hint-label">Correct: </span><div class="hint-text">This is an additional hint.</div></div>'},
|
||||
{'problem_id': u'1_3_1', 'choice': '4',
|
||||
'expected_string': u'<div class="feedback-hint-correct"><div class="explanation-title">Answer</div><span class="hint-label">Correct: </span><div class="hint-text">Pretty easy, uh?.</div></div>'},
|
||||
# should get hint, when correct via numeric-tolerance
|
||||
|
||||
@@ -1390,7 +1390,7 @@ class NumericalResponseTest(ResponseTest): # pylint: disable=missing-docstring
|
||||
# For simple things its not worth the effort.
|
||||
def test_grade_range_tolerance(self):
|
||||
problem_setup = [
|
||||
# [given_asnwer, [list of correct responses], [list of incorrect responses]]
|
||||
# [given_answer, [list of correct responses], [list of incorrect responses]]
|
||||
['[5, 7)', ['5', '6', '6.999'], ['4.999', '7']],
|
||||
['[1.6e-5, 1.9e24)', ['0.000016', '1.6*10^-5', '1.59e24'], ['1.59e-5', '1.9e24', '1.9*10^24']],
|
||||
['[0, 1.6e-5]', ['1.6*10^-5'], ["2"]],
|
||||
@@ -1400,6 +1400,54 @@ class NumericalResponseTest(ResponseTest): # pylint: disable=missing-docstring
|
||||
problem = self.build_problem(answer=given_answer)
|
||||
self.assert_multiple_grade(problem, correct_responses, incorrect_responses)
|
||||
|
||||
def test_additional_answer_grading(self):
|
||||
"""
|
||||
Test additional answers are graded correct with their associated correcthint.
|
||||
"""
|
||||
primary_answer = '100'
|
||||
primary_correcthint = 'primary feedback'
|
||||
additional_answers = {
|
||||
'1': '1. additional feedback',
|
||||
'2': '2. additional feedback',
|
||||
'4': '4. additional feedback',
|
||||
'5': ''
|
||||
}
|
||||
problem = self.build_problem(
|
||||
answer=primary_answer,
|
||||
additional_answers=additional_answers,
|
||||
correcthint=primary_correcthint
|
||||
)
|
||||
|
||||
# Assert primary answer is graded correctly.
|
||||
correct_map = problem.grade_answers({'1_2_1': primary_answer})
|
||||
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
|
||||
self.assertIn(primary_correcthint, correct_map.get_msg('1_2_1'))
|
||||
|
||||
# Assert additional answers are graded correct
|
||||
for answer, correcthint in additional_answers.items():
|
||||
correct_map = problem.grade_answers({'1_2_1': answer})
|
||||
self.assertEqual(correct_map.get_correctness('1_2_1'), 'correct')
|
||||
self.assertIn(correcthint, correct_map.get_msg('1_2_1'))
|
||||
|
||||
def test_additional_answer_get_score(self):
|
||||
"""
|
||||
Test `get_score` is working for additional answers.
|
||||
"""
|
||||
problem = self.build_problem(answer='100', additional_answers={'1': ''})
|
||||
responder = problem.responders.values()[0]
|
||||
|
||||
# Check primary answer.
|
||||
new_cmap = responder.get_score({'1_2_1': '100'})
|
||||
self.assertEqual(new_cmap.get_correctness('1_2_1'), 'correct')
|
||||
|
||||
# Check additional answer.
|
||||
new_cmap = responder.get_score({'1_2_1': '1'})
|
||||
self.assertEqual(new_cmap.get_correctness('1_2_1'), 'correct')
|
||||
|
||||
# Check any wrong answer.
|
||||
new_cmap = responder.get_score({'1_2_1': '2'})
|
||||
self.assertEqual(new_cmap.get_correctness('1_2_1'), 'incorrect')
|
||||
|
||||
def test_grade_range_tolerance_partial_credit(self):
|
||||
problem_setup = [
|
||||
# [given_answer,
|
||||
|
||||
@@ -187,21 +187,80 @@ describe 'MarkdownEditingDescriptor', ->
|
||||
|
||||
|
||||
</problem>""")
|
||||
it 'markup with multiple answers doesn\'t break numerical response', ->
|
||||
it 'markup with additional answer does not break numerical response', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Enter 1 with a tolerance:
|
||||
= 1 +- .02
|
||||
or= 2 +- 5%
|
||||
or= 2
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<numericalresponse answer="1">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="2"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>"""
|
||||
)
|
||||
it 'markup for numerical with multiple additional answers renders correctly', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Enter 1 with a tolerance:
|
||||
= 1 +- .02
|
||||
or= 2
|
||||
or= 3
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<numericalresponse answer="1">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="2"/>
|
||||
<additional_answer answer="3"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>""")
|
||||
</problem>"""
|
||||
)
|
||||
it 'Do not render ranged/tolerance/alphabetical additional answers for numerical response', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Enter 1 with a tolerance:
|
||||
= 1 +- .02
|
||||
or= 2
|
||||
or= 3 +- 0.1
|
||||
or= [4,6]
|
||||
or= ABC
|
||||
or= 7
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<numericalresponse answer="1">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="2"/>
|
||||
<additional_answer answer="7"/>
|
||||
<formulaequationinput/>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>"""
|
||||
)
|
||||
it 'markup with feedback renders correctly in additional answer for numerical response', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""
|
||||
Enter 1 with a tolerance:
|
||||
= 100 +- .02 {{ main feedback }}
|
||||
or= 10 {{ additional feedback }}
|
||||
""")
|
||||
expect(data).toXMLEqual("""<problem>
|
||||
<numericalresponse answer="100">
|
||||
<p>Enter 1 with a tolerance:</p>
|
||||
<responseparam type="tolerance" default=".02"/>
|
||||
<additional_answer answer="10">
|
||||
<correcthint>additional feedback</correcthint>
|
||||
</additional_answer>
|
||||
<formulaequationinput/>
|
||||
<correcthint>main feedback</correcthint>
|
||||
</numericalresponse>
|
||||
|
||||
</problem>"""
|
||||
)
|
||||
it 'converts multiple choice to xml', ->
|
||||
data = MarkdownEditingDescriptor.markdownToXml("""A multiple choice problem presents radio buttons for student input. Students can only select a single option presented. Multiple Choice questions have been the subject of many areas of research due to the early invention and adoption of bubble sheets.
|
||||
|
||||
|
||||
@@ -567,50 +567,108 @@
|
||||
// Line split here, trim off leading xxx= in each function
|
||||
var answersList = p.split('\n'),
|
||||
|
||||
processNumericalResponse = function(val) {
|
||||
var params, answer, string, textHint, hintLine, value;
|
||||
// Numeric case is just a plain leading = with a single answer
|
||||
value = val.replace(/^\=\s*/, '');
|
||||
isRangeToleranceCase = function(answer) {
|
||||
return _.contains(
|
||||
['[', '('], answer[0]) && _.contains([']', ')'], answer[answer.length - 1]
|
||||
);
|
||||
},
|
||||
|
||||
textHint = extractHint(value);
|
||||
hintLine = '';
|
||||
if (textHint.hint) {
|
||||
value = textHint.nothint;
|
||||
hintLine = ' <correcthint' + textHint.labelassign + '>' + textHint.hint +
|
||||
'</correcthint>\n';
|
||||
getAnswerData = function(answerValue) {
|
||||
var answerData = {},
|
||||
answerParams = /(.*?)\+\-\s*(.*?$)/.exec(answerValue);
|
||||
if (answerParams) {
|
||||
answerData.answer = answerParams[1].replace(/\s+/g, ''); // inputs like 5*2 +- 10
|
||||
answerData.default = answerParams[2];
|
||||
} else {
|
||||
answerData.answer = answerValue.replace(/\s+/g, ''); // inputs like 5*2
|
||||
}
|
||||
return answerData;
|
||||
},
|
||||
|
||||
if (_.contains(['[', '('], value[0]) && _.contains([']', ')'], value[value.length - 1])) {
|
||||
// [5, 7) or (5, 7), or (1.2345 * (2+3), 7*4 ] - range tolerance case
|
||||
// = (5*2)*3 should not be used as range tolerance
|
||||
string = '<numericalresponse answer="' + value + '">\n';
|
||||
string += ' <formulaequationinput />\n';
|
||||
string += hintLine;
|
||||
string += '</numericalresponse>\n\n';
|
||||
return string;
|
||||
}
|
||||
processNumericalResponse = function(answerValues) {
|
||||
var firstAnswer, answerData, numericalResponseString, additionalAnswerString,
|
||||
textHint, hintLine, additionalTextHint, additionalHintLine, orMatch, hasTolerance;
|
||||
|
||||
if (isNaN(parseFloat(value))) {
|
||||
// First string case is s?= [e.g. = 100]
|
||||
firstAnswer = answerValues[0].replace(/^\=\s*/, '');
|
||||
|
||||
// If answer is not numerical
|
||||
if (isNaN(parseFloat(firstAnswer)) && !isRangeToleranceCase(firstAnswer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Tries to extract parameters from string like 'expr +- tolerance'
|
||||
params = /(.*?)\+\-\s*(.*?$)/.exec(value);
|
||||
|
||||
if (params) {
|
||||
answer = params[1].replace(/\s+/g, ''); // support inputs like 5*2 +- 10
|
||||
string = '<numericalresponse answer="' + answer + '">\n';
|
||||
string += ' <responseparam type="tolerance" default="' + params[2] + '" />\n';
|
||||
} else {
|
||||
answer = value.replace(/\s+/g, ''); // support inputs like 5*2
|
||||
string = '<numericalresponse answer="' + answer + '">\n';
|
||||
textHint = extractHint(firstAnswer);
|
||||
hintLine = '';
|
||||
if (textHint.hint) {
|
||||
firstAnswer = textHint.nothint;
|
||||
// safe-lint: disable=javascript-concat-html
|
||||
hintLine = ' <correcthint' + textHint.labelassign + '>' +
|
||||
// safe-lint: disable=javascript-concat-html
|
||||
textHint.hint + '</correcthint>\n';
|
||||
}
|
||||
|
||||
string += ' <formulaequationinput />\n';
|
||||
string += hintLine;
|
||||
string += '</numericalresponse>\n\n';
|
||||
// Range case
|
||||
if (isRangeToleranceCase(firstAnswer)) {
|
||||
// [5, 7) or (5, 7), or (1.2345 * (2+3), 7*4 ] - range tolerance case
|
||||
// = (5*2)*3 should not be used as range tolerance
|
||||
// safe-lint: disable=javascript-concat-html
|
||||
numericalResponseString = '<numericalresponse answer="' + firstAnswer + '">\n';
|
||||
} else {
|
||||
answerData = getAnswerData(firstAnswer);
|
||||
// safe-lint: disable=javascript-concat-html
|
||||
numericalResponseString = '<numericalresponse answer="' + answerData.answer + '">\n';
|
||||
if (answerData.default) {
|
||||
// safe-lint: disable=javascript-concat-html
|
||||
numericalResponseString += ' <responseparam type="tolerance" default="' +
|
||||
// safe-lint: disable=javascript-concat-html
|
||||
answerData.default + '" />\n';
|
||||
}
|
||||
}
|
||||
|
||||
return string;
|
||||
// Additional answer case or= [e.g. or= 10]
|
||||
// Since answerValues[0] is firstAnswer, so we will not include this in additional answers.
|
||||
additionalAnswerString = '';
|
||||
for (i = 1; i < answerValues.length; i++) {
|
||||
additionalHintLine = '';
|
||||
additionalTextHint = extractHint(answerValues[i]);
|
||||
orMatch = /^or\=\s*(.*)/.exec(additionalTextHint.nothint);
|
||||
if (orMatch) {
|
||||
hasTolerance = /(.*?)\+\-\s*(.*?$)/.exec(orMatch[1]);
|
||||
// Do not add additional_answer if additional answer is not numerical (eg. or= ABC)
|
||||
// or contains range tolerance case (eg. or= (5,7)
|
||||
// or has tolerance (eg. or= 10 +- 0.02)
|
||||
if (isNaN(parseFloat(orMatch[1])) ||
|
||||
isRangeToleranceCase(orMatch[1]) ||
|
||||
hasTolerance) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (additionalTextHint.hint) {
|
||||
// safe-lint: disable=javascript-concat-html
|
||||
additionalHintLine = '<correcthint' +
|
||||
// safe-lint: disable=javascript-concat-html
|
||||
additionalTextHint.labelassign + '>' +
|
||||
// safe-lint: disable=javascript-concat-html
|
||||
additionalTextHint.hint + '</correcthint>';
|
||||
}
|
||||
|
||||
// safe-lint: disable=javascript-concat-html
|
||||
additionalAnswerString += ' <additional_answer answer="' + orMatch[1] + '">';
|
||||
additionalAnswerString += additionalHintLine;
|
||||
additionalAnswerString += '</additional_answer>\n';
|
||||
}
|
||||
}
|
||||
|
||||
// Add additional answers string to numerical problem string.
|
||||
if (additionalAnswerString) {
|
||||
numericalResponseString += additionalAnswerString;
|
||||
}
|
||||
|
||||
numericalResponseString += ' <formulaequationinput />\n';
|
||||
numericalResponseString += hintLine;
|
||||
numericalResponseString += '</numericalresponse>\n\n';
|
||||
|
||||
return numericalResponseString;
|
||||
},
|
||||
|
||||
processStringResponse = function(values) {
|
||||
@@ -657,7 +715,7 @@
|
||||
return string;
|
||||
};
|
||||
|
||||
return processNumericalResponse(answersList[0]) || processStringResponse(answersList);
|
||||
return processNumericalResponse(answersList) || processStringResponse(answersList);
|
||||
});
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user