Commit 77c208a1 by polesye

BLD-474: Allow multiple answers for string response.

parent d526027d
...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes, ...@@ -5,6 +5,8 @@ These are notable changes in edx-platform. This is a rolling list of changes,
in roughly chronological order, most recent first. Add your entries at or near in roughly chronological order, most recent first. Add your entries at or near
the top. Include a label indicating the component affected. the top. Include a label indicating the component affected.
Blades: Allow multiple strings as the correct answer to a string response question. BLD-474.
Blades: a11y - Videos will alert screenreaders when the video is over. Blades: a11y - Videos will alert screenreaders when the video is over.
LMS: Trap focus on the loading element when a user loads more threads LMS: Trap focus on the loading element when a user loads more threads
......
...@@ -75,7 +75,9 @@ ...@@ -75,7 +75,9 @@
<img src="${static.url("img/string-example.png")}" /> <img src="${static.url("img/string-example.png")}" />
</div> </div>
<div class="col"> <div class="col">
<pre><code>= dog</code></pre> <pre><code>= dog
or= cat
or= mouse</code></pre>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
......
...@@ -946,17 +946,34 @@ class NumericalResponse(LoncapaResponse): ...@@ -946,17 +946,34 @@ class NumericalResponse(LoncapaResponse):
class StringResponse(LoncapaResponse): class StringResponse(LoncapaResponse):
'''
This response type allows one or more answers. Use `_or_` separator to set
more than 1 answer.
Example:
# One answer
<stringresponse answer="Michigan">
<textline size="20" />
</stringresponse >
# Multiple answers
<stringresponse answer="Martin Luther King_or_Dr. Martin Luther King Jr.">
<textline size="20" />
</stringresponse >
'''
response_tag = 'stringresponse' response_tag = 'stringresponse'
hint_tag = 'stringhint' hint_tag = 'stringhint'
allowed_inputfields = ['textline'] allowed_inputfields = ['textline']
required_attributes = ['answer'] required_attributes = ['answer']
max_inputfields = 1 max_inputfields = 1
correct_answer = None correct_answer = []
SEPARATOR = '_or_'
def setup_response(self): def setup_response(self):
self.correct_answer = contextualize_text( self.correct_answer = [contextualize_text(answer, self.context).strip()
self.xml.get('answer'), self.context).strip() for answer in self.xml.get('answer').split(self.SEPARATOR)]
def get_score(self, student_answers): def get_score(self, student_answers):
'''Grade a string response ''' '''Grade a string response '''
...@@ -966,23 +983,25 @@ class StringResponse(LoncapaResponse): ...@@ -966,23 +983,25 @@ class StringResponse(LoncapaResponse):
def check_string(self, expected, given): def check_string(self, expected, given):
if self.xml.get('type') == 'ci': if self.xml.get('type') == 'ci':
return given.lower() == expected.lower() return given.lower() in [i.lower() for i in expected]
return given == expected return given in expected
def check_hint_condition(self, hxml_set, student_answers): def check_hint_condition(self, hxml_set, student_answers):
given = student_answers[self.answer_id].strip() given = student_answers[self.answer_id].strip()
hints_to_show = [] hints_to_show = []
for hxml in hxml_set: for hxml in hxml_set:
name = hxml.get('name') name = hxml.get('name')
correct_answer = contextualize_text(
hxml.get('answer'), self.context).strip() correct_answer = [contextualize_text(answer, self.context).strip()
for answer in hxml.get('answer').split(self.SEPARATOR)]
if self.check_string(correct_answer, given): if self.check_string(correct_answer, given):
hints_to_show.append(name) hints_to_show.append(name)
log.debug('hints_to_show = %s', hints_to_show) log.debug('hints_to_show = %s', hints_to_show)
return hints_to_show return hints_to_show
def get_answers(self): def get_answers(self):
return {self.answer_id: self.correct_answer} return {self.answer_id: ' <b>or</b> '.join(self.correct_answer)}
#----------------------------------------------------------------------------- #-----------------------------------------------------------------------------
......
...@@ -500,6 +500,7 @@ class StringResponseTest(ResponseTest): ...@@ -500,6 +500,7 @@ class StringResponseTest(ResponseTest):
xml_factory_class = StringResponseXMLFactory xml_factory_class = StringResponseXMLFactory
def test_case_sensitive(self): def test_case_sensitive(self):
# Test single answer
problem = self.build_problem(answer="Second", case_sensitive=True) problem = self.build_problem(answer="Second", case_sensitive=True)
# Exact string should be correct # Exact string should be correct
...@@ -509,7 +510,20 @@ class StringResponseTest(ResponseTest): ...@@ -509,7 +510,20 @@ class StringResponseTest(ResponseTest):
self.assert_grade(problem, "Other String", "incorrect") self.assert_grade(problem, "Other String", "incorrect")
self.assert_grade(problem, "second", "incorrect") self.assert_grade(problem, "second", "incorrect")
# Test multiple answers
answers = ["Second", "Third", "Fourth"]
problem = self.build_problem(answer="_or_".join(answers), case_sensitive=True)
for answer in answers:
# Exact string should be correct
self.assert_grade(problem, answer, "correct")
# Other strings and the lowercase version of the string are incorrect
self.assert_grade(problem, "Other String", "incorrect")
self.assert_grade(problem, "second", "incorrect")
def test_case_insensitive(self): def test_case_insensitive(self):
# Test single answer
problem = self.build_problem(answer="Second", case_sensitive=False) problem = self.build_problem(answer="Second", case_sensitive=False)
# Both versions of the string should be allowed, regardless # Both versions of the string should be allowed, regardless
...@@ -520,9 +534,28 @@ class StringResponseTest(ResponseTest): ...@@ -520,9 +534,28 @@ class StringResponseTest(ResponseTest):
# Other strings are not allowed # Other strings are not allowed
self.assert_grade(problem, "Other String", "incorrect") self.assert_grade(problem, "Other String", "incorrect")
# Test multiple answers
answers = ["Second", "Third", "Fourth"]
problem = self.build_problem(answer="_or_".join(answers), case_sensitive=False)
for answer in answers:
# Exact string should be correct
self.assert_grade(problem, answer, "correct")
self.assert_grade(problem, answer.lower(), "correct")
# Other strings and the lowercase version of the string are incorrect
self.assert_grade(problem, "Other String", "incorrect")
def test_hints(self): def test_hints(self):
multiple_answers = [
"Martin Luther King Junior",
"Doctor Martin Luther King Junior",
"Dr. Martin Luther King Jr.",
"Martin Luther King"
]
hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"), hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"),
("minnesota", "minn", "The state capital of Minnesota is St. Paul")] ("minnesota", "minn", "The state capital of Minnesota is St. Paul"),
("_or_".join(multiple_answers), "mlk", "He lead the civil right movement in the United States of America.")]
problem = self.build_problem(answer="Michigan", problem = self.build_problem(answer="Michigan",
case_sensitive=False, case_sensitive=False,
...@@ -550,6 +583,14 @@ class StringResponseTest(ResponseTest): ...@@ -550,6 +583,14 @@ class StringResponseTest(ResponseTest):
correct_map = problem.grade_answers(input_dict) correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "") self.assertEquals(correct_map.get_hint('1_2_1'), "")
# We should get the same hint for each answer
for answer in multiple_answers:
input_dict = {'1_2_1': answer}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'),
"He lead the civil right movement in the United States of America.")
def test_computed_hints(self): def test_computed_hints(self):
problem = self.build_problem( problem = self.build_problem(
answer="Michigan", answer="Michigan",
......
...@@ -228,11 +228,13 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor ...@@ -228,11 +228,13 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
}); });
// replace string and numerical // replace string and numerical
xml = xml.replace(/^\=\s*(.*?$)/gm, function(match, p) { xml = xml.replace(/(^\=\s*(.*?$)(\n*or\=\s*(.*?$))*)+/gm, function(match, p) {
var string; var string,
var floatValue = parseFloat(p); answersList = p.replace(/^(or)?=\s*/gm, '').split('\n'),
floatValue = parseFloat(answersList[0]);
if(!isNaN(floatValue)) { if(!isNaN(floatValue)) {
var params = /(.*?)\+\-\s*(.*?$)/.exec(p); var params = /(.*?)\+\-\s*(.*?$)/.exec(answersList[0]);
if(params) { if(params) {
string = '<numericalresponse answer="' + floatValue + '">\n'; string = '<numericalresponse answer="' + floatValue + '">\n';
string += ' <responseparam type="tolerance" default="' + params[2] + '" />\n'; string += ' <responseparam type="tolerance" default="' + params[2] + '" />\n';
...@@ -242,10 +244,16 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor ...@@ -242,10 +244,16 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
string += ' <formulaequationinput />\n'; string += ' <formulaequationinput />\n';
string += '</numericalresponse>\n\n'; string += '</numericalresponse>\n\n';
} else { } else {
string = '<stringresponse answer="' + p + '" type="ci">\n <textline size="20"/>\n</stringresponse>\n\n'; var answers = [];
for(var i = 0; i < answersList.length; i++) {
answers.push(answersList[i])
}
string = '<stringresponse answer="' + answers.join('_or_') + '" type="ci">\n <textline size="20"/>\n</stringresponse>\n\n';
} }
return string; return string;
}); });
// replace selects // replace selects
xml = xml.replace(/\[\[(.+?)\]\]/g, function(match, p) { xml = xml.replace(/\[\[(.+?)\]\]/g, function(match, p) {
...@@ -262,13 +270,13 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor ...@@ -262,13 +270,13 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
selectString += '</optionresponse>\n\n'; selectString += '</optionresponse>\n\n';
return selectString; return selectString;
}); });
// replace explanations // replace explanations
xml = xml.replace(/\[explanation\]\n?([^\]]*)\[\/?explanation\]/gmi, function(match, p1) { xml = xml.replace(/\[explanation\]\n?([^\]]*)\[\/?explanation\]/gmi, function(match, p1) {
var selectString = '<solution>\n<div class="detailed-solution">\nExplanation\n\n' + p1 + '\n</div>\n</solution>'; var selectString = '<solution>\n<div class="detailed-solution">\nExplanation\n\n' + p1 + '\n</div>\n</solution>';
return selectString; return selectString;
}); });
// replace code blocks // replace code blocks
xml = xml.replace(/\[code\]\n?([^\]]*)\[\/?code\]/gmi, function(match, p1) { xml = xml.replace(/\[code\]\n?([^\]]*)\[\/?code\]/gmi, function(match, p1) {
var selectString = '<pre><code>\n' + p1 + '</code></pre>'; var selectString = '<pre><code>\n' + p1 + '</code></pre>';
...@@ -293,7 +301,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor ...@@ -293,7 +301,7 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
// rid white space // rid white space
xml = xml.replace(/\n\n\n/g, '\n'); xml = xml.replace(/\n\n\n/g, '\n');
// surround w/ problem tag // surround w/ problem tag
xml = '<problem>\n' + xml + '\n</problem>'; xml = '<problem>\n' + xml + '\n</problem>';
......
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