Repository navigation
[QTI] Perseus export publishes wrong answers for input questions that aren't plain numbers #6195
Description
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Overview
parser.extract_value accepts an input question answer when a number appears anywhere in it, and publishes that number as the correct answer, so learners who enter the intended answer are marked wrong. This affects 22 items in 4 live channels.
Complexity: Low
Target branch: unstable
Context
- Every parse function in
parser.pyusesre.search, so it matches a number anywhere in the answer:studio/contentcuration/contentcuration/utils/parser.py
Lines 103 to 141 in 8cd2daf
def parse_integer(text): match = INTEGER.search(text) return match and float(to_en(match.group(1))) def parse_decimal(text): match = DECIMAL.search(text) return match and float(to_en(match.group(1))) def parse_fraction(text): match = FRACTION.search(text) return match and float(parse_integer(match.group(2))) / float( parse_integer(match.group(3)) ) def parse_mixed_number(text): match = MIXED_NUMBER.search(text) if match: number = parse_integer(match.group(1)) return (abs(number) + parse_fraction(match.group(3))) * ( float(number) / float(abs(number)) ) return None def parse_percentage(text): match = PERCENTAGE.search(text) return match and extract_value(match.group(1)) / float(100) def parse_exponent(text): eval_str = None match = EXPONENT.search(text) if match: val1 = extract_value(match.group(2) or match.group(4)) val2 = extract_value(match.group(5)) eval_str = val1 and val2 and "{int}e{exp}".format(int=val1, exp=int(val2)) - The Perseus export replaces each input answer with the result of
extract_value:studio/contentcuration/contentcuration/utils/assessment/perseus.py
Lines 74 to 80 in 8cd2daf
def _process_input_answers(self, processed_data): """Extract input answer processing logic""" non_empty_answers = [] for answer in processed_data["answers"]: answer["answer"] = extract_value(answer["answer"]) if answer["answer"] or answer["answer"] == 0: non_empty_answers.append(answer) - Stored answers and what they publish as:
4\.62 -> 0.62 249\.6 -> 0.6 8\.5 -> 0.5 0\.0025 -> 0.0025 1:3 -> 1.0
The Change
extract_valueshould return a number only when the whole answer is one, and reject anything else (e.g.1:3) instead of pulling a number out of part of it.- A markdown-escaped decimal point should parse as a plain one:
4\.62reads as4.62.
How to Get There
- Create an exercise with an input question whose answer is
4\.62, publish the channel, and open the item's JSON in the published.perseusfile: the numeric-input answer value is0.62. - Or call
extract_value("4\\.62")directly.
Out of Scope
- Rewriting the stored
\.-escaped answers. - Finding which editor or import wrote the escapes.
Acceptance Criteria
General
-
extract_valuereturns4.62,249.6,8.5and0.0025for4\.62,249\.6,8\.5and0\.0025. -
extract_valuereturnsNonefor1:3,5 apples, and thetest_parser.pycases that only parse part of the answer:1,00,00,1.-2,1.2,00,1/2/123,12/0,1.1.23,1,000e+-3. - The other existing
test_parser.pycases return the same values as today.
Testing
-
test_parser.pycovers escaped decimals and answers with text around a number.
Testing
pytest contentcuration/contentcuration/tests/test_parser.py
AI usage
I directed a survey of input question answers on production and chose this issue's scope, including which formats parse and which are rejected. Claude Code wrote and ran the survey scripts, traced the wrong answers to extract_value, and drafted this issue section by section.

Activity
rtibblesbot commentedon Sep 25, 2026
Plan:
extract_valuereturns a number only when the whole answer is one, and reads\.as a decimal point.Assumptions — the issue did not settle these; say so if one is wrong.
1,000e+-3fully matchesEXPONENTtoday, so the AC'sNonemeans rejectinge+-; if only partial parses should change, it stays 1.0.4.62drops from the export.\.is unescaped; if other escapes should parse,\-5and5\%keep dropping from the export.None; if they should parse,10(tests/fixtures/tree.json:96) keeps dropping from the export.convert.py:223-236still publishes4\.62as a string answer.@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?