Commit cbe97d6f by Alexander Kryklia

Merge pull request #1948 from edx/anton/allow-arbitrary-regex-in-stringresponse

Anton/allow arbitrary regex in stringresponse
parents de6eb99b ee1953b3
......@@ -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
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
- ALLOW_ALL_ADVANCED_COMPONENTS disables the hard-coded list of advanced
components in Studio, and allows any xblock to be added as an
......
......@@ -329,8 +329,8 @@ class LoncapaResponse(object):
rephints = hintgroup.findall(self.hint_tag)
hints_to_show = self.check_hint_condition(
rephints, student_answers)
# can be 'on_request' or 'always' (default)
hintmode = hintgroup.get('mode', 'always')
for hintpart in hintgroup.findall('hintpart'):
if hintpart.get('on') in hints_to_show:
......@@ -947,21 +947,35 @@ class NumericalResponse(LoncapaResponse):
class StringResponse(LoncapaResponse):
'''
This response type allows one or more answers. Use `_or_` separator to set
more than 1 answer.
This response type allows one or more answers.
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">
<textline size="20" />
</stringresponse >
# Multiple answers
<stringresponse answer="Martin Luther King_or_Dr. Martin Luther King Jr.">
<textline size="20" />
<textline size="20" />
</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'
hint_tag = 'stringhint'
......@@ -969,11 +983,30 @@ class StringResponse(LoncapaResponse):
required_attributes = ['answer']
max_inputfields = 1
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):
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):
'''Grade a string response '''
......@@ -981,21 +1014,61 @@ class StringResponse(LoncapaResponse):
correct = self.check_string(self.correct_answer, student_answer)
return CorrectMap(self.answer_id, 'correct' if correct else 'incorrect')
def check_string(self, expected, given):
if self.xml.get('type') == 'ci':
def check_string_backward(self, expected, given):
if self.case_insensitive:
return given.lower() in [i.lower() for i 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):
given = student_answers[self.answer_id].strip()
hints_to_show = []
for hxml in hxml_set:
name = hxml.get('name')
correct_answer = [contextualize_text(answer, self.context).strip()
for answer in hxml.get('answer').split(self.SEPARATOR)]
hinted_answer = contextualize_text(hxml.get('answer'), self.context).strip()
if self.check_string(correct_answer, given):
if self.check_string([hinted_answer], given):
hints_to_show.append(name)
log.debug('hints_to_show = %s', hints_to_show)
return hints_to_show
......
......@@ -690,22 +690,30 @@ class StringResponseXMLFactory(ResponseXMLFactory):
*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
answer = kwargs.get("answer", None)
case_sensitive = kwargs.get("case_sensitive", True)
hint_list = kwargs.get('hints', None)
hint_fn = kwargs.get('hintfn', None)
regexp = kwargs.get('regexp', None)
additional_answers = kwargs.get('additional_answers', [])
assert answer
# Create the <stringresponse> element
response_element = etree.Element("stringresponse")
# Set the answer attribute
response_element.set("answer", str(answer))
response_element.set("answer", unicode(answer))
# Set the case sensitivity
response_element.set("type", "cs" if case_sensitive else "ci")
# Set the case sensitivity and regexp:
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
if hint_list or hint_fn:
......@@ -727,6 +735,9 @@ class StringResponseXMLFactory(ResponseXMLFactory):
assert not hint_list
hintgroup_element.set("hintfn", hint_fn)
for additional_answer in additional_answers:
etree.SubElement(response_element, "additional_answer").text = additional_answer
return response_element
def create_input_element(self, **kwargs):
......
......@@ -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>Which US state has Lansing as its capital?</p>
<stringresponse answer="Michigan" type="ci">
<stringresponse answer="Michigan" type="ci" >
<textline size="20"/>
</stringresponse>
......@@ -283,6 +283,29 @@ describe 'MarkdownEditingDescriptor', ->
</div>
</solution>
</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', ->
data = MarkdownEditingDescriptor.markdownToXml("""Who lead the civil right movement in the United States of America?
= Dr. Martin Luther King Jr.
......@@ -296,7 +319,39 @@ describe 'MarkdownEditingDescriptor', ->
""")
expect(data).toEqual("""<problem>
<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"/>
</stringresponse>
......
......@@ -247,13 +247,17 @@ class @MarkdownEditingDescriptor extends XModule.Descriptor
string += ' <formulaequationinput />\n';
string += '</numericalresponse>\n\n';
} 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++) {
answers.push(answersList[i])
string += ' <additional_answer>' + answersList[i] + '</additional_answer>\n'
}
string = '<stringresponse answer="' + answers.join('_or_') + '" type="ci">\n <textline size="20"/>\n</stringresponse>\n\n';
string += ' <textline size="20"/>\n</stringresponse>\n\n';
}
return string;
});
......
......@@ -17,7 +17,7 @@
# Our libraries:
-e git+https://github.com/edx/XBlock.git@a1a3e76b269d15b7bbd11976d8aef63e1db6c4c2#egg=XBlock
-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/django-waffle.git@823a102e48#egg=django-waffle
-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