From f09161f82df1479a0be708038c052be056da333f Mon Sep 17 00:00:00 2001 From: Samson Akol Date: Wed, 3 Dec 2025 16:01:32 +0300 Subject: [PATCH 1/3] Makes pre/post test updates on schema and model --- js/MasteryCriteria.js | 48 +++++++++++++++++++++++++- le_utils/constants/exercises.py | 2 ++ le_utils/constants/mastery_criteria.py | 38 +++++++++++++++++++- spec/schema-mastery_criteria.json | 47 ++++++++++++++++++++++++- tests/test_schemas.py | 28 +++++++++++++++ 5 files changed, 160 insertions(+), 3 deletions(-) diff --git a/js/MasteryCriteria.js b/js/MasteryCriteria.js index 5553722..929358f 100644 --- a/js/MasteryCriteria.js +++ b/js/MasteryCriteria.js @@ -9,6 +9,7 @@ export default { NUM_CORRECT_IN_A_ROW_2: "num_correct_in_a_row_2", NUM_CORRECT_IN_A_ROW_3: "num_correct_in_a_row_3", NUM_CORRECT_IN_A_ROW_5: "num_correct_in_a_row_5", + PRE_POST_TEST: "pre_post_test", }; export const SCHEMA = { @@ -28,7 +29,41 @@ export const SCHEMA = { "num_correct_in_a_row_2", "num_correct_in_a_row_3", "num_correct_in_a_row_5", - "num_correct_in_a_row_10" + "num_correct_in_a_row_10", + "pre_post_test" + ] + }, + "pre_post_test": { + "type": "object", + "description": "Definition for pre/post test", + "additionalProperties": false, + "properties": { + "assessment_item_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of assessment item IDs for version A and B of the pre/post test" + }, + "version_a_item_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of assessment item IDs for version A of the pre/post test" + }, + "version_b_item_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of assessment item IDs for version B of the pre/post test" + } + }, + "required": [ + "assessment_item_ids", + "version_a_item_ids", + "version_b_item_ids" ] } }, @@ -37,6 +72,9 @@ export const SCHEMA = { "n": true, "mastery_model": { "$ref": "#/definitions/mastery_model" + }, + "pre_post_test": { + "$ref": "#/definitions/pre_post_test" } }, "anyOf": [ @@ -66,6 +104,14 @@ export const SCHEMA = { "type": "null" } } + }, + { + "properties": { + "mastery_model": { + "const": "pre_post_test" + } + }, + "required": ["pre_post_test"] } ] }; diff --git a/le_utils/constants/exercises.py b/le_utils/constants/exercises.py index 1591f3c..3be2f11 100644 --- a/le_utils/constants/exercises.py +++ b/le_utils/constants/exercises.py @@ -8,6 +8,7 @@ SKILL_CHECK = "skill_check" M_OF_N = "m_of_n" QUIZ = "quiz" +PRE_POST_TEST = "pre_post_test" MASTERY_MODELS = ( (DO_ALL, "Do all"), @@ -18,6 +19,7 @@ (SKILL_CHECK, "Skill check"), (M_OF_N, "M out of N"), (QUIZ, "Quiz"), + (PRE_POST_TEST, "Pre/Post Test"), ) IMG_PLACEHOLDER = "{☣ LOCALPATH}" diff --git a/le_utils/constants/mastery_criteria.py b/le_utils/constants/mastery_criteria.py index 353e1fc..1a858f2 100644 --- a/le_utils/constants/mastery_criteria.py +++ b/le_utils/constants/mastery_criteria.py @@ -10,6 +10,7 @@ NUM_CORRECT_IN_A_ROW_2 = "num_correct_in_a_row_2" NUM_CORRECT_IN_A_ROW_3 = "num_correct_in_a_row_3" NUM_CORRECT_IN_A_ROW_5 = "num_correct_in_a_row_5" +PRE_POST_TEST = "pre_post_test" choices = ( (DO_ALL, "Do All"), @@ -18,6 +19,7 @@ (NUM_CORRECT_IN_A_ROW_2, "Num Correct In A Row 2"), (NUM_CORRECT_IN_A_ROW_3, "Num Correct In A Row 3"), (NUM_CORRECT_IN_A_ROW_5, "Num Correct In A Row 5"), + (PRE_POST_TEST, "Pre Post Test"), ) MASTERYCRITERIALIST = [ @@ -27,6 +29,7 @@ NUM_CORRECT_IN_A_ROW_2, NUM_CORRECT_IN_A_ROW_3, NUM_CORRECT_IN_A_ROW_5, + PRE_POST_TEST, ] SCHEMA = { @@ -47,13 +50,42 @@ "num_correct_in_a_row_3", "num_correct_in_a_row_5", "num_correct_in_a_row_10", + "pre_post_test", ], - } + }, + "pre_post_test": { + "type": "object", + "description": "Definition for pre/post test", + "additionalProperties": False, + "properties": { + "assessment_item_ids": { + "type": "array", + "items": {"type": "string"}, + "description": "List of assessment item IDs for version A and B of the pre/post test", + }, + "version_a_item_ids": { + "type": "array", + "items": {"type": "string"}, + "description": "List of assessment item IDs for version A of the pre/post test", + }, + "version_b_item_ids": { + "type": "array", + "items": {"type": "string"}, + "description": "List of assessment item IDs for version B of the pre/post test", + }, + }, + "required": [ + "assessment_item_ids", + "version_a_item_ids", + "version_b_item_ids", + ], + }, }, "properties": { "m": True, "n": True, "mastery_model": {"$ref": "#/definitions/mastery_model"}, + "pre_post_test": {"$ref": "#/definitions/pre_post_test"}, }, "anyOf": [ {"properties": {"mastery_model": {"const": "m_of_n"}}, "required": ["m", "n"]}, @@ -72,5 +104,9 @@ "n": {"type": "null"}, } }, + { + "properties": {"mastery_model": {"const": "pre_post_test"}}, + "required": ["pre_post_test"], + }, ], } diff --git a/spec/schema-mastery_criteria.json b/spec/schema-mastery_criteria.json index ef6d459..9362bb8 100644 --- a/spec/schema-mastery_criteria.json +++ b/spec/schema-mastery_criteria.json @@ -15,7 +15,41 @@ "num_correct_in_a_row_2", "num_correct_in_a_row_3", "num_correct_in_a_row_5", - "num_correct_in_a_row_10" + "num_correct_in_a_row_10", + "pre_post_test" + ] + }, + "pre_post_test": { + "type": "object", + "description": "Definition for pre/post test", + "additionalProperties": false, + "properties": { + "assessment_item_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of assessment item IDs for version A and B of the pre/post test" + }, + "version_a_item_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of assessment item IDs for version A of the pre/post test" + }, + "version_b_item_ids": { + "type": "array", + "items": { + "type": "string" + }, + "description": "List of assessment item IDs for version B of the pre/post test" + } + }, + "required": [ + "assessment_item_ids", + "version_a_item_ids", + "version_b_item_ids" ] } }, @@ -24,6 +58,9 @@ "n": true, "mastery_model": { "$ref": "#/definitions/mastery_model" + }, + "pre_post_test": { + "$ref": "#/definitions/pre_post_test" } }, "anyOf": [ @@ -53,6 +90,14 @@ "type": "null" } } + }, + { + "properties": { + "mastery_model": { + "const": "pre_post_test" + } + }, + "required": ["pre_post_test"] } ] } \ No newline at end of file diff --git a/tests/test_schemas.py b/tests/test_schemas.py index de5e37c..7209a27 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -223,6 +223,19 @@ def test_completion_criteria__mastery_model__valid(): "threshold": {"mastery_model": "m_of_n", "m": 1, "n": 2}, } ) + _validate_completion_criteria( + { + "model": "mastery", + "threshold": { + "mastery_model": "pre_post_test", + "pre_post_test": { + "assessment_item_ids": ["id1", "id2", "id3"], + "version_a_item_ids": ["id1", "id3"], + "version_b_item_ids": ["id2"], + }, + }, + } + ) @skip_if_jsonschema_unavailable @@ -258,6 +271,21 @@ def test_completion_criteria__mastery_model__invalid(): "threshold": -1, } ) + # Missing version_b_item_ids + with pytest.raises(jsonschema.ValidationError): + _validate_completion_criteria( + { + "model": "mastery", + "threshold": { + "mastery_model": "pre_post_test", + "pre_post_test": { + "assessment_item_ids": [], + "version_a_item_ids": ["id1", "id3"], + }, + }, + "learner_managed": False, + } + ) @skip_if_jsonschema_unavailable From 2c37fbe9bf8b1e503bd28e8e77b2091b28c77c41 Mon Sep 17 00:00:00 2001 From: Samson Akol Date: Wed, 3 Dec 2025 21:19:04 +0300 Subject: [PATCH 2/3] Adds validation for item ids --- js/MasteryCriteria.js | 15 ++++++++------ le_utils/constants/mastery_criteria.py | 21 +++++++++++++------ spec/schema-mastery_criteria.json | 15 ++++++++------ tests/test_schemas.py | 28 +++++++++++++++++++++----- 4 files changed, 56 insertions(+), 23 deletions(-) diff --git a/js/MasteryCriteria.js b/js/MasteryCriteria.js index 929358f..8aecbc8 100644 --- a/js/MasteryCriteria.js +++ b/js/MasteryCriteria.js @@ -41,23 +41,26 @@ export const SCHEMA = { "assessment_item_ids": { "type": "array", "items": { - "type": "string" + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }, - "description": "List of assessment item IDs for version A and B of the pre/post test" + "description": "List of assessment item UUIDs for version A and B of the pre/post test" }, "version_a_item_ids": { "type": "array", "items": { - "type": "string" + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }, - "description": "List of assessment item IDs for version A of the pre/post test" + "description": "List of assessment item UUIDs for version A of the pre/post test" }, "version_b_item_ids": { "type": "array", "items": { - "type": "string" + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }, - "description": "List of assessment item IDs for version B of the pre/post test" + "description": "List of assessment item UUIDs for version B of the pre/post test" } }, "required": [ diff --git a/le_utils/constants/mastery_criteria.py b/le_utils/constants/mastery_criteria.py index 1a858f2..0ecf005 100644 --- a/le_utils/constants/mastery_criteria.py +++ b/le_utils/constants/mastery_criteria.py @@ -60,18 +60,27 @@ "properties": { "assessment_item_ids": { "type": "array", - "items": {"type": "string"}, - "description": "List of assessment item IDs for version A and B of the pre/post test", + "items": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + }, + "description": "List of assessment item UUIDs for version A and B of the pre/post test", }, "version_a_item_ids": { "type": "array", - "items": {"type": "string"}, - "description": "List of assessment item IDs for version A of the pre/post test", + "items": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + }, + "description": "List of assessment item UUIDs for version A of the pre/post test", }, "version_b_item_ids": { "type": "array", - "items": {"type": "string"}, - "description": "List of assessment item IDs for version B of the pre/post test", + "items": { + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", + }, + "description": "List of assessment item UUIDs for version B of the pre/post test", }, }, "required": [ diff --git a/spec/schema-mastery_criteria.json b/spec/schema-mastery_criteria.json index 9362bb8..225e472 100644 --- a/spec/schema-mastery_criteria.json +++ b/spec/schema-mastery_criteria.json @@ -27,23 +27,26 @@ "assessment_item_ids": { "type": "array", "items": { - "type": "string" + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }, - "description": "List of assessment item IDs for version A and B of the pre/post test" + "description": "List of assessment item UUIDs for version A and B of the pre/post test" }, "version_a_item_ids": { "type": "array", "items": { - "type": "string" + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }, - "description": "List of assessment item IDs for version A of the pre/post test" + "description": "List of assessment item UUIDs for version A of the pre/post test" }, "version_b_item_ids": { "type": "array", "items": { - "type": "string" + "type": "string", + "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" }, - "description": "List of assessment item IDs for version B of the pre/post test" + "description": "List of assessment item UUIDs for version B of the pre/post test" } }, "required": [ diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 7209a27..2e2312e 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -229,9 +229,11 @@ def test_completion_criteria__mastery_model__valid(): "threshold": { "mastery_model": "pre_post_test", "pre_post_test": { - "assessment_item_ids": ["id1", "id2", "id3"], - "version_a_item_ids": ["id1", "id3"], - "version_b_item_ids": ["id2"], + "assessment_item_ids": [str(uuid.uuid4())], # v4 UUID + "version_a_item_ids": [str(uuid.uuid4())], # v4 UUID + "version_b_item_ids": [ + str(uuid.uuid5(uuid.NAMESPACE_DNS, "test")) + ], # v5 UUID }, }, } @@ -279,8 +281,24 @@ def test_completion_criteria__mastery_model__invalid(): "threshold": { "mastery_model": "pre_post_test", "pre_post_test": { - "assessment_item_ids": [], - "version_a_item_ids": ["id1", "id3"], + "assessment_item_ids": [str(uuid.uuid4())], # v4 UUID + "version_a_item_ids": [str(uuid.uuid4())], # v4 UUID + }, + }, + "learner_managed": False, + } + ) + # Invalid UUIDs + with pytest.raises(jsonschema.ValidationError): + _validate_completion_criteria( + { + "model": "mastery", + "threshold": { + "mastery_model": "pre_post_test", + "pre_post_test": { + "assessment_item_ids": ["not-a-uuid"], + "version_a_item_ids": ["not-a-uuid"], + "version_b_item_ids": ["not-a-uuid"], }, }, "learner_managed": False, From 89aa6b680fabad1bb3376f744c3514f37bd9c0ae Mon Sep 17 00:00:00 2001 From: Samson Akol Date: Tue, 9 Dec 2025 22:13:24 +0300 Subject: [PATCH 3/3] Adds validation to assesment and version item ids --- js/MasteryCriteria.js | 3 +++ le_utils/constants/mastery_criteria.py | 3 +++ spec/schema-mastery_criteria.json | 3 +++ tests/test_schemas.py | 5 ++++- 4 files changed, 13 insertions(+), 1 deletion(-) diff --git a/js/MasteryCriteria.js b/js/MasteryCriteria.js index 8aecbc8..e51c336 100644 --- a/js/MasteryCriteria.js +++ b/js/MasteryCriteria.js @@ -40,6 +40,7 @@ export const SCHEMA = { "properties": { "assessment_item_ids": { "type": "array", + "minItems": 2, "items": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" @@ -48,6 +49,7 @@ export const SCHEMA = { }, "version_a_item_ids": { "type": "array", + "minItems": 1, "items": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" @@ -56,6 +58,7 @@ export const SCHEMA = { }, "version_b_item_ids": { "type": "array", + "minItems": 1, "items": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" diff --git a/le_utils/constants/mastery_criteria.py b/le_utils/constants/mastery_criteria.py index 0ecf005..79ba4b1 100644 --- a/le_utils/constants/mastery_criteria.py +++ b/le_utils/constants/mastery_criteria.py @@ -60,6 +60,7 @@ "properties": { "assessment_item_ids": { "type": "array", + "minItems": 2, "items": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", @@ -68,6 +69,7 @@ }, "version_a_item_ids": { "type": "array", + "minItems": 1, "items": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", @@ -76,6 +78,7 @@ }, "version_b_item_ids": { "type": "array", + "minItems": 1, "items": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", diff --git a/spec/schema-mastery_criteria.json b/spec/schema-mastery_criteria.json index 225e472..b67529e 100644 --- a/spec/schema-mastery_criteria.json +++ b/spec/schema-mastery_criteria.json @@ -26,6 +26,7 @@ "properties": { "assessment_item_ids": { "type": "array", + "minItems": 2, "items": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" @@ -34,6 +35,7 @@ }, "version_a_item_ids": { "type": "array", + "minItems": 1, "items": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" @@ -42,6 +44,7 @@ }, "version_b_item_ids": { "type": "array", + "minItems": 1, "items": { "type": "string", "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$" diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 2e2312e..79cbfaf 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -229,7 +229,10 @@ def test_completion_criteria__mastery_model__valid(): "threshold": { "mastery_model": "pre_post_test", "pre_post_test": { - "assessment_item_ids": [str(uuid.uuid4())], # v4 UUID + "assessment_item_ids": [ + str(uuid.uuid4()), + str(uuid.uuid4()), + ], # v4 UUID "version_a_item_ids": [str(uuid.uuid4())], # v4 UUID "version_b_item_ids": [ str(uuid.uuid5(uuid.NAMESPACE_DNS, "test"))