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):
......
...@@ -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;
}); });
......
...@@ -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