From c51c5e0350dfa822bb5746cddbbf2e272d08cb35 Mon Sep 17 00:00:00 2001 From: rtibblesbot Date: Tue, 18 Aug 2026 17:03:35 -0700 Subject: [PATCH] fix: serialise QTI item language as xml:lang MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit QTI 3.0 declares xml:lang on qti-assessment-item; `language` is not an attribute of the element. Renaming the pydantic field to `xml__lang` is enough — XMLElement.to_element already maps `__` to `:`. Exercise zip checksums shift because the generated XML changed. --- .../qti/fixtures/free_response_no_answers.xml | 2 +- .../qti/fixtures/free_response_with_maths.xml | 2 +- .../utils/qti/fixtures/input_question.xml | 2 +- .../math_content_choice_interaction.xml | 2 +- .../utils/qti/fixtures/multiple_selection.xml | 2 +- .../utils/qti/fixtures/single_selection.xml | 2 +- .../tests/utils/qti/fixtures/true_false.xml | 2 +- .../tests/utils/qti/test_assessment_items.py | 22 +++++++++---------- .../tests/utils/qti/test_qti.py | 4 ++-- .../tests/utils/qti/test_validation.py | 2 +- .../tests/utils/test_exercise_creation.py | 6 ++--- .../utils/assessment/qti/assessment_item.py | 2 +- .../utils/assessment/qti/convert.py | 4 ++-- 13 files changed, 27 insertions(+), 27 deletions(-) diff --git a/contentcuration/contentcuration/tests/utils/qti/fixtures/free_response_no_answers.xml b/contentcuration/contentcuration/tests/utils/qti/fixtures/free_response_no_answers.xml index 0fb625636a..70b1c0ca89 100644 --- a/contentcuration/contentcuration/tests/utils/qti/fixtures/free_response_no_answers.xml +++ b/contentcuration/contentcuration/tests/utils/qti/fixtures/free_response_no_answers.xml @@ -1,5 +1,5 @@ - + diff --git a/contentcuration/contentcuration/tests/utils/qti/fixtures/free_response_with_maths.xml b/contentcuration/contentcuration/tests/utils/qti/fixtures/free_response_with_maths.xml index 898dc86ce5..900e7e1a16 100644 --- a/contentcuration/contentcuration/tests/utils/qti/fixtures/free_response_with_maths.xml +++ b/contentcuration/contentcuration/tests/utils/qti/fixtures/free_response_with_maths.xml @@ -1,5 +1,5 @@ - + Nothing diff --git a/contentcuration/contentcuration/tests/utils/qti/fixtures/input_question.xml b/contentcuration/contentcuration/tests/utils/qti/fixtures/input_question.xml index 1839fad96d..1c37bd0613 100644 --- a/contentcuration/contentcuration/tests/utils/qti/fixtures/input_question.xml +++ b/contentcuration/contentcuration/tests/utils/qti/fixtures/input_question.xml @@ -1,5 +1,5 @@ - + 1 diff --git a/contentcuration/contentcuration/tests/utils/qti/fixtures/math_content_choice_interaction.xml b/contentcuration/contentcuration/tests/utils/qti/fixtures/math_content_choice_interaction.xml index d439354dac..d38a9b7b2c 100644 --- a/contentcuration/contentcuration/tests/utils/qti/fixtures/math_content_choice_interaction.xml +++ b/contentcuration/contentcuration/tests/utils/qti/fixtures/math_content_choice_interaction.xml @@ -1,5 +1,5 @@ - + choice_0 diff --git a/contentcuration/contentcuration/tests/utils/qti/fixtures/multiple_selection.xml b/contentcuration/contentcuration/tests/utils/qti/fixtures/multiple_selection.xml index 44a4367aa6..452d7b5104 100644 --- a/contentcuration/contentcuration/tests/utils/qti/fixtures/multiple_selection.xml +++ b/contentcuration/contentcuration/tests/utils/qti/fixtures/multiple_selection.xml @@ -1,5 +1,5 @@ - + choice_0 diff --git a/contentcuration/contentcuration/tests/utils/qti/fixtures/single_selection.xml b/contentcuration/contentcuration/tests/utils/qti/fixtures/single_selection.xml index 00147521fb..fe4795f269 100644 --- a/contentcuration/contentcuration/tests/utils/qti/fixtures/single_selection.xml +++ b/contentcuration/contentcuration/tests/utils/qti/fixtures/single_selection.xml @@ -1,5 +1,5 @@ - + choice_0 diff --git a/contentcuration/contentcuration/tests/utils/qti/fixtures/true_false.xml b/contentcuration/contentcuration/tests/utils/qti/fixtures/true_false.xml index 0cdfcad547..5ea2bcd422 100644 --- a/contentcuration/contentcuration/tests/utils/qti/fixtures/true_false.xml +++ b/contentcuration/contentcuration/tests/utils/qti/fixtures/true_false.xml @@ -1,5 +1,5 @@ - + choice_0 diff --git a/contentcuration/contentcuration/tests/utils/qti/test_assessment_items.py b/contentcuration/contentcuration/tests/utils/qti/test_assessment_items.py index cdcb3f5cd2..4bc4fc1bb9 100644 --- a/contentcuration/contentcuration/tests/utils/qti/test_assessment_items.py +++ b/contentcuration/contentcuration/tests/utils/qti/test_assessment_items.py @@ -56,7 +56,7 @@ def test_assessment_item_with_catalog_info_orders_between_item_body_and_response assessment_item = AssessmentItem( identifier="item1", title="Test", - language="en-US", + xml__lang="en-US", item_body=item_body, catalog_info=catalog_info, response_processing=response_processing, @@ -78,7 +78,7 @@ def test_true_false_question(self): title="BG true false example " adaptive="false" time-dependent="false" - language="en-US" + xml:lang="en-US" tool-name="kolibri" tool-version="0.1"> @@ -139,7 +139,7 @@ def test_true_false_question(self): assessment_item = AssessmentItem( identifier="beginnersguide007", title="BG true false example ", - language="EN-US", + xml__lang="EN-US", time_dependent=False, item_body=item_body, response_declaration=[response_declaration], @@ -162,7 +162,7 @@ def test_multiple_choice_question(self): title="BG Choice example" adaptive="false" time-dependent="false" - language="en-US" + xml:lang="en-US" tool-name="kolibri" tool-version="0.1"> @@ -257,7 +257,7 @@ def test_multiple_choice_question(self): assessment_item = AssessmentItem( identifier="beginnersguide008", title="BG Choice example", - language="EN-US", + xml__lang="EN-US", time_dependent=False, item_body=item_body, response_declaration=[response_declaration], @@ -277,7 +277,7 @@ def test_long_text_question(self): title="BG Postcard example" adaptive="false" time-dependent="false" - language="en-US" + xml:lang="en-US" tool-name="kolibri" tool-version="0.1"> @@ -350,7 +350,7 @@ def test_long_text_question(self): assessment_item = AssessmentItem( identifier="beginnersguide009", title="BG Postcard example", - language="en-US", + xml__lang="en-US", time_dependent=False, item_body=item_body, response_declaration=[response_declaration], @@ -369,7 +369,7 @@ def test_missing_word_question(self): title="BG Missing Word example" adaptive="false" time-dependent="false" - language="en-US" + xml:lang="en-US" tool-name="kolibri" tool-version="0.1"> @@ -460,7 +460,7 @@ def test_missing_word_question(self): assessment_item = AssessmentItem( identifier="beginnersguide010", title="BG Missing Word example", - language="en-US", + xml__lang="en-US", time_dependent=False, item_body=item_body, response_declaration=[response_declaration], @@ -480,7 +480,7 @@ def test_numerical_entry_question(self): title="Numerical Entry Question" adaptive="false" time-dependent="false" - language="en-US" + xml:lang="en-US" tool-name="kolibri" tool-version="0.1"> @@ -525,7 +525,7 @@ def test_numerical_entry_question(self): assessment_item = AssessmentItem( identifier="numerical-entry-item", title="Numerical Entry Question", - language="en-US", + xml__lang="en-US", time_dependent=False, item_body=ItemBody( children=[ diff --git a/contentcuration/contentcuration/tests/utils/qti/test_qti.py b/contentcuration/contentcuration/tests/utils/qti/test_qti.py index 61f7fc700e..a21fce87cf 100644 --- a/contentcuration/contentcuration/tests/utils/qti/test_qti.py +++ b/contentcuration/contentcuration/tests/utils/qti/test_qti.py @@ -220,10 +220,10 @@ def test_assessment_item_element(self): assessment_item_element = AssessmentItem( identifier="item_1", title="Test Assessment Item", - language="en-US", + xml__lang="en-US", item_body=item_body, ) - expected_xml = '

Test Item Body Content

' # noqa: E501 + expected_xml = '

Test Item Body Content

' # noqa: E501 self.assertEqual(assessment_item_element.to_xml_string(), expected_xml) def test_prompt_element(self): diff --git a/contentcuration/contentcuration/tests/utils/qti/test_validation.py b/contentcuration/contentcuration/tests/utils/qti/test_validation.py index c18ccb277d..4589b2b098 100644 --- a/contentcuration/contentcuration/tests/utils/qti/test_validation.py +++ b/contentcuration/contentcuration/tests/utils/qti/test_validation.py @@ -24,7 +24,7 @@ def _item_xml(identifier, title, response_declaration, item_body): 'xsi:schemaLocation="http://www.imsglobal.org/xsd/imsqtiasi_v3p0 ' 'https://purl.imsglobal.org/spec/qti/v3p0/schema/xsd/imsqti_asiv3p0p1_v1p0.xsd" ' 'identifier="%s" title="%s" adaptive="false" time-dependent="false" ' - 'language="en-US" tool-name="kolibri" tool-version="0.1">' + 'xml:lang="en-US" tool-name="kolibri" tool-version="0.1">' "%s" '' "%s" diff --git a/contentcuration/contentcuration/tests/utils/test_exercise_creation.py b/contentcuration/contentcuration/tests/utils/test_exercise_creation.py index 17541d8ba3..c74361a9b3 100644 --- a/contentcuration/contentcuration/tests/utils/test_exercise_creation.py +++ b/contentcuration/contentcuration/tests/utils/test_exercise_creation.py @@ -1637,7 +1637,7 @@ def test_exercise_with_image(self): _normalize_xml(actual_manifest_xml), ) - self.assertEqual(exercise_file.checksum, "cd5a770d35fa1c25092331ee00f4ce4a") + self.assertEqual(exercise_file.checksum, "08f316508478ea4710b96473d0e5e1f3") def test_image_resizing(self): # Create a base image file @@ -1707,7 +1707,7 @@ def test_image_resizing(self): # Expected QTI item XML content with MathML conversion expected_item_xml = f""" - + choice_0 @@ -1843,7 +1843,7 @@ def test_multiple_question_types_mixed(self): _normalize_xml(actual_manifest_xml), ) - self.assertEqual(exercise_file.checksum, "f15370f74b06b59bca6e289fe0e9cb87") + self.assertEqual(exercise_file.checksum, "f4689243ba9f9b5e1abdc874d2d7527d") def test_unsupported_question_type(self): """Test that unsupported question types raise appropriate errors""" diff --git a/contentcuration/contentcuration/utils/assessment/qti/assessment_item.py b/contentcuration/contentcuration/utils/assessment/qti/assessment_item.py index 87d29cc268..20796ab6d6 100644 --- a/contentcuration/contentcuration/utils/assessment/qti/assessment_item.py +++ b/contentcuration/contentcuration/utils/assessment/qti/assessment_item.py @@ -282,7 +282,7 @@ class AssessmentItem(QTIBase): label: Optional[str] = None adaptive: bool = False time_dependent: Optional[bool] = None - language: BCP47Language + xml__lang: BCP47Language tool_name: str = "kolibri" tool_version: str = "0.1" diff --git a/contentcuration/contentcuration/utils/assessment/qti/convert.py b/contentcuration/contentcuration/utils/assessment/qti/convert.py index 2a64cd251f..d363f3735f 100644 --- a/contentcuration/contentcuration/utils/assessment/qti/convert.py +++ b/contentcuration/contentcuration/utils/assessment/qti/convert.py @@ -244,7 +244,7 @@ def build_perseus_custom_interaction_item( item = AssessmentItem( identifier=identifier, title=title, - language=language, + xml__lang=language, adaptive=False, time_dependent=False, response_declaration=[ @@ -327,7 +327,7 @@ def convert_legacy_assessment_item_to_qti( qti_item = AssessmentItem( identifier=qti_item_id, title=item.title, - language=item.language, + xml__lang=item.language, adaptive=False, time_dependent=False, response_declaration=[response_declaration],