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):
"""