diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 9dfd50836f..12bee69b01 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -309,6 +309,9 @@ class OptionInput(InputTypeBase): Given options string, convert it into an ordered list of (option_id, option_description) tuples, where id==description for now. TODO: make it possible to specify different id and descriptions. """ + # convert single quotes inside option values to html encoded string + options = re.sub(r"([a-zA-Z])('|\\')([a-zA-Z])", r"\1'\3", options) + options = re.sub(r"\\'", r"'", options) # replace already escaped single quotes # parse the set of possible options lexer = shlex.shlex(options[1:-1].encode('utf8')) lexer.quotes = "'" @@ -316,7 +319,8 @@ class OptionInput(InputTypeBase): lexer.whitespace = ", " # remove quotes - tokens = [x[1:-1].decode('utf8') for x in lexer] + # convert escaped single quotes (html encoded string) back to single quotes + tokens = [x[1:-1].decode('utf8').replace("'", "'") for x in lexer] # make list of (option_id, option_description), with description=id return [(t, t) for t in tokens] diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 6d7a8e1ce9..b878220edb 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -41,7 +41,7 @@ class OptionInputTest(unittest.TestCase): ''' def test_rendering(self): - xml_str = """""" + xml_str = """""" element = etree.fromstring(xml_str) state = {'value': 'Down', @@ -54,7 +54,7 @@ class OptionInputTest(unittest.TestCase): expected = { 'STATIC_URL': '/dummy-static/', 'value': 'Down', - 'options': [('Up', 'Up'), ('Down', 'Down')], + 'options': [('Up', 'Up'), ('Down', 'Down'), ('Don\'t know', 'Don\'t know')], 'status': 'answered', 'msg': '', 'inline': False, @@ -80,6 +80,10 @@ class OptionInputTest(unittest.TestCase): check(u"('б в','в')", [u'б в', u'в']) check(u"('Мой \"кавыки\"место','в')", [u'Мой \"кавыки\"место', u'в']) + # check that escaping single quotes with leading backslash (\') properly works + # note: actual input by user will be hasn\'t but json parses it as hasn\\'t + check(u"('hasnt','hasn't')", [u'hasnt', u'hasn\'t']) + class ChoiceGroupTest(unittest.TestCase): ''' diff --git a/common/lib/capa/capa/tests/test_responsetypes.py b/common/lib/capa/capa/tests/test_responsetypes.py index 6b400dc653..1c6ccd67c3 100644 --- a/common/lib/capa/capa/tests/test_responsetypes.py +++ b/common/lib/capa/capa/tests/test_responsetypes.py @@ -345,6 +345,17 @@ class OptionResponseTest(ResponseTest): # Options not in the list should be marked incorrect self.assert_grade(problem, "invalid_option", "incorrect") + def test_quote_option(self): + # Test that option response properly escapes quotes inside options strings + problem = self.build_problem(options=["hasnot", "hasn't", "has'nt"], + correct_option="hasn't") + + # Assert that correct option with a quote inside is marked correctly + self.assert_grade(problem, "hasnot", "incorrect") + self.assert_grade(problem, "hasn't", "correct") + self.assert_grade(problem, "hasn\'t", "correct") + self.assert_grade(problem, "has'nt", "incorrect") + class FormulaResponseTest(ResponseTest): """