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):
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]
);
},
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 = '';
if (textHint.hint) {
value = textHint.nothint;
hintLine = ' <correcthint' + textHint.labelassign + '>' + textHint.hint +
'</correcthint>\n';
firstAnswer = textHint.nothint;
// safe-lint: disable=javascript-concat-html
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*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;
// 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';
}
}
if (isNaN(parseFloat(value))) {
return false;
}
// 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;
}
// Tries to extract parameters from string like 'expr +- tolerance'
params = /(.*?)\+\-\s*(.*?$)/.exec(value);
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>';
}
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';
// safe-lint: disable=javascript-concat-html
additionalAnswerString += ' <additional_answer answer="' + orMatch[1] + '">';
additionalAnswerString += additionalHintLine;
additionalAnswerString += '</additional_answer>\n';
}
}
string += ' <formulaequationinput />\n';
string += hintLine;
string += '</numericalresponse>\n\n';
// Add additional answers string to numerical problem string.
if (additionalAnswerString) {
numericalResponseString += additionalAnswerString;
}
return string;
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);
});
......
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