diff --git a/js/LearningObjectives.js b/js/LearningObjectives.js new file mode 100644 index 0000000..ade682e --- /dev/null +++ b/js/LearningObjectives.js @@ -0,0 +1,79 @@ +// -*- 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": { + "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": { + "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", + "minLength": 1, + "pattern": "^\\s*\\S[\\s\\S]*$", + "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, + "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" + } + } + } + }, + "lesson_objectives": { + "type": "object", + "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" + } + } + } + } + }, + "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..542bdcb --- /dev/null +++ b/le_utils/constants/learning_objectives.py @@ -0,0 +1,78 @@ +# -*- 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": { + "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": { + "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", + "minLength": 1, + "pattern": "^\\s*\\S[\\s\\S]*$", + "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, + "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"}, + } + }, + }, + "lesson_objectives": { + "type": "object", + "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"}, + } + }, + }, + }, + "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..0c43fd5 --- /dev/null +++ b/spec/schema-learning_objectives.json @@ -0,0 +1,73 @@ +{ + "$id": "/schemas/learning_objectives", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "description": "Schema for Learning Objectives mapping", + "additionalProperties": false, + "definitions": { + "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": { + "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", + "minLength": 1, + "pattern": "^\\s*\\S[\\s\\S]*$", + "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, + "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" + } + } + } + }, + "lesson_objectives": { + "type": "object", + "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" + } + } + } + } + }, + "required": ["learning_objectives", "assessment_objectives", "lesson_objectives"] +} diff --git a/tests/test_schemas.py b/tests/test_schemas.py index de5e37c..6c74e4e 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,289 @@ 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": 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": { + 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": { + "abcdef1234567890abcdef1234567890": [ + str(uuid.uuid5(uuid.NAMESPACE_DNS, "test6")) + ], + "abcdef1234567890abcdef1234567891": [ + str(uuid.uuid5(uuid.NAMESPACE_DNS, "test7")) + ], + }, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_lo_structure(): + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [ + {"id": str(uuid.uuid4())}, # Missing text + ], + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_assessment_mapping(): + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": { + str(uuid.uuid4()): str(uuid.uuid4()), # Should be an array + }, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, + } + ) + + +@skip_if_jsonschema_unavailable +def test_learning_objectives__invalid_lesson_mapping(): + 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": str( + uuid.uuid4() + ), # 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": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, + } + ) + + # Missing lesson_objectives + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + } + ) + + # Missing lesson_objectives + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + } + ) + + +@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": {str(uuid.uuid4()): [str(uuid.uuid4())]}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, + } + ) + + # Empty assessment_objectives + with pytest.raises(jsonschema.ValidationError): + _validate_learning_objectives( + { + "learning_objectives": [{"id": str(uuid.uuid4()), "text": "LO1"}], + "assessment_objectives": {}, + "lesson_objectives": { + "abcdef1234567890abcdef1234567890": [str(uuid.uuid4())] + }, + } + ) + + # Empty lesson_objectives + 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": {}, + } + ) + + +@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())] + }, + } + )