Skip to content

Commit 1578bcb

Browse files
committed
fix: give an answerless legacy choice question an empty choice interaction
Every newly added question is one: the editor writes type single_selection with no answers, and the XSD requires at least one qti-simple-choice, so conversion raised. Stand in the single empty choice the QTI editor opens a new choice interaction with.
1 parent 273f754 commit 1578bcb

3 files changed

Lines changed: 71 additions & 1 deletion

File tree

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<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">
3+
<qti-response-declaration identifier="RESPONSE" cardinality="single" base-type="identifier" />
4+
<qti-outcome-declaration identifier="SCORE" cardinality="single" base-type="float" />
5+
<qti-item-body>
6+
<qti-choice-interaction response-identifier="RESPONSE" shuffle="true" max-choices="1" min-choices="0" orientation="vertical">
7+
<qti-prompt>
8+
<p>What is 2+2?</p>
9+
</qti-prompt>
10+
<qti-simple-choice identifier="choice_0" show-hide="show" fixed="false" />
11+
</qti-choice-interaction>
12+
</qti-item-body>
13+
<qti-response-processing template="https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct" />
14+
</qti-assessment-item>

‎contentcuration/contentcuration/tests/utils/qti/test_convert.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,57 @@ def test_true_false(self):
117117
)
118118
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)
119119

120+
def test_single_selection_no_answers(self):
121+
item = _make_item(
122+
type=exercises.SINGLE_SELECTION,
123+
question="What is 2+2?",
124+
answers=[],
125+
randomize=True,
126+
assessment_id="abcdef1234567890abcdef1234567890",
127+
)
128+
129+
result = convert_legacy_assessment_item_to_qti(item)
130+
131+
self.assertEqual(result.identifier, "Kq83vEjRWeJCrze8SNFZ4kA")
132+
self.assertEqual(
133+
_normalize_xml(_load_fixture("single_selection_no_answers.xml")),
134+
_normalize_xml(result.xml),
135+
)
136+
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)
137+
138+
def test_choice_types_with_no_answers_get_one_empty_choice(self):
139+
# test_single_selection_no_answers pins SINGLE_SELECTION against the fixture.
140+
for question_type in (exercises.MULTIPLE_SELECTION, "true_false"):
141+
with self.subTest(question_type=question_type):
142+
item = _make_item(
143+
type=question_type,
144+
question="What is 2+2?",
145+
answers=[],
146+
assessment_id="abcdef1234567890abcdef1234567890",
147+
)
148+
149+
result = convert_legacy_assessment_item_to_qti(item)
150+
151+
self.assertIn(
152+
'<qti-simple-choice identifier="choice_0" show-hide="show" fixed="false" />',
153+
result.xml,
154+
)
155+
self.assertIn("<p>What is 2+2?</p>", result.xml)
156+
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)
157+
158+
def test_choice_type_with_no_answers_and_no_question(self):
159+
item = _make_item(
160+
type=exercises.MULTIPLE_SELECTION,
161+
question="",
162+
answers=[],
163+
assessment_id="abcdef1234567890abcdef1234567890",
164+
)
165+
166+
result = convert_legacy_assessment_item_to_qti(item)
167+
168+
self.assertIn("<qti-prompt />", result.xml)
169+
self.assertTrue(validate_qti_item(result.xml.encode("utf-8")).is_valid)
170+
120171
def test_media_reference_survives(self):
121172
item = _make_item(
122173
type=exercises.SINGLE_SELECTION,

‎contentcuration/contentcuration/utils/assessment/qti/convert.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,9 +164,14 @@ def _create_choice_interaction_and_response(
164164

165165
prompt = Prompt(children=_create_html_content_from_text(item.question))
166166

167+
# Every newly added question starts answerless, but the XSD requires at least one
168+
# qti-simple-choice. Stand in the single empty choice the QTI editor opens a new
169+
# choice interaction with.
170+
answers = item.answers or [{}]
171+
167172
choices = []
168173
correct_values = []
169-
for i, answer in enumerate(item.answers):
174+
for i, answer in enumerate(answers):
170175
choice_id = f"choice_{i}"
171176
choice_content = _create_html_content_from_text(answer.get("answer", ""))
172177

0 commit comments

Comments
 (0)