Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<qti-assessment-item xmlns="http://www.imsglobal.org/xsd/imsqtiasi_v3p0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqtiasi_v3p0 https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0p1_v1p0.xsd" identifier="Kq83vEjRWeJCrze8SNFZ4kA" title="Test Question 1" adaptive="false" time-dependent="false" xml:lang="en-US" tool-name="kolibri" tool-version="0.1">
<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="identifier" />
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float" />
<qti-item-body>
<qti-choice-interaction response-identifier="RESPONSE" shuffle="true" max-choices="1" min-choices="0" orientation="vertical">
<qti-prompt>
<p>What is 2+2?</p>
</qti-prompt>
<qti-simple-choice identifier="choice_0" show-hide="show" fixed="false" />
</qti-choice-interaction>
</qti-item-body>
<qti-response-processing template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct" />
</qti-assessment-item>
157 changes: 157 additions & 0 deletions contentcuration/contentcuration/tests/utils/qti/test_convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,57 @@ def test_true_false(self):
)
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)

def test_single_selection_no_answers(self):
item = _make_item(
type=exercises.SINGLE_SELECTION,
question="What is 2+2?",
answers=[],
randomize=True,
assessment_id="abcdef1234567890abcdef1234567890",
)

result = convert_legacy_assessment_item_to_qti(item)

self.assertEqual(result.identifier, "Kq83vEjRWeJCrze8SNFZ4kA")
self.assertEqual(
_normalize_xml(_load_fixture("single_selection_no_answers.xml")),
_normalize_xml(result.xml),
)
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)

def test_choice_types_with_no_answers_get_one_empty_choice(self):
# test_single_selection_no_answers pins SINGLE_SELECTION against the fixture.
for question_type in (exercises.MULTIPLE_SELECTION, "true_false"):
with self.subTest(question_type=question_type):
item = _make_item(
type=question_type,
question="What is 2+2?",
answers=[],
assessment_id="abcdef1234567890abcdef1234567890",
)

result = convert_legacy_assessment_item_to_qti(item)

self.assertIn(
'<qti-simple-choice identifier="choice_0" show-hide="show" fixed="false" />',
result.xml,
)
self.assertIn("<p>What is 2+2?</p>", result.xml)
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)

def test_choice_type_with_no_answers_and_no_question(self):
item = _make_item(
type=exercises.MULTIPLE_SELECTION,
question="",
answers=[],
assessment_id="abcdef1234567890abcdef1234567890",
)

result = convert_legacy_assessment_item_to_qti(item)

self.assertIn("<qti-prompt />", result.xml)
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)

def test_media_reference_survives(self):
item = _make_item(
type=exercises.SINGLE_SELECTION,
Expand Down Expand Up @@ -239,6 +290,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).", "<p>Read the docs.</p>"),
("Read [the docs](./docs.html).", "<p>Read the docs.</p>"),
(
"See <https://learningequality.org> for more.",
"<p>See https://learningequality.org for more.</p>",
),
(
'Read <a href="https://learningequality.org">this</a>.',
"<p>Read this.</p>",
),
)
for markdown, expected in cases:
with self.subTest(markdown=markdown):
result = self._convert(markdown)

self.assertIn(expected, result.xml)
self.assertNotIn("<a ", result.xml)
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)

def test_link_markup_inside_the_anchor_survives(self):
result = self._convert("Read [**the docs**](https://learningequality.org).")

self.assertIn("<p>Read <strong>the docs</strong>.</p>", result.xml)

def test_link_in_an_answer_is_stripped(self):
result = self._convert(
answers=[
{"answer": "See [here](https://learningequality.org)", "correct": True}
]
)

self.assertIn("<p>See here</p>", result.xml)
self.assertNotIn("<a ", result.xml)
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)

def test_link_in_a_hint_is_stripped(self):
result = self._convert(
hints=[{"hint": "Look at [the docs](https://learningequality.org)"}]
)

self.assertIn("<p>Look at the docs</p>", result.xml)
self.assertNotIn("<a ", result.xml)
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)

def test_image_is_not_stripped(self):
result = self._convert("![alt](image.png) illustrates it.")

self.assertIn('<img alt="alt" src="image.png"', result.xml)

def test_strikethrough_is_stripped(self):
# The QTI 3.0 HTML profile has no element for a strikethrough, so the
# text survives and the mark does not.
result = self._convert("It is ~~not~~ four.")

self.assertIn("<p>It is not four.</p>", result.xml)
self.assertNotIn("<s>", result.xml)
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)

def test_raw_html_marks_are_stripped(self):
# Every inline mark the renderer passes through as raw HTML that the QTI
# 3.0 HTML profile has no element for.
for tag in ("s", "del", "ins", "u", "mark", "strike"):
with self.subTest(tag=tag):
result = self._convert(f"It is <{tag}>not</{tag}> four.")

self.assertIn("<p>It is not four.</p>", result.xml)
self.assertNotIn(f"<{tag}>", result.xml)
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)

def test_maths_in_a_list_item(self):
# Validity is not asserted here or below: rendered MathML carries no
# namespace, the same gap test_free_response_with_maths lives with.
result = self._convert("- $$x^2$$\n- two")

self.assertIn('<li><math display="block">', result.xml)

def test_maths_in_a_table_cell(self):
result = self._convert("| a |\n|---|\n| $$x^2$$ |")

self.assertIn('<td><math display="inline">', result.xml)

def test_maths_in_an_answer(self):
result = self._convert(answers=[{"answer": "$$x^2$$", "correct": True}])

self.assertIn('><math display="block">', result.xml)


class CustomInteractionTests(unittest.TestCase):
ASSESSMENT_ID = "2b1c3d4e5f60718293a4b5c6d7e8f900"

Expand Down
Loading
Loading