Commit 387f2103 by Mushtaq Ali Committed by Mushtaq Ali

Add support for additional answers for Numerical Input problems

TNL-5581
parent 19ce4393
...@@ -1710,6 +1710,8 @@ class NumericalResponse(LoncapaResponse): ...@@ -1710,6 +1710,8 @@ class NumericalResponse(LoncapaResponse):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.correct_answer = '' self.correct_answer = ''
self.additional_answers = []
self.additional_answer_index = -1
self.tolerance = default_tolerance self.tolerance = default_tolerance
self.range_tolerance = False self.range_tolerance = False
self.answer_range = self.inclusion = None self.answer_range = self.inclusion = None
...@@ -1720,6 +1722,10 @@ class NumericalResponse(LoncapaResponse): ...@@ -1720,6 +1722,10 @@ class NumericalResponse(LoncapaResponse):
context = self.context context = self.context
answer = xml.get('answer') 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 if answer.startswith(('[', '(')) and answer.endswith((']', ')')): # range tolerance case
self.range_tolerance = True self.range_tolerance = True
self.inclusion = ( self.inclusion = (
...@@ -1930,6 +1936,20 @@ class NumericalResponse(LoncapaResponse): ...@@ -1930,6 +1936,20 @@ class NumericalResponse(LoncapaResponse):
if compare_with_tolerance(student_float, correct_float, expanded_tolerance): if compare_with_tolerance(student_float, correct_float, expanded_tolerance):
is_correct = 'partially-correct' 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': if is_correct == 'partially-correct':
return CorrectMap(self.answer_id, is_correct, npoints=partial_score) return CorrectMap(self.answer_id, is_correct, npoints=partial_score)
else: else:
...@@ -1957,7 +1977,38 @@ class NumericalResponse(LoncapaResponse): ...@@ -1957,7 +1977,38 @@ class NumericalResponse(LoncapaResponse):
return False return False
def get_answers(self): 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): def get_extended_hints(self, student_answers, new_cmap):
""" """
...@@ -1966,16 +2017,11 @@ class NumericalResponse(LoncapaResponse): ...@@ -1966,16 +2017,11 @@ class NumericalResponse(LoncapaResponse):
""" """
if self.answer_id in student_answers: if self.answer_id in student_answers:
if new_cmap.cmap[self.answer_id]['correctness'] == 'correct': # if the grader liked the student's answer 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 # Answer is not an additional answer.
hints = self.xml.xpath('//numericalresponse[@id=$id]/correcthint', id=self.id) if self.additional_answer_index == -1:
if hints: self.set_cmap_msg(student_answers, new_cmap, 'correcthint', 0)
hint_node = hints[0] else:
new_cmap[self.answer_id]['msg'] += self.make_hint_div( self.set_cmap_msg(student_answers, new_cmap, 'additional_answer', self.additional_answer_index)
hint_node,
True,
[student_answers[self.answer_id]],
self.tags[0]
)
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
......
...@@ -204,6 +204,10 @@ class NumericalResponseXMLFactory(ResponseXMLFactory): ...@@ -204,6 +204,10 @@ class NumericalResponseXMLFactory(ResponseXMLFactory):
*answer*: The correct answer (e.g. "5") *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 *tolerance*: The tolerance within which a response
is considered correct. Can be a decimal (e.g. "0.01") is considered correct. Can be a decimal (e.g. "0.01")
or percentage (e.g. "2%") or percentage (e.g. "2%")
...@@ -219,6 +223,8 @@ class NumericalResponseXMLFactory(ResponseXMLFactory): ...@@ -219,6 +223,8 @@ class NumericalResponseXMLFactory(ResponseXMLFactory):
""" """
answer = kwargs.get('answer', None) answer = kwargs.get('answer', None)
correcthint = kwargs.get('correcthint', '')
additional_answers = kwargs.get('additional_answers', {})
tolerance = kwargs.get('tolerance', None) tolerance = kwargs.get('tolerance', None)
credit_type = kwargs.get('credit_type', None) credit_type = kwargs.get('credit_type', None)
partial_range = kwargs.get('partial_range', None) partial_range = kwargs.get('partial_range', None)
...@@ -232,6 +238,13 @@ class NumericalResponseXMLFactory(ResponseXMLFactory): ...@@ -232,6 +238,13 @@ class NumericalResponseXMLFactory(ResponseXMLFactory):
else: else:
response_element.set('answer', str(answer)) 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: if tolerance:
responseparam_element = etree.SubElement(response_element, 'responseparam') responseparam_element = etree.SubElement(response_element, 'responseparam')
responseparam_element.set('type', 'tolerance') responseparam_element.set('type', 'tolerance')
...@@ -244,6 +257,10 @@ class NumericalResponseXMLFactory(ResponseXMLFactory): ...@@ -244,6 +257,10 @@ class NumericalResponseXMLFactory(ResponseXMLFactory):
responseparam_element = etree.SubElement(response_element, 'responseparam') responseparam_element = etree.SubElement(response_element, 'responseparam')
responseparam_element.set('partial_answers', partial_answers) responseparam_element.set('partial_answers', partial_answers)
if correcthint:
correcthint_element = etree.SubElement(response_element, 'correcthint')
correcthint_element.text = str(correcthint)
return response_element return response_element
def create_input_element(self, **kwargs): def create_input_element(self, **kwargs):
...@@ -732,7 +749,7 @@ class StringResponseXMLFactory(ResponseXMLFactory): ...@@ -732,7 +749,7 @@ class StringResponseXMLFactory(ResponseXMLFactory):
*regexp*: Whether the response is regexp *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_answers*: list of additional answers to be coded in the
non-attribute format non-attribute format
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
<label>What value when squared is approximately equal to 2 (give your answer to 2 decimal places)?</label> <label>What value when squared is approximately equal to 2 (give your answer to 2 decimal places)?</label>
<responseparam default=".01" type="tolerance"/> <responseparam default=".01" type="tolerance"/>
<formulaequationinput/> <formulaequationinput/>
<additional_answer answer="10">
<correcthint>This is an additional hint.</correcthint>
</additional_answer>
<correcthint label="Nice"> <correcthint label="Nice">
The square root of two turns up in the strangest places. The square root of two turns up in the strangest places.
......
...@@ -236,6 +236,9 @@ class NumericInputHintsTest(HintTest): ...@@ -236,6 +236,9 @@ class NumericInputHintsTest(HintTest):
@data( @data(
{'problem_id': u'1_2_1', 'choice': '1.141', {'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>'}, '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', {'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>'}, '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 # should get hint, when correct via numeric-tolerance
......
...@@ -1390,7 +1390,7 @@ class NumericalResponseTest(ResponseTest): # pylint: disable=missing-docstring ...@@ -1390,7 +1390,7 @@ class NumericalResponseTest(ResponseTest): # pylint: disable=missing-docstring
# For simple things its not worth the effort. # For simple things its not worth the effort.
def test_grade_range_tolerance(self): def test_grade_range_tolerance(self):
problem_setup = [ 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']], ['[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']], ['[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"]], ['[0, 1.6e-5]', ['1.6*10^-5'], ["2"]],
...@@ -1400,6 +1400,54 @@ class NumericalResponseTest(ResponseTest): # pylint: disable=missing-docstring ...@@ -1400,6 +1400,54 @@ class NumericalResponseTest(ResponseTest): # pylint: disable=missing-docstring
problem = self.build_problem(answer=given_answer) problem = self.build_problem(answer=given_answer)
self.assert_multiple_grade(problem, correct_responses, incorrect_responses) 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): def test_grade_range_tolerance_partial_credit(self):
problem_setup = [ problem_setup = [
# [given_answer, # [given_answer,
......
...@@ -187,21 +187,80 @@ describe 'MarkdownEditingDescriptor', -> ...@@ -187,21 +187,80 @@ describe 'MarkdownEditingDescriptor', ->
</problem>""") </problem>""")
it 'markup with multiple answers doesn\'t break numerical response', -> it 'markup with additional answer does not break numerical response', ->
data = MarkdownEditingDescriptor.markdownToXml(""" data = MarkdownEditingDescriptor.markdownToXml("""
Enter 1 with a tolerance: Enter 1 with a tolerance:
= 1 +- .02 = 1 +- .02
or= 2 +- 5% or= 2
""") """)
expect(data).toXMLEqual("""<problem> expect(data).toXMLEqual("""<problem>
<numericalresponse answer="1"> <numericalresponse answer="1">
<p>Enter 1 with a tolerance:</p> <p>Enter 1 with a tolerance:</p>
<responseparam type="tolerance" default=".02"/> <responseparam type="tolerance" default=".02"/>
<additional_answer answer="2"/>
<formulaequationinput/> <formulaequationinput/>
</numericalresponse> </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', -> 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. 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 @@ ...@@ -567,50 +567,108 @@
// Line split here, trim off leading xxx= in each function // Line split here, trim off leading xxx= in each function
var answersList = p.split('\n'), var answersList = p.split('\n'),
processNumericalResponse = function(val) { isRangeToleranceCase = function(answer) {
var params, answer, string, textHint, hintLine, value; return _.contains(
// Numeric case is just a plain leading = with a single answer ['[', '('], answer[0]) && _.contains([']', ')'], answer[answer.length - 1]
value = val.replace(/^\=\s*/, ''); );
},
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;
},
processNumericalResponse = function(answerValues) {
var firstAnswer, answerData, numericalResponseString, additionalAnswerString,
textHint, hintLine, additionalTextHint, additionalHintLine, orMatch, hasTolerance;
textHint = extractHint(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;
}
textHint = extractHint(firstAnswer);
hintLine = ''; hintLine = '';
if (textHint.hint) { if (textHint.hint) {
value = textHint.nothint; firstAnswer = textHint.nothint;
hintLine = ' <correcthint' + textHint.labelassign + '>' + textHint.hint + // safe-lint: disable=javascript-concat-html
'</correcthint>\n'; hintLine = ' <correcthint' + textHint.labelassign + '>' +
// safe-lint: disable=javascript-concat-html
textHint.hint + '</correcthint>\n';
} }
if (_.contains(['[', '('], value[0]) && _.contains([']', ')'], value[value.length - 1])) { // Range case
if (isRangeToleranceCase(firstAnswer)) {
// [5, 7) or (5, 7), or (1.2345 * (2+3), 7*4 ] - range tolerance case // [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 // = (5*2)*3 should not be used as range tolerance
string = '<numericalresponse answer="' + value + '">\n'; // safe-lint: disable=javascript-concat-html
string += ' <formulaequationinput />\n'; numericalResponseString = '<numericalresponse answer="' + firstAnswer + '">\n';
string += hintLine; } else {
string += '</numericalresponse>\n\n'; answerData = getAnswerData(firstAnswer);
return string; // 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';
}
} }
if (isNaN(parseFloat(value))) { // Additional answer case or= [e.g. or= 10]
return false; // 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;
}
// Tries to extract parameters from string like 'expr +- tolerance' if (additionalTextHint.hint) {
params = /(.*?)\+\-\s*(.*?$)/.exec(value); // 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>';
}
if (params) { // safe-lint: disable=javascript-concat-html
answer = params[1].replace(/\s+/g, ''); // support inputs like 5*2 +- 10 additionalAnswerString += ' <additional_answer answer="' + orMatch[1] + '">';
string = '<numericalresponse answer="' + answer + '">\n'; additionalAnswerString += additionalHintLine;
string += ' <responseparam type="tolerance" default="' + params[2] + '" />\n'; additionalAnswerString += '</additional_answer>\n';
} else { }
answer = value.replace(/\s+/g, ''); // support inputs like 5*2
string = '<numericalresponse answer="' + answer + '">\n';
} }
string += ' <formulaequationinput />\n'; // Add additional answers string to numerical problem string.
string += hintLine; if (additionalAnswerString) {
string += '</numericalresponse>\n\n'; numericalResponseString += additionalAnswerString;
}
return string; numericalResponseString += ' <formulaequationinput />\n';
numericalResponseString += hintLine;
numericalResponseString += '</numericalresponse>\n\n';
return numericalResponseString;
}, },
processStringResponse = function(values) { processStringResponse = function(values) {
...@@ -657,7 +715,7 @@ ...@@ -657,7 +715,7 @@
return string; return string;
}; };
return processNumericalResponse(answersList[0]) || processStringResponse(answersList); return processNumericalResponse(answersList) || processStringResponse(answersList);
}); });
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment