From 846fc98e6b64534e0e48852efc3d928ef1c4f135 Mon Sep 17 00:00:00 2001 From: rtibblesbot Date: Wed, 19 Aug 2026 14:03:36 -0700 Subject: [PATCH 1/3] fix: convert the legacy markdown the QTI models rejected Inline maths: li, td, th, caption, dt, dd and qti-simple-choice took no math, though the XSD admits m3:math in all seven. Links and inline marks (a, s, del, ins, u, mark, strike): unwrapped in the rendered markup, keeping their text. An anchor has nothing to navigate to on a device with no internet access, and the QTI 3.0 HTML profile has no element for the marks. Publish and ricecooker upload reach the same converter, so both stop failing on these items too. --- .../tests/utils/qti/test_convert.py | 106 ++++++++++++++++++ .../utils/assessment/qti/convert.py | 18 ++- .../utils/assessment/qti/html/__init__.py | 2 + .../utils/assessment/qti/html/sequence.py | 8 +- .../utils/assessment/qti/html/table.py | 8 +- .../qti/interaction_types/simple.py | 4 +- 6 files changed, 135 insertions(+), 11 deletions(-) diff --git a/contentcuration/contentcuration/tests/utils/qti/test_convert.py b/contentcuration/contentcuration/tests/utils/qti/test_convert.py index 63d5ec32e2..526b1aed95 100644 --- a/contentcuration/contentcuration/tests/utils/qti/test_convert.py +++ b/contentcuration/contentcuration/tests/utils/qti/test_convert.py @@ -239,6 +239,112 @@ def test_free_response_with_maths(self): ) +class MarkdownContentConversionTests(unittest.TestCase): + """Markdown a legacy question can hold that the QTI models rejected.""" + + def _convert(self, question="Question", answers=None, hints=None): + item = _make_item( + type=exercises.SINGLE_SELECTION, + question=question, + answers=answers + if answers is not None + else [{"answer": "4", "correct": True, "order": 1}], + assessment_id="abcdef1234567890abcdef1234567890", + hints=hints, + ) + return convert_legacy_assessment_item_to_qti(item) + + def test_links_are_stripped(self): + # A QTI item is delivered offline, so the link text survives and the + # anchor does not. + cases = ( + ("Read [the docs](https://learningequality.org).", "Read the docs."), + ("Read [the docs](./docs.html).", "Read the docs."), + ( + "See for more.", + "See https://learningequality.org for more.", + ), + ( + 'Read this.', + "Read this.", + ), + ) + for markdown, expected in cases: + with self.subTest(markdown=markdown): + result = self._convert(markdown) + + self.assertIn(expected, result.xml) + self.assertNotIn("Read the docs.
Read the docs.
See https://learningequality.org for more.
Read this.
See here
It is not four.
What is 2+2?