Commit ee1953b3 by polesye Committed by Alexander Kryklia

BLD-474: Allow multiple answers for string response.

parent 7b6cf0dc
...@@ -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 regexp strings as the correct answer to a string response question. BLD-475.
Common: Add feature flags to allow developer use of pure XBlocks Common: Add feature flags to allow developer use of pure XBlocks
- ALLOW_ALL_ADVANCED_COMPONENTS disables the hard-coded list of advanced - ALLOW_ALL_ADVANCED_COMPONENTS disables the hard-coded list of advanced
components in Studio, and allows any xblock to be added as an components in Studio, and allows any xblock to be added as an
......
...@@ -329,8 +329,8 @@ class LoncapaResponse(object): ...@@ -329,8 +329,8 @@ class LoncapaResponse(object):
rephints = hintgroup.findall(self.hint_tag) rephints = hintgroup.findall(self.hint_tag)
hints_to_show = self.check_hint_condition( hints_to_show = self.check_hint_condition(
rephints, student_answers) rephints, student_answers)
# can be 'on_request' or 'always' (default) # can be 'on_request' or 'always' (default)
hintmode = hintgroup.get('mode', 'always') hintmode = hintgroup.get('mode', 'always')
for hintpart in hintgroup.findall('hintpart'): for hintpart in hintgroup.findall('hintpart'):
if hintpart.get('on') in hints_to_show: if hintpart.get('on') in hints_to_show:
...@@ -947,21 +947,35 @@ class NumericalResponse(LoncapaResponse): ...@@ -947,21 +947,35 @@ class NumericalResponse(LoncapaResponse):
class StringResponse(LoncapaResponse): class StringResponse(LoncapaResponse):
''' '''
This response type allows one or more answers. Use `_or_` separator to set This response type allows one or more answers.
more than 1 answer.
Example: Additional answers are added by `additional_answer` tag.
If `regexp` is in `type` attribute, than answers and hints are treated as regular expressions.
# One answer Examples:
<stringresponse answer="Michigan"> <stringresponse answer="Michigan">
<textline size="20" /> <textline size="20" />
</stringresponse >
# Multiple answers
<stringresponse answer="Martin Luther King_or_Dr. Martin Luther King Jr.">
<textline size="20" />
</stringresponse > </stringresponse >
<stringresponse answer="a1" type="ci regexp">
<additional_answer>\d5</additional_answer>
<additional_answer>a3</additional_answer>
<textline size="20"/>
<hintgroup>
<stringhint answer="a0" type="ci" name="ha0" />
<stringhint answer="a4" type="ci" name="ha4" />
<stringhint answer="^\d" type="ci" name="re1" />
<hintpart on="ha0">
<startouttext />+1<endouttext />
</hintpart >
<hintpart on="ha4">
<startouttext />-1<endouttext />
</hintpart >
<hintpart on="re1">
<startouttext />Any number+5<endouttext />
</hintpart >
</hintgroup>
</stringresponse>
''' '''
response_tag = 'stringresponse' response_tag = 'stringresponse'
hint_tag = 'stringhint' hint_tag = 'stringhint'
...@@ -969,11 +983,30 @@ class StringResponse(LoncapaResponse): ...@@ -969,11 +983,30 @@ class StringResponse(LoncapaResponse):
required_attributes = ['answer'] required_attributes = ['answer']
max_inputfields = 1 max_inputfields = 1
correct_answer = [] correct_answer = []
SEPARATOR = '_or_'
def setup_response_backward(self):
self.correct_answer = [
contextualize_text(answer, self.context).strip() for answer in self.xml.get('answer').split('_or_')
]
def setup_response(self): def setup_response(self):
self.correct_answer = [contextualize_text(answer, self.context).strip()
for answer in self.xml.get('answer').split(self.SEPARATOR)] self.backward = '_or_' in self.xml.get('answer').lower()
self.regexp = 'regexp' in self.xml.get('type').lower().split(' ')
self.case_insensitive = 'ci' in self.xml.get('type').lower().split(' ')
# backward compatibility, can be removed in future, it is up to @Lyla Fisher.
if self.backward:
self.setup_response_backward()
return
# end of backward compatibility
correct_answers = [self.xml.get('answer')] + [el.text for el in self.xml.findall('additional_answer')]
self.correct_answer = [contextualize_text(answer, self.context).strip() for answer in correct_answers]
# remove additional_answer from xml, otherwise they will be displayed
for el in self.xml.findall('additional_answer'):
self.xml.remove(el)
def get_score(self, student_answers): def get_score(self, student_answers):
'''Grade a string response ''' '''Grade a string response '''
...@@ -981,21 +1014,61 @@ class StringResponse(LoncapaResponse): ...@@ -981,21 +1014,61 @@ class StringResponse(LoncapaResponse):
correct = self.check_string(self.correct_answer, student_answer) correct = self.check_string(self.correct_answer, student_answer)
return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect') return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect')
def check_string(self, expected, given): def check_string_backward(self, expected, given):
if self.xml.get('type') == 'ci': if self.case_insensitive:
return given.lower() in [i.lower() for i in expected] return given.lower() in [i.lower() for i in expected]
return given in expected return given in expected
def check_string(self, expected, given):
"""
Find given in expected.
If self.regexp is true, regular expression search is used.
if self.case_insensitive is true, case insensitive search is used, otherwise case sensitive search is used.
Spaces around values of attributes are stripped in XML parsing step.
Args:
expected: list.
given: str.
Returns: bool
Raises: `ResponseError` if it fails to compile regular expression.
Note: for old code, which supports _or_ separator, we add some backward compatibility handling.
Should be removed soon. When to remove it, is up to Lyla Fisher.
"""
# backward compatibility, should be removed in future.
if self.backward:
return self.check_string_backward(expected, given)
# end of backward compatibility
if self.regexp: # regexp match
flags = re.IGNORECASE if self.case_insensitive else 0
try:
regexp = re.compile('^'+ '|'.join(expected) + '$', flags=flags | re.UNICODE)
result = re.search(regexp, given)
except Exception as err:
msg = '[courseware.capa.responsetypes.stringresponse] error: {}'.format(err.message)
log.error(msg, exc_info=True)
raise ResponseError(msg)
return bool(result)
else: # string match
if self.case_insensitive:
return given.lower() in [i.lower() for i in expected]
else:
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(answer, self.context).strip() hinted_answer = contextualize_text(hxml.get('answer'), self.context).strip()
for answer in hxml.get('answer').split(self.SEPARATOR)]
if self.check_string(correct_answer, given): if self.check_string([hinted_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
......
...@@ -690,22 +690,30 @@ class StringResponseXMLFactory(ResponseXMLFactory): ...@@ -690,22 +690,30 @@ class StringResponseXMLFactory(ResponseXMLFactory):
*hintfn*: The name of a function in the script to use for hints. *hintfn*: The name of a function in the script to use for hints.
*regexp*: Whether the response is regexp
*additional_answers*: list of additional asnwers.
""" """
# Retrieve the **kwargs # Retrieve the **kwargs
answer = kwargs.get("answer", None) answer = kwargs.get("answer", None)
case_sensitive = kwargs.get("case_sensitive", True) case_sensitive = kwargs.get("case_sensitive", True)
hint_list = kwargs.get('hints', None) hint_list = kwargs.get('hints', None)
hint_fn = kwargs.get('hintfn', None) hint_fn = kwargs.get('hintfn', None)
regexp = kwargs.get('regexp', None)
additional_answers = kwargs.get('additional_answers', [])
assert answer assert answer
# Create the <stringresponse> element # Create the <stringresponse> element
response_element = etree.Element("stringresponse") response_element = etree.Element("stringresponse")
# Set the answer attribute # Set the answer attribute
response_element.set("answer", str(answer)) response_element.set("answer", unicode(answer))
# Set the case sensitivity # Set the case sensitivity and regexp:
response_element.set("type", "cs" if case_sensitive else "ci") type_value = "cs" if case_sensitive else "ci"
type_value += ' regexp' if regexp else ''
response_element.set("type", type_value)
# Add the hints if specified # Add the hints if specified
if hint_list or hint_fn: if hint_list or hint_fn:
...@@ -727,6 +735,9 @@ class StringResponseXMLFactory(ResponseXMLFactory): ...@@ -727,6 +735,9 @@ class StringResponseXMLFactory(ResponseXMLFactory):
assert not hint_list assert not hint_list
hintgroup_element.set("hintfn", hint_fn) hintgroup_element.set("hintfn", hint_fn)
for additional_answer in additional_answers:
etree.SubElement(response_element, "additional_answer").text = additional_answer
return response_element return response_element
def create_input_element(self, **kwargs): def create_input_element(self, **kwargs):
......
# -*- coding: utf-8 -*-
""" """
Tests of responsetypes Tests of responsetypes
""" """
...@@ -502,6 +503,135 @@ class StringResponseTest(ResponseTest): ...@@ -502,6 +503,135 @@ class StringResponseTest(ResponseTest):
from capa.tests.response_xml_factory import StringResponseXMLFactory from capa.tests.response_xml_factory import StringResponseXMLFactory
xml_factory_class = StringResponseXMLFactory xml_factory_class = StringResponseXMLFactory
def test_backward_compatibility_for_multiple_answers(self):
"""
Remove this test, once support for _or_ separator will be removed.
"""
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")
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")
self.assert_grade(problem, "Other String", "incorrect")
def test_regexp(self):
problem = self.build_problem(answer="Second", case_sensitive=False, regexp=True)
self.assert_grade(problem, "Second", "correct")
problem = self.build_problem(answer="sec", case_sensitive=False, regexp=True)
self.assert_grade(problem, "Second", "incorrect")
problem = self.build_problem(answer="sec.*", case_sensitive=False, regexp=True)
self.assert_grade(problem, "Second", "correct")
problem = self.build_problem(answer="sec.*", case_sensitive=True, regexp=True)
self.assert_grade(problem, "Second", "incorrect")
problem = self.build_problem(answer="Sec.*$", case_sensitive=False, regexp=True)
self.assert_grade(problem, "Second", "correct")
problem = self.build_problem(answer="^sec$", case_sensitive=False, regexp=True)
self.assert_grade(problem, "Second", "incorrect")
problem = self.build_problem(answer="^Sec(ond)?$", case_sensitive=False, regexp=True)
self.assert_grade(problem, "Second", "correct")
problem = self.build_problem(answer="^Sec(ond)?$", case_sensitive=False, regexp=True)
self.assert_grade(problem, "Sec", "correct")
problem = self.build_problem(answer="tre+", case_sensitive=False, regexp=True)
self.assert_grade(problem, "There is a tree", "incorrect")
problem = self.build_problem(answer=".*tre+", case_sensitive=False, regexp=True)
self.assert_grade(problem, "There is a tree", "correct")
answers = [
"Martin Luther King Junior",
"Doctor Martin Luther King Junior",
"Dr. Martin Luther King Jr.",
"Martin Luther King"
]
problem = self.build_problem(answer="\w*\.?.*Luther King\s*.*", case_sensitive=True, regexp=True)
for answer in answers:
self.assert_grade(problem, answer, "correct")
problem = self.build_problem(answer="^(-\|){2,5}$", case_sensitive=False, regexp=True)
self.assert_grade(problem, "-|-|-|", "correct")
self.assert_grade(problem, "-|", "incorrect")
self.assert_grade(problem, "-|-|-|-|-|-|", "incorrect")
regexps = [
"^One$",
"two",
"^thre+",
"^4|Four$",
]
problem = self.build_problem(
answer="just_sample",
case_sensitive=False,
regexp=True,
additional_answers=regexps
)
self.assert_grade(problem, "One", "correct")
self.assert_grade(problem, "two", "correct")
self.assert_grade(problem, "!!two!!", "correct")
self.assert_grade(problem, "threeeee", "correct")
self.assert_grade(problem, "three", "correct")
self.assert_grade(problem, "4", "correct")
self.assert_grade(problem, "Four", "correct")
self.assert_grade(problem, "Five", "incorrect")
self.assert_grade(problem, "|", "incorrect")
# test unicode
problem = self.build_problem(answer=u"æ", case_sensitive=False, regexp=True, additional_answers=[u'ö'])
self.assert_grade(problem, u"æ", "correct")
self.assert_grade(problem, u"ö", "correct")
self.assert_grade(problem, u"î", "incorrect")
self.assert_grade(problem, u"o", "incorrect")
def test_backslash_and_unicode_regexps(self):
"""
Test some special cases of [unicode] regexps.
One needs to use either r'' strings or write real `repr` of unicode strings, because of the following
(from python docs, http://docs.python.org/2/library/re.html):
'for example, to match a literal backslash, one might have to write '\\\\' as the pattern string,
because the regular expression must be \\,
and each backslash must be expressed as \\ inside a regular Python string literal.'
Example of real use case in Studio:
a) user inputs regexp in usual regexp language,
b) regexp is saved to xml and is read in python as repr of that string
So a\d in front-end editor will become a\\\\d in xml, so it will match a1 as student answer.
"""
problem = self.build_problem(answer=ur"5\\æ", case_sensitive=False, regexp=True)
self.assert_grade(problem, u"5\æ", "correct")
problem = self.build_problem(answer=u"5\\\\æ", case_sensitive=False, regexp=True)
self.assert_grade(problem, u"5\æ", "correct")
def test_backslash(self):
problem = self.build_problem(answer=u"a\\\\c1", case_sensitive=False, regexp=True)
self.assert_grade(problem, u"a\c1", "correct")
def test_special_chars(self):
problem = self.build_problem(answer=ur"a \s1", case_sensitive=False, regexp=True)
self.assert_grade(problem, u"a 1", "correct")
def test_case_sensitive(self): def test_case_sensitive(self):
# Test single answer # Test single answer
problem = self.build_problem(answer="Second", case_sensitive=True) problem = self.build_problem(answer="Second", case_sensitive=True)
...@@ -515,7 +645,7 @@ class StringResponseTest(ResponseTest): ...@@ -515,7 +645,7 @@ class StringResponseTest(ResponseTest):
# Test multiple answers # Test multiple answers
answers = ["Second", "Third", "Fourth"] answers = ["Second", "Third", "Fourth"]
problem = self.build_problem(answer="_or_".join(answers), case_sensitive=True) problem = self.build_problem(answer="sample_answer", case_sensitive=True, additional_answers=answers)
for answer in answers: for answer in answers:
# Exact string should be correct # Exact string should be correct
...@@ -525,6 +655,18 @@ class StringResponseTest(ResponseTest): ...@@ -525,6 +655,18 @@ 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")
def test_bogus_escape_not_raised(self):
"""
We now adding ^ and $ around regexp, so no bogus escape error will be raised.
"""
problem = self.build_problem(answer=u"\\", case_sensitive=False, regexp=True)
self.assert_grade(problem, u"\\", "incorrect")
# right way to search for \
problem = self.build_problem(answer=u"\\\\", case_sensitive=False, regexp=True)
self.assert_grade(problem, u"\\", "correct")
def test_case_insensitive(self): def test_case_insensitive(self):
# Test single answer # Test single answer
problem = self.build_problem(answer="Second", case_sensitive=False) problem = self.build_problem(answer="Second", case_sensitive=False)
...@@ -539,7 +681,7 @@ class StringResponseTest(ResponseTest): ...@@ -539,7 +681,7 @@ class StringResponseTest(ResponseTest):
# Test multiple answers # Test multiple answers
answers = ["Second", "Third", "Fourth"] answers = ["Second", "Third", "Fourth"]
problem = self.build_problem(answer="_or_".join(answers), case_sensitive=False) problem = self.build_problem(answer="sample_answer", case_sensitive=False, additional_answers=answers)
for answer in answers: for answer in answers:
# Exact string should be correct # Exact string should be correct
...@@ -549,20 +691,77 @@ class StringResponseTest(ResponseTest): ...@@ -549,20 +691,77 @@ class StringResponseTest(ResponseTest):
# Other strings and the lowercase version of the string are incorrect # Other strings and the lowercase version of the string are incorrect
self.assert_grade(problem, "Other String", "incorrect") self.assert_grade(problem, "Other String", "incorrect")
def test_partial_matching(self):
problem = self.build_problem(answer="a2", case_sensitive=False, regexp=True, additional_answers=['.?\\d.?'])
self.assert_grade(problem, "a3", "correct")
self.assert_grade(problem, "3a", "correct")
def test_exception(self):
problem = self.build_problem(answer="a2", case_sensitive=False, regexp=True, additional_answers=['?\\d?'])
with self.assertRaises(Exception) as cm:
self.assert_grade(problem, "a3", "correct")
exception_message = cm.exception.message
self.assertIn("nothing to repeat", exception_message)
def test_hints(self): def test_hints(self):
multiple_answers = [
"Martin Luther King Junior", hints = [
"Doctor Martin Luther King Junior", ("wisconsin", "wisc", "The state capital of Wisconsin is Madison"),
"Dr. Martin Luther King Jr.", ("minnesota", "minn", "The state capital of Minnesota is St. Paul"),
"Martin Luther King"
] ]
hints = [("wisconsin", "wisc", "The state capital of Wisconsin is Madison"), problem = self.build_problem(
("minnesota", "minn", "The state capital of Minnesota is St. Paul"), answer="Michigan",
("_or_".join(multiple_answers), "mlk", "He lead the civil right movement in the United States of America.")] case_sensitive=False,
hints=hints,
)
# We should get a hint for Wisconsin
input_dict = {'1_2_1': 'Wisconsin'}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'),
"The state capital of Wisconsin is Madison")
problem = self.build_problem(answer="Michigan", # We should get a hint for Minnesota
case_sensitive=False, input_dict = {'1_2_1': 'Minnesota'}
hints=hints) correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'),
"The state capital of Minnesota is St. Paul")
# We should NOT get a hint for Michigan (the correct answer)
input_dict = {'1_2_1': 'Michigan'}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "")
# We should NOT get a hint for any other string
input_dict = {'1_2_1': 'California'}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "")
def test_hints_regexp_and_answer_regexp(self):
different_student_answers = [
"May be it is Boston",
"Boston, really?",
"Boston",
"OK, I see, this is Boston",
]
# if problem has regexp = true, it will accept hints written in regexp
hints = [
("wisconsin", "wisc", "The state capital of Wisconsin is Madison"),
("minnesota", "minn", "The state capital of Minnesota is St. Paul"),
(".*Boston.*", "bst", "First letter of correct answer is M."),
('^\\d9$', "numbers", "Should not end with 9."),
]
additional_answers = [
'^\\d[0-8]$',
]
problem = self.build_problem(
answer="Michigan",
case_sensitive=False,
hints=hints,
additional_answers=additional_answers,
regexp=True
)
# We should get a hint for Wisconsin # We should get a hint for Wisconsin
input_dict = {'1_2_1': 'Wisconsin'} input_dict = {'1_2_1': 'Wisconsin'}
...@@ -587,12 +786,18 @@ class StringResponseTest(ResponseTest): ...@@ -587,12 +786,18 @@ class StringResponseTest(ResponseTest):
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 # We should get the same hint for each answer
for answer in multiple_answers: for answer in different_student_answers:
input_dict = {'1_2_1': answer} input_dict = {'1_2_1': answer}
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'), "First letter of correct answer is M.")
"He lead the civil right movement in the United States of America.")
input_dict = {'1_2_1': '59'}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "Should not end with 9.")
input_dict = {'1_2_1': '57'}
correct_map = problem.grade_answers(input_dict)
self.assertEquals(correct_map.get_hint('1_2_1'), "")
def test_computed_hints(self): def test_computed_hints(self):
problem = self.build_problem( problem = self.build_problem(
......
...@@ -270,7 +270,7 @@ describe 'MarkdownEditingDescriptor', -> ...@@ -270,7 +270,7 @@ describe 'MarkdownEditingDescriptor', ->
<p>The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear.</p> <p>The answer is correct if it matches every character of the expected answer. This can be a problem with international spelling, dates, or anything where the format of the answer is not clear.</p>
<p>Which US state has Lansing as its capital?</p> <p>Which US state has Lansing as its capital?</p>
<stringresponse answer="Michigan" type="ci"> <stringresponse answer="Michigan" type="ci" >
<textline size="20"/> <textline size="20"/>
</stringresponse> </stringresponse>
...@@ -283,6 +283,29 @@ describe 'MarkdownEditingDescriptor', -> ...@@ -283,6 +283,29 @@ describe 'MarkdownEditingDescriptor', ->
</div> </div>
</solution> </solution>
</problem>""") </problem>""")
it 'converts StringResponse with regular expression to xml', ->
data = MarkdownEditingDescriptor.markdownToXml("""Who lead the civil right movement in the United States of America?
= | \w*\.?\s*Luther King\s*.*
[Explanation]
Test Explanation.
[Explanation]
""")
expect(data).toEqual("""<problem>
<p>Who lead the civil right movement in the United States of America?</p>
<stringresponse answer="\w*\.?\s*Luther King\s*.*" type="ci regexp" >
<textline size="20"/>
</stringresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>Test Explanation.</p>
</div>
</solution>
</problem>""")
it 'converts StringResponse with multiple answers to xml', -> it 'converts StringResponse with multiple answers to xml', ->
data = MarkdownEditingDescriptor.markdownToXml("""Who lead the civil right movement in the United States of America? data = MarkdownEditingDescriptor.markdownToXml("""Who lead the civil right movement in the United States of America?
= Dr. Martin Luther King Jr. = Dr. Martin Luther King Jr.
...@@ -296,7 +319,39 @@ describe 'MarkdownEditingDescriptor', -> ...@@ -296,7 +319,39 @@ describe 'MarkdownEditingDescriptor', ->
""") """)
expect(data).toEqual("""<problem> expect(data).toEqual("""<problem>
<p>Who lead the civil right movement in the United States of America?</p> <p>Who lead the civil right movement in the United States of America?</p>
<stringresponse answer="Dr. Martin Luther King Jr._or_Doctor Martin Luther King Junior_or_Martin Luther King_or_Martin Luther King Junior" type="ci"> <stringresponse answer="Dr. Martin Luther King Jr." type="ci" >
<additional_answer>Doctor Martin Luther King Junior</additional_answer>
<additional_answer>Martin Luther King</additional_answer>
<additional_answer>Martin Luther King Junior</additional_answer>
<textline size="20"/>
</stringresponse>
<solution>
<div class="detailed-solution">
<p>Explanation</p>
<p>Test Explanation.</p>
</div>
</solution>
</problem>""")
it 'converts StringResponse with multiple answers and regular expressions to xml', ->
data = MarkdownEditingDescriptor.markdownToXml("""Write a number from 1 to 4.
=| ^One$
or= two
or= ^thre+
or= ^4|Four$
[Explanation]
Test Explanation.
[Explanation]
""")
expect(data).toEqual("""<problem>
<p>Write a number from 1 to 4.</p>
<stringresponse answer="^One$" type="ci regexp" >
<additional_answer>two</additional_answer>
<additional_answer>^thre+</additional_answer>
<additional_answer>^4|Four$</additional_answer>
<textline size="20"/> <textline size="20"/>
</stringresponse> </stringresponse>
......
...@@ -247,13 +247,17 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor ...@@ -247,13 +247,17 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
string += ' <formulaequationinput />\n'; string += ' <formulaequationinput />\n';
string += '</numericalresponse>\n\n'; string += '</numericalresponse>\n\n';
} else { } else {
var answers = []; var firstAnswer = answersList.shift();
if (firstAnswer[0] === '|') { // this is regexp case
string = '<stringresponse answer="' + firstAnswer.slice(1).trim() + '" type="ci regexp" >\n'
}
else {
string = '<stringresponse answer="' + firstAnswer + '" type="ci" >\n'
}
for(var i = 0; i < answersList.length; i++) { for(var i = 0; i < answersList.length; i++) {
answers.push(answersList[i]) string += ' <additional_answer>' + answersList[i] + '</additional_answer>\n'
} }
string += ' <textline size="20"/>\n</stringresponse>\n\n';
string = '<stringresponse answer="' + answers.join('_or_') + '" type="ci">\n <textline size="20"/>\n</stringresponse>\n\n';
} }
return string; return string;
}); });
......
.. raw:: latex .. raw:: latex
\newpage % \newpage %
.. _Appendix E: .. _Appendix E:
...@@ -25,7 +25,7 @@ differences between the two include the following. ...@@ -25,7 +25,7 @@ differences between the two include the following.
Sample Problem: Sample Problem:
.. image:: ../Images/image287.png .. image:: ../Images/image287.png
:width: 600 :width: 600
**Problem Code:** **Problem Code:**
...@@ -70,7 +70,7 @@ Sample Problem: ...@@ -70,7 +70,7 @@ Sample Problem:
<div class="detailed-solution"> <div class="detailed-solution">
</div> </div>
</solution> </solution>
</problem> </problem>
...@@ -88,11 +88,11 @@ Sample Problem: ...@@ -88,11 +88,11 @@ Sample Problem:
.. raw:: latex .. raw:: latex
\newpage % \newpage %
Multiple Choice Multiple Choice
=============== ===============
...@@ -110,14 +110,14 @@ differences between the two include the following. ...@@ -110,14 +110,14 @@ differences between the two include the following.
• The Option Response drop-down input format makes it more likely for students to think of an answer and then search for it, rather than relying purely on recognition to answer the question. • The Option Response drop-down input format makes it more likely for students to think of an answer and then search for it, rather than relying purely on recognition to answer the question.
• The Multiple Choice format is more explicit and visual. This makes it a more appropriate choice for presenting tricky or complicated answer options which areintended to get the student to pause and think. • The Multiple Choice format is more explicit and visual. This makes it a more appropriate choice for presenting tricky or complicated answer options which are intended to get the student to pause and think.
Sample Problem: Sample Problem:
.. image:: ../Images/image289.png .. image:: ../Images/image289.png
:width: 600 :width: 600
**Problem Code:** **Problem Code:**
.. code-block:: xml .. code-block:: xml
...@@ -137,7 +137,7 @@ Sample Problem: ...@@ -137,7 +137,7 @@ Sample Problem:
<solution> <solution>
<div class="detailed-solution"> <div class="detailed-solution">
<p>Explanation</p> <p>Explanation</p>
<p>It depends on how many choices are marked as correct in the underlying XML.</p> <p>It depends on how many choices are marked as correct in the underlying XML.</p>
<p>Note that if all choices are marked as incorrect, there is no <p>Note that if all choices are marked as incorrect, there is no
correct response.</p> correct response.</p>
</div> </div>
...@@ -145,7 +145,7 @@ Sample Problem: ...@@ -145,7 +145,7 @@ Sample Problem:
</problem> </problem>
**Template** **Template**
.. code-block:: xml .. code-block:: xml
...@@ -185,7 +185,7 @@ Sample Problem: ...@@ -185,7 +185,7 @@ Sample Problem:
.. raw:: latex .. raw:: latex
\newpage % \newpage %
...@@ -206,7 +206,7 @@ have zero correct responses. ...@@ -206,7 +206,7 @@ have zero correct responses.
Sample Problem: Sample Problem:
.. image:: ../Images/image290.png .. image:: ../Images/image290.png
:width: 600 :width: 600
**Problem Code:** **Problem Code:**
...@@ -244,7 +244,7 @@ Sample Problem: ...@@ -244,7 +244,7 @@ Sample Problem:
</problem> </problem>
.. raw:: latex .. raw:: latex
\newpage % \newpage %
...@@ -266,7 +266,7 @@ clear. ...@@ -266,7 +266,7 @@ clear.
Sample Problem: Sample Problem:
.. image:: ../Images/image291.png .. image:: ../Images/image291.png
:width: 600 :width: 600
**Problem Code:** **Problem Code:**
...@@ -301,19 +301,104 @@ Sample Problem: ...@@ -301,19 +301,104 @@ Sample Problem:
</solution> </solution>
</problem> </problem>
This response type allows to add more than one answer. Use `additional_answer` tag to add more answers.
You can add `regexp` to value of `type` attribute, for example: `type="ci regexp"` or `type="regexp"` or `type="regexp cs"`.
In this case, any answer and hint will be treated as regular expressions.
Regular expression has to match whole answer, for answer to be correct.
Student answers "foobar", "o foo" or " ==foo==", will be correct if teacher has set answer=".*foo.*" with type="regexp".
**Template**
.. code-block:: xml
<problem>
<stringresponse answer="a1" type="ci regexp">
<additional_answer>\d5</additional_answer>
<additional_answer>a3</additional_answer>
<textline size="20"/>
<hintgroup>
<stringhint answer="a0" type="ci" name="ha0" />
<stringhint answer="a4" type="ci" name="ha4" />
<stringhint answer="^\d" type="ci" name="re1" />
<hintpart on="ha0">
<startouttext />+1<endouttext />
</hintpart >
<hintpart on="ha4">
<startouttext />-1<endouttext />
</hintpart >
<hintpart on="re1">
<startouttext />Any number+5<endouttext />
</hintpart >
</hintgroup>
</stringresponse>
</problem>
**XML Attribute Information** **XML Attribute Information**
<stringresponse> <stringresponse>
.. image:: ../Images/stringresponse.png .. raw:: html
<table border="1" class="docutils" width="60%">
<colgroup>
<col width="15%">
<col width="75%">
<col width="10%">
</colgroup>
<thead valign="bottom">
<tr class="row-odd"><th class="head">Attribute</th>
<th class="head">Description</th>
<th class="head">Notes</th>
</tr>
</thead>
<tbody valign="top">
<tr class="row-even"><td>type</td>
<td>(optional) “[ci] [regex]”. Add “ci” if the student response should be graded case-insensitively. The default is to take case into consideration when grading. Add “regexp” for correct answer to be treated as regular expression.</td>
<td>&nbsp;</td>
</tr>
<tr class="row-odd"><td>answer</td>
<td>The string that is used to compare with student answer. If "regexp" is not presented in value of <em>type</em> attribute, student should enter value equal to exact value of this attribute in order to get credit. If "regexp" is presented in value of <em>type</em> attribute, value of <em>answer</em> is treated as regular expression and exact match of this expression and student answer will be done. If search is successful, student will get credit.</td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
<table border="1" class="docutils" width="60%">
<colgroup>
<col width="15%">
<col width="75%">
<col width="10%">
</colgroup>
<thead valign="bottom">
<tr class="row-odd"><th class="head">Children</th>
<th class="head">Description</th>
<th class="head">Notes</th>
</tr>
</thead>
<tbody valign="top">
<tr class="row-even"><td>textline</td>
<td>used to accept student input. See description below.</td>
<td>&nbsp;</td>
</tr>
<tr class="row-odd"><td>additional_answer</td>
<td>todo</td>
<td>&nbsp;</td>
</tr>
</tbody>
</table>
<textline> <textline>
.. image:: ../Images/stringresponse2.png .. image:: ../Images/stringresponse2.png
<additional_answer> - Can be unlimited number of this tags. Any tag adds one more additional answer for matching.
.. raw:: latex .. raw:: latex
\newpage % \newpage %
...@@ -339,7 +424,7 @@ only, and the examples below show its use. ...@@ -339,7 +424,7 @@ only, and the examples below show its use.
Sample Problem: Sample Problem:
.. image:: ../Images/image292.png .. image:: ../Images/image292.png
:width: 600 :width: 600
**Problem Code**: **Problem Code**:
...@@ -366,7 +451,7 @@ Sample Problem: ...@@ -366,7 +451,7 @@ Sample Problem:
<script type="loncapa/python"> <script type="loncapa/python">
computed_response = math.sqrt(math.fsum([math.pow(math.pi,2), math.pow(math.e,2)])) computed_response = math.sqrt(math.fsum([math.pow(math.pi,2), math.pow(math.e,2)]))
</script> </script>
<p>What is the distance in the plane between the points (pi, 0) and (0, e)? You can type math. <p>What is the distance in the plane between the points (pi, 0) and (0, e)? You can type math.
<numericalresponse answer="$computed_response"> <numericalresponse answer="$computed_response">
<responseparam type="tolerance" default="0.0001" /> <responseparam type="tolerance" default="0.0001" />
...@@ -383,7 +468,7 @@ Sample Problem: ...@@ -383,7 +468,7 @@ Sample Problem:
the square root of the sum of the squares of the differences of each coordinate. the square root of the sum of the squares of the differences of each coordinate.
Even though an exact numerical value is checked in this case, the Even though an exact numerical value is checked in this case, the
easiest way to enter this answer is to type easiest way to enter this answer is to type
<code>sqrt(pi^2+e^2)</code> into the editor. <code>sqrt(pi^2+e^2)</code> into the editor.
Other answers like <code>sqrt((pi-0)^2+(0-e)^2)</code> also work. Other answers like <code>sqrt((pi-0)^2+(0-e)^2)</code> also work.
</p> </p>
</div> </div>
...@@ -589,7 +674,7 @@ default. These include: ...@@ -589,7 +674,7 @@ default. These include:
Operators and Functions Operators and Functions
~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~
As expected, the normal operators apply (with normal order of operations): As expected, the normal operators apply (with normal order of operations):
``+ - * / ^``. Also provided is a special "parallel resistors" operator given ``+ - * / ^``. Also provided is a special "parallel resistors" operator given
by ``||``. For example, an input of ``1 || 2`` would represent the resistance by ``||``. For example, an input of ``1 || 2`` would represent the resistance
of a pair of parallel resistors (of resistance 1 and 2 ohms), evaluating to 2/3 of a pair of parallel resistors (of resistance 1 and 2 ohms), evaluating to 2/3
...@@ -610,7 +695,7 @@ The default included functions are the following: ...@@ -610,7 +695,7 @@ The default included functions are the following:
coth, arcsinh, arccosh, arctanh, arcsech, arccsch, arccoth coth, arcsinh, arccosh, arctanh, arcsech, arccsch, arccoth
.. raw:: latex .. raw:: latex
\newpage % \newpage %
...@@ -651,7 +736,7 @@ backward 3. ...@@ -651,7 +736,7 @@ backward 3.
Sample Problem: Sample Problem:
.. image:: ../Images/image293.png .. image:: ../Images/image293.png
:width: 600 :width: 600
**Problem Code**: **Problem Code**:
...@@ -663,7 +748,7 @@ Sample Problem: ...@@ -663,7 +748,7 @@ Sample Problem:
<p>Write an expression for the product of R_1, R_2, and the inverse of R_3.</p> <p>Write an expression for the product of R_1, R_2, and the inverse of R_3.</p>
<formularesponse type="ci" samples="R_1,R_2,R_3@1,2,3:3,4,5#10" answer="$VoVi"> <formularesponse type="ci" samples="R_1,R_2,R_3@1,2,3:3,4,5#10" answer="$VoVi">
<responseparam type="tolerance" default="0.00001"/> <responseparam type="tolerance" default="0.00001"/>
<formulaequationinput size="40" /> <formulaequationinput size="40" />
</formularesponse> </formularesponse>
...@@ -672,7 +757,7 @@ Sample Problem: ...@@ -672,7 +757,7 @@ Sample Problem:
VoVi = "(R_1*R_2)/R_3" VoVi = "(R_1*R_2)/R_3"
</script> </script>
<formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="m*c^2"> <formularesponse type="cs" samples="m,c@1,2:3,4#10" answer="m*c^2">
<responseparam type="tolerance" default="0.00001"/> <responseparam type="tolerance" default="0.00001"/>
<text><i>E</i> =</text> <formulaequationinput size="40"/> <text><i>E</i> =</text> <formulaequationinput size="40"/>
</formularesponse> </formularesponse>
...@@ -681,13 +766,13 @@ Sample Problem: ...@@ -681,13 +766,13 @@ Sample Problem:
derivative = "n*x^(n-1)" derivative = "n*x^(n-1)"
</script> </script>
<formularesponse type="ci" samples="x,n@1,2:3,4#10" answer="$derivative"> <formularesponse type="ci" samples="x,n@1,2:3,4#10" answer="$derivative">
<responseparam type="tolerance" default="0.00001"/> <responseparam type="tolerance" default="0.00001"/>
<formulaequationinput size="40" /> <formulaequationinput size="40" />
</formularesponse> </formularesponse>
<!-- Example problem specifying only one variable --> <!-- Example problem specifying only one variable -->
<formularesponse type="ci" samples="x@1,9#10" answer="x**2 - x + 4"> <formularesponse type="ci" samples="x@1,9#10" answer="x**2 - x + 4">
<responseparam type="tolerance" default="0.00001"/> <responseparam type="tolerance" default="0.00001"/>
<formulaequationinput size="40" /> <formulaequationinput size="40" />
</formularesponse> </formularesponse>
...@@ -736,7 +821,7 @@ size (optional) defines the size (i.e. the width) ...@@ -736,7 +821,7 @@ size (optional) defines the size (i.e. the width)
========= ============================================= ===== ========= ============================================= =====
.. raw:: latex .. raw:: latex
\newpage % \newpage %
...@@ -751,7 +836,7 @@ Images have to be uploaded to the courseware Assets directory. Response clicks a ...@@ -751,7 +836,7 @@ Images have to be uploaded to the courseware Assets directory. Response clicks a
Sample Problem: Sample Problem:
.. image:: ../Images/image294.png .. image:: ../Images/image294.png
:width: 600 :width: 600
**Problem Code**: **Problem Code**:
...@@ -771,7 +856,7 @@ Sample Problem: ...@@ -771,7 +856,7 @@ Sample Problem:
<problem> <problem>
<imageresponse> <imageresponse>
<imageinput src="Path_to_Image_File.png" width="220" height="150" rectangle="(80,40)-(130,90)" /> <imageinput src="Path_to_Image_File.png" width="220" height="150" rectangle="(80,40)-(130,90)" />
</imageresponse> </imageresponse>
</problem> </problem>
XML Attribute Information XML Attribute Information
...@@ -786,7 +871,7 @@ XML Attribute Information ...@@ -786,7 +871,7 @@ XML Attribute Information
.. image:: ../Images/imageresponse2.png .. image:: ../Images/imageresponse2.png
.. raw:: latex .. raw:: latex
\newpage % \newpage %
.. _Custom Response: .. _Custom Response:
...@@ -799,7 +884,7 @@ A Custom Response input type accepts one or more lines of text input from the st ...@@ -799,7 +884,7 @@ A Custom Response input type accepts one or more lines of text input from the st
Sample Problem: Sample Problem:
.. image:: ../Images/image295.png .. image:: ../Images/image295.png
:width: 600 :width: 600
**Problem Code**: **Problem Code**:
...@@ -846,7 +931,7 @@ Sample Problem: ...@@ -846,7 +931,7 @@ Sample Problem:
<div class="detailed-solution"> <div class="detailed-solution">
<p>Explanation</p> <p>Explanation</p>
<p>For the first part, any two numbers of the form <i>n</i> <p>For the first part, any two numbers of the form <i>n</i>
and <i>10-n</i>, where <i>n</i> is any integer, will work. and <i>10-n</i>, where <i>n</i> is any integer, will work.
One possible answer would be the pair 0 and 10. One possible answer would be the pair 0 and 10.
</p> </p>
<p>For the second part, any pair <i>x</i> and <i>20-x</i> will work, where <i>x</i> is any real number with a finite decimal representation. Both inputs have to be entered either in standard decimal notation or in scientific exponential notation. One possible answer would be the pair 0.5 and 19.5. Another way to write this would be 5e-1 and 1.95e1. <p>For the second part, any pair <i>x</i> and <i>20-x</i> will work, where <i>x</i> is any real number with a finite decimal representation. Both inputs have to be entered either in standard decimal notation or in scientific exponential notation. One possible answer would be the pair 0.5 and 19.5. Another way to write this would be 5e-1 and 1.95e1.
...@@ -915,7 +1000,7 @@ Sample Problem: ...@@ -915,7 +1000,7 @@ Sample Problem:
.. raw:: latex .. raw:: latex
\newpage % \newpage %
.. _Chemical Equation Response: .. _Chemical Equation Response:
...@@ -924,12 +1009,12 @@ Chemical Equation Response ...@@ -924,12 +1009,12 @@ Chemical Equation Response
========================== ==========================
The Chemical Equation Response input type is a special type of Custom Response The Chemical Equation Response input type is a special type of Custom Response
that allows the student to enter chemical equations as answers. that allows the student to enter chemical equations as answers.
Sample Problem: Sample Problem:
.. image:: ../Images/image296.png .. image:: ../Images/image296.png
:width: 600 :width: 600
**Problem Code**: **Problem Code**:
...@@ -945,7 +1030,7 @@ Sample Problem: ...@@ -945,7 +1030,7 @@ Sample Problem:
<chemicalequationinput size="50"/> <chemicalequationinput size="50"/>
<answer type="loncapa/python"> <answer type="loncapa/python">
if chemcalc.chemical_equations_equal(submission[0], 'H2SO4 -> H^+ + HSO4^-'): if chemcalc.chemical_equations_equal(submission[0], 'H2SO4 -> H^+ + HSO4^-'):
correct = ['correct'] correct = ['correct']
else: else:
correct = ['incorrect'] correct = ['incorrect']
...@@ -955,28 +1040,28 @@ Sample Problem: ...@@ -955,28 +1040,28 @@ Sample Problem:
<p> Some tips:<ul><li>Only real element symbols are permitted.</li><li>Subscripts are entered with plain text.</li><li>Superscripts are indicated with a caret (^).</li><li>The reaction arrow (\(\longrightarrow\)) is indicated with "->".</li></ul> <p> Some tips:<ul><li>Only real element symbols are permitted.</li><li>Subscripts are entered with plain text.</li><li>Superscripts are indicated with a caret (^).</li><li>The reaction arrow (\(\longrightarrow\)) is indicated with "->".</li></ul>
So, you can enter "H2SO4 -> H^+ + HSO4^-".</p> So, you can enter "H2SO4 -> H^+ + HSO4^-".</p>
<endouttext/> <endouttext/>
</problem> </problem>
.. raw:: latex .. raw:: latex
\newpage % \newpage %
Schematic Response Schematic Response
================== ==================
The Schematic Response input type provides an interactive grid on which the The Schematic Response input type provides an interactive grid on which the
student can construct a schematic answer, such as a circuit. student can construct a schematic answer, such as a circuit.
Sample Problem: Sample Problem:
.. image:: ../Images/image297.png .. image:: ../Images/image297.png
:width: 600 :width: 600
.. image:: ../Images/image298.png .. image:: ../Images/image298.png
:width: 600 :width: 600
.. image:: ../Images/image299.png .. image:: ../Images/image299.png
:width: 600 :width: 600
**Problem Code**: **Problem Code**:
......
...@@ -17,7 +17,7 @@ ...@@ -17,7 +17,7 @@
# Our libraries: # Our libraries:
-e git+https://github.com/edx/XBlock.git@cd77808aadd3ea1c2027ca8c0aa5624d8ccccc52#egg=XBlock -e git+https://github.com/edx/XBlock.git@cd77808aadd3ea1c2027ca8c0aa5624d8ccccc52#egg=XBlock
-e git+https://github.com/edx/codejail.git@e3d98f9455#egg=codejail -e git+https://github.com/edx/codejail.git@e3d98f9455#egg=codejail
-e git+https://github.com/edx/diff-cover.git@v0.2.6#egg=diff_cover -e git+https://github.com/edx/diff-cover.git@v0.2.9#egg=diff_cover
-e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool -e git+https://github.com/edx/js-test-tool.git@v0.1.5#egg=js_test_tool
-e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle -e git+https://github.com/edx/django-waffle.git@823a102e48#egg=django-waffle
-e git+https://github.com/edx/event-tracking.git@f0211d702d#egg=event-tracking -e git+https://github.com/edx/event-tracking.git@f0211d702d#egg=event-tracking
......
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