From cffae05dbb5ec42af3c86ac7e0ba516c822495d0 Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Thu, 4 Dec 2025 15:33:35 -0500 Subject: [PATCH 1/2] Add earning objectives schema --- js/LearningObjectives.js | 66 ++++++++++ le_utils/constants/learning_objectives.py | 69 +++++++++++ spec/schema-learning_objectives.json | 60 ++++++++++ tests/test_schemas.py | 139 ++++++++++++++++++++++ 4 files changed, 334 insertions(+) create mode 100644 js/LearningObjectives.js create mode 100644 le_utils/constants/learning_objectives.py create mode 100644 spec/schema-learning_objectives.json diff --git a/js/LearningObjectives.js b/js/LearningObjectives.js new file mode 100644 index 0000000..ed0b19d --- /dev/null +++ b/js/LearningObjectives.js @@ -0,0 +1,66 @@ +// -*- coding: utf-8 -*- +// Generated by scripts/generate_from_specs.py +// LearningObjectives + + +export const SCHEMA = { + "$id": "/schemas/learning_objectives", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "description": "Schema for Learning Objectives mapping", + "additionalProperties": false, + "definitions": { + "learning_objective_id": { + "type": "string", + "description": "Unique identifier for the Learning Objective" + } + }, + "properties": { + "learning_objectives": { + "type": "array", + "description": "A list of Learning Objectives available in the unit", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/learning_objective_id" + }, + "text": { + "type": "string", + "description": "Human-readable text describing the Learning Objective" + }, + "metadata": { + "type": "object", + "description": "Optional metadata associated with the Learning Objective" + } + }, + "required": ["id", "text"] + } + }, + "assessment_objectives": { + "type": "object", + "description": "Mapping of assessment question IDs to Learning Objective IDs", + "minProperties": 1, + "patternProperties": { + "^.*$": { + "type": "array", + "items": { "$ref": "#/definitions/learning_objective_id" } + } + } + }, + "lesson_objectives": { + "type": "object", + "description": "Mapping of lesson content node IDs to Learning Objective IDs", + "minProperties": 1, + "patternProperties": { + "^.*$": { + "type": "array", + "items": { "$ref": "#/definitions/learning_objective_id" } + } + } + } + }, + "required": ["learning_objectives", "assessment_objectives", "lesson_objectives"] +} +; diff --git a/le_utils/constants/learning_objectives.py b/le_utils/constants/learning_objectives.py new file mode 100644 index 0000000..7ba9011 --- /dev/null +++ b/le_utils/constants/learning_objectives.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +# Generated by scripts/generate_from_specs.py +from __future__ import unicode_literals + +# LearningObjectives + + +choices = () + +LEARNINGOBJECTIVESLIST = [] + +SCHEMA = { + "$id": "/schemas/learning_objectives", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "description": "Schema for Learning Objectives mapping", + "additionalProperties": False, + "definitions": { + "learning_objective_id": { + "type": "string", + "description": "Unique identifier for the Learning Objective", + } + }, + "properties": { + "learning_objectives": { + "type": "array", + "description": "A list of Learning Objectives available in the unit", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "id": {"$ref": "#/definitions/learning_objective_id"}, + "text": { + "type": "string", + "description": "Human-readable text describing the Learning Objective", + }, + "metadata": { + "type": "object", + "description": "Optional metadata associated with the Learning Objective", + }, + }, + "required": ["id", "text"], + }, + }, + "assessment_objectives": { + "type": "object", + "description": "Mapping of assessment question IDs to Learning Objective IDs", + "minProperties": 1, + "patternProperties": { + "^.*$": { + "type": "array", + "items": {"$ref": "#/definitions/learning_objective_id"}, + } + }, + }, + "lesson_objectives": { + "type": "object", + "description": "Mapping of lesson content node IDs to Learning Objective IDs", + "minProperties": 1, + "patternProperties": { + "^.*$": { + "type": "array", + "items": {"$ref": "#/definitions/learning_objective_id"}, + } + }, + }, + }, + "required": ["learning_objectives", "assessment_objectives", "lesson_objectives"], +} diff --git a/spec/schema-learning_objectives.json b/spec/schema-learning_objectives.json new file mode 100644 index 0000000..2c147bc --- /dev/null +++ b/spec/schema-learning_objectives.json @@ -0,0 +1,60 @@ +{ + "$id": "/schemas/learning_objectives", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "description": "Schema for Learning Objectives mapping", + "additionalProperties": false, + "definitions": { + "learning_objective_id": { + "type": "string", + "description": "Unique identifier for the Learning Objective" + } + }, + "properties": { + "learning_objectives": { + "type": "array", + "description": "A list of Learning Objectives available in the unit", + "minItems": 1, + "items": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/learning_objective_id" + }, + "text": { + "type": "string", + "description": "Human-readable text describing the Learning Objective" + }, + "metadata": { + "type": "object", + "description": "Optional metadata associated with the Learning Objective" + } + }, + "required": ["id", "text"] + } + }, + "assessment_objectives": { + "type": "object", + "description": "Mapping of assessment question IDs to Learning Objective IDs", + "minProperties": 1, + "patternProperties": { + "^.*$": { + "type": "array", + "items": { "$ref": "#/definitions/learning_objective_id" } + } + } + }, + "lesson_objectives": { + "type": "object", + "description": "Mapping of lesson content node IDs to Learning Objective IDs", + "minProperties": 1, + "patternProperties": { + "^.*$": { + "type": "array", + "items": { "$ref": "#/definitions/learning_objective_id" } + } + } + } + }, + "required": ["learning_objectives", "assessment_objectives", "lesson_objectives"] +} diff --git a/tests/test_schemas.py b/tests/test_schemas.py index de5e37c..68a8c01 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -9,6 +9,7 @@ from le_utils.constants import completion_criteria from le_utils.constants import embed_content_request from le_utils.constants import embed_topics_request +from le_utils.constants import learning_objectives from le_utils.constants import mastery_criteria try: @@ -624,3 +625,141 @@ def test_embed__content__invalid_preset_files(): }, } ) + + +def _validate_learning_objectives(data): + """ + :param data: Dictionary of data to validate + :raises: jsonschema.ValidationError: When invalid + """ + jsonschema.validate(instance=data, schema=learning_objectives.SCHEMA) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__valid(): + with _assert_not_raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [ + {"id": "lo1", "text": "Learning Objective 1"}, + { + "id": "lo2", + "text": "Learning Objective 2", + "metadata": {"key": "value"}, + }, + ], + "assessment_objectives": { + "q1": ["lo1"], + "q2": ["lo1", "lo2"], + }, + "lesson_objectives": { + "lesson1": ["lo1"], + "lesson2": ["lo2"], + }, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_lo_structure(): + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [ + {"id": "lo1"}, # Missing text + ], + "assessment_objectives": {"q1": ["lo1"]}, + "lesson_objectives": {"lesson1": ["lo1"]}, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_assessment_mapping(): + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": "lo1", "text": "LO1"}], + "assessment_objectives": { + "q1": "lo1", # Should be an array + }, + "lesson_objectives": {"lesson1": ["lo1"]}, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_lesson_mapping(): + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": "lo1", "text": "LO1"}], + "assessment_objectives": {"q1": ["lo1"]}, + "lesson_objectives": { + "lesson1": "lo1", # Should be an array + }, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__missing_required_fields(): + # Missing learning_objectives + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "assessment_objectives": {"q1": ["lo1"]}, + "lesson_objectives": {"lesson1": ["lo1"]}, + } + ) + + # Missing assessment_objectives + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": "lo1", "text": "LO1"}], + "lesson_objectives": {"lesson1": ["lo1"]}, + } + ) + + # Missing lesson_objectives + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": "lo1", "text": "LO1"}], + "assessment_objectives": {"q1": ["lo1"]}, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__empty_structures(): + # Empty learning_objectives + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [], + "assessment_objectives": {"q1": ["lo1"]}, + "lesson_objectives": {"lesson1": ["lo1"]}, + } + ) + + # Empty assessment_objectives + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": "lo1", "text": "LO1"}], + "assessment_objectives": {}, + "lesson_objectives": {"lesson1": ["lo1"]}, + } + ) + + # Empty lesson_objectives + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": "lo1", "text": "LO1"}], + "assessment_objectives": {"q1": ["lo1"]}, + "lesson_objectives": {}, + } + ) From 55e4a809597f139f391872e553b707ad07a188c5 Mon Sep 17 00:00:00 2001 From: Alex Velez Date: Tue, 9 Dec 2025 17:41:37 -0500 Subject: [PATCH 2/2] Adding more restrictions to learning objectives json schema --- js/LearningObjectives.js | 25 ++- le_utils/constants/learning_objectives.py | 19 +- spec/schema-learning_objectives.json | 25 ++- tests/test_schemas.py | 204 +++++++++++++++++++--- 4 files changed, 228 insertions(+), 45 deletions(-) diff --git a/js/LearningObjectives.js b/js/LearningObjectives.js index ed0b19d..ade682e 100644 --- a/js/LearningObjectives.js +++ b/js/LearningObjectives.js @@ -10,8 +10,13 @@ export const SCHEMA = { "description": "Schema for Learning Objectives mapping", "additionalProperties": false, "definitions": { - "learning_objective_id": { + "uuid": { "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": "A unique identifier in the form of a UUID v4 or v5" + }, + "learning_objective_id": { + "$ref": "#/definitions/uuid", "description": "Unique identifier for the Learning Objective" } }, @@ -28,6 +33,8 @@ export const SCHEMA = { }, "text": { "type": "string", + "minLength": 1, + "pattern": "^\\s*\\S[\\s\\S]*$", "description": "Human-readable text describing the Learning Objective" }, "metadata": { @@ -42,21 +49,27 @@ export const SCHEMA = { "type": "object", "description": "Mapping of assessment question IDs to Learning Objective IDs", "minProperties": 1, + "additionalProperties": false, "patternProperties": { - "^.*$": { + "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$": { "type": "array", - "items": { "$ref": "#/definitions/learning_objective_id" } + "items": { + "$ref": "#/definitions/learning_objective_id" + } } } }, "lesson_objectives": { "type": "object", - "description": "Mapping of lesson content node IDs to Learning Objective IDs", + "description": "Mapping of lesson IDs to Learning Objective IDs", "minProperties": 1, + "additionalProperties": false, "patternProperties": { - "^.*$": { + "^[0-9a-f]{32}$": { "type": "array", - "items": { "$ref": "#/definitions/learning_objective_id" } + "items": { + "$ref": "#/definitions/learning_objective_id" + } } } } diff --git a/le_utils/constants/learning_objectives.py b/le_utils/constants/learning_objectives.py index 7ba9011..542bdcb 100644 --- a/le_utils/constants/learning_objectives.py +++ b/le_utils/constants/learning_objectives.py @@ -16,10 +16,15 @@ "description": "Schema for Learning Objectives mapping", "additionalProperties": False, "definitions": { - "learning_objective_id": { + "uuid": { "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": "A unique identifier in the form of a UUID v4 or v5", + }, + "learning_objective_id": { + "$ref": "#/definitions/uuid", "description": "Unique identifier for the Learning Objective", - } + }, }, "properties": { "learning_objectives": { @@ -32,6 +37,8 @@ "id": {"$ref": "#/definitions/learning_objective_id"}, "text": { "type": "string", + "minLength": 1, + "pattern": "^\\s*\\S[\\s\\S]*$", "description": "Human-readable text describing the Learning Objective", }, "metadata": { @@ -46,8 +53,9 @@ "type": "object", "description": "Mapping of assessment question IDs to Learning Objective IDs", "minProperties": 1, + "additionalProperties": False, "patternProperties": { - "^.*$": { + "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$": { "type": "array", "items": {"$ref": "#/definitions/learning_objective_id"}, } @@ -55,10 +63,11 @@ }, "lesson_objectives": { "type": "object", - "description": "Mapping of lesson content node IDs to Learning Objective IDs", + "description": "Mapping of lesson IDs to Learning Objective IDs", "minProperties": 1, + "additionalProperties": False, "patternProperties": { - "^.*$": { + "^[0-9a-f]{32}$": { "type": "array", "items": {"$ref": "#/definitions/learning_objective_id"}, } diff --git a/spec/schema-learning_objectives.json b/spec/schema-learning_objectives.json index 2c147bc..0c43fd5 100644 --- a/spec/schema-learning_objectives.json +++ b/spec/schema-learning_objectives.json @@ -5,8 +5,13 @@ "description": "Schema for Learning Objectives mapping", "additionalProperties": false, "definitions": { - "learning_objective_id": { + "uuid": { "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": "A unique identifier in the form of a UUID v4 or v5" + }, + "learning_objective_id": { + "$ref": "#/definitions/uuid", "description": "Unique identifier for the Learning Objective" } }, @@ -23,6 +28,8 @@ }, "text": { "type": "string", + "minLength": 1, + "pattern": "^\\s*\\S[\\s\\S]*$", "description": "Human-readable text describing the Learning Objective" }, "metadata": { @@ -37,21 +44,27 @@ "type": "object", "description": "Mapping of assessment question IDs to Learning Objective IDs", "minProperties": 1, + "additionalProperties": false, "patternProperties": { - "^.*$": { + "^[0-9a-f]{8}-[0-9a-f]{4}-[45][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$": { "type": "array", - "items": { "$ref": "#/definitions/learning_objective_id" } + "items": { + "$ref": "#/definitions/learning_objective_id" + } } } }, "lesson_objectives": { "type": "object", - "description": "Mapping of lesson content node IDs to Learning Objective IDs", + "description": "Mapping of lesson IDs to Learning Objective IDs", "minProperties": 1, + "additionalProperties": false, "patternProperties": { - "^.*$": { + "^[0-9a-f]{32}$": { "type": "array", - "items": { "$ref": "#/definitions/learning_objective_id" } + "items": { + "$ref": "#/definitions/learning_objective_id" + } } } } diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 68a8c01..6c74e4e 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -641,20 +641,63 @@ def test_learning_objectives__valid(): _validate_learning_objectives( { "learning_objectives": [ - {"id": "lo1", "text": "Learning Objective 1"}, { - "id": "lo2", + "id": str(uuid.uuid4()), + "text": "Learning Objective 1", + }, + { + "id": str(uuid.uuid4()), + "text": "Learning Objective 2", + "metadata": {"key": "value"}, + }, + ], + "assessment_objectives": { + str(uuid.uuid4()): [str(uuid.uuid4())], + str(uuid.uuid4()): [ + str(uuid.uuid4()), + str(uuid.uuid4()), + ], + }, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())], + "abcdef1234567890abcdef1234567891": [str(uuid.uuid4())], + }, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__valid_uuid_v5(): + with _assert_not_raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [ + { + "id": str(uuid.uuid5(uuid.NAMESPACE_DNS, "test")), + "text": "Learning Objective 1", + }, + { + "id": str(uuid.uuid5(uuid.NAMESPACE_DNS, "test2")), "text": "Learning Objective 2", "metadata": {"key": "value"}, }, ], "assessment_objectives": { - "q1": ["lo1"], - "q2": ["lo1", "lo2"], + str(uuid.uuid5(uuid.NAMESPACE_DNS, "test")): [ + str(uuid.uuid5(uuid.NAMESPACE_DNS, "test3")) + ], + str(uuid.uuid5(uuid.NAMESPACE_DNS, "test2")): [ + str(uuid.uuid5(uuid.NAMESPACE_DNS, "test4")), + str(uuid.uuid5(uuid.NAMESPACE_DNS, "test5")), + ], }, "lesson_objectives": { - "lesson1": ["lo1"], - "lesson2": ["lo2"], + "abcdef1234567890abcdef1234567890": [ + str(uuid.uuid5(uuid.NAMESPACE_DNS, "test6")) + ], + "abcdef1234567890abcdef1234567891": [ + str(uuid.uuid5(uuid.NAMESPACE_DNS, "test7")) + ], }, } ) @@ -666,10 +709,12 @@ def test_learning_objectives__invalid_lo_structure(): _validate_learning_objectives( { "learning_objectives": [ - {"id": "lo1"}, # Missing text + {"id": str(uuid.uuid4())}, # Missing text ], - "assessment_objectives": {"q1": ["lo1"]}, - "lesson_objectives": {"lesson1": ["lo1"]}, + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, } ) @@ -679,11 +724,13 @@ def test_learning_objectives__invalid_assessment_mapping(): with pytest.raises(jsonschema.ValidationError): _validate_learning_objectives( { - "learning_objectives": [{"id": "lo1", "text": "LO1"}], + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], "assessment_objectives": { - "q1": "lo1", # Should be an array + str(uuid.uuid4()): str(uuid.uuid4()), # Should be an array + }, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] }, - "lesson_objectives": {"lesson1": ["lo1"]}, } ) @@ -693,10 +740,12 @@ def test_learning_objectives__invalid_lesson_mapping(): with pytest.raises(jsonschema.ValidationError): _validate_learning_objectives( { - "learning_objectives": [{"id": "lo1", "text": "LO1"}], - "assessment_objectives": {"q1": ["lo1"]}, + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, "lesson_objectives": { - "lesson1": "lo1", # Should be an array + "abcdef1234567890abcdef1234567890": str( + uuid.uuid4() + ), # Should be an array }, } ) @@ -708,17 +757,19 @@ def test_learning_objectives__missing_required_fields(): with pytest.raises(jsonschema.ValidationError): _validate_learning_objectives( { - "assessment_objectives": {"q1": ["lo1"]}, - "lesson_objectives": {"lesson1": ["lo1"]}, + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, } ) - # Missing assessment_objectives + # Missing lesson_objectives with pytest.raises(jsonschema.ValidationError): _validate_learning_objectives( { - "learning_objectives": [{"id": "lo1", "text": "LO1"}], - "lesson_objectives": {"lesson1": ["lo1"]}, + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, } ) @@ -726,8 +777,8 @@ def test_learning_objectives__missing_required_fields(): with pytest.raises(jsonschema.ValidationError): _validate_learning_objectives( { - "learning_objectives": [{"id": "lo1", "text": "LO1"}], - "assessment_objectives": {"q1": ["lo1"]}, + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, } ) @@ -739,8 +790,10 @@ def test_learning_objectives__empty_structures(): _validate_learning_objectives( { "learning_objectives": [], - "assessment_objectives": {"q1": ["lo1"]}, - "lesson_objectives": {"lesson1": ["lo1"]}, + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, } ) @@ -748,9 +801,11 @@ def test_learning_objectives__empty_structures(): with pytest.raises(jsonschema.ValidationError): _validate_learning_objectives( { - "learning_objectives": [{"id": "lo1", "text": "LO1"}], + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], "assessment_objectives": {}, - "lesson_objectives": {"lesson1": ["lo1"]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, } ) @@ -758,8 +813,101 @@ def test_learning_objectives__empty_structures(): with pytest.raises(jsonschema.ValidationError): _validate_learning_objectives( { - "learning_objectives": [{"id": "lo1", "text": "LO1"}], - "assessment_objectives": {"q1": ["lo1"]}, + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, "lesson_objectives": {}, } ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_uuid_format_in_learning_objectives(): + # Invalid UUID format for learning objective ID + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": "invalid-uuid", "text": "LO1"}], + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_uuid_format_in_references(): + # Invalid UUID format in assessment and lesson objective references + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": {str(uuid.uuid4()): ["invalid-uuid"]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_question_id_pattern(): + # Invalid question ID pattern (not a valid UUID v4) + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": { + "invalid-uuid-format": [str(uuid.uuid4())] + }, # Invalid UUID format + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_lesson_id_pattern(): + # Invalid lesson ID pattern (has extra characters) + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890:extra": [str(uuid.uuid4())] + }, # Has extra characters + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_text_patterns(): + # Empty text after trimming whitespace + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [ + {"id": str(uuid.uuid4()), "text": " "} + ], # Only whitespace + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, + } + ) + + # Empty text + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [ + {"id": str(uuid.uuid4()), "text": ""} + ], # Empty string + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, + } + )