From a1e61d0dbf6dd2d89bf77e43c6d7f1265ea0b7cb Mon Sep 17 00:00:00 2001 From: Blaine Jester Date: Mon, 21 Mar 2022 16:17:46 -0700 Subject: [PATCH 1/4] Breakout mastery criteria; add percentage support for pages completion model --- js/CompletionCriteria.js | 67 ++++----------- js/MasteryCriteria.js | 71 ++++++++++++++++ le_utils/constants/completion_criteria.py | 99 +++++++++-------------- le_utils/constants/mastery_criteria.py | 76 +++++++++++++++++ spec/schema-completion_criteria.json | 67 ++++----------- spec/schema-mastery_criteria.json | 58 +++++++++++++ tests/test_schemas.py | 17 +++- 7 files changed, 290 insertions(+), 165 deletions(-) create mode 100644 js/MasteryCriteria.js create mode 100644 le_utils/constants/mastery_criteria.py create mode 100644 spec/schema-mastery_criteria.json diff --git a/js/CompletionCriteria.js b/js/CompletionCriteria.js index 9189c87..e7282ef 100644 --- a/js/CompletionCriteria.js +++ b/js/CompletionCriteria.js @@ -11,6 +11,7 @@ export default { }; export const SCHEMA = { + "$id": "/schemas/completion_criteria", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "description": "Schema for completion criteria of content nodes", @@ -27,56 +28,7 @@ export const SCHEMA = { "reference" ] }, - "mastery_criteria": { - "type": "object", - "$comment": "TODO move to separate schema", - "additionalProperties": false, - "required": ["mastery_model"], - "properties": { - "m": true, - "n": true, - "mastery_model": { - "type": "string", - "enum": [ - "do_all", - "m_of_n", - "num_correct_in_a_row_2", - "num_correct_in_a_row_3", - "num_correct_in_a_row_5", - "num_correct_in_a_row_10" - ] - } - }, - "anyOf": [ - { - "properties": { - "mastery_model": { - "const": "m_of_n" - } - }, - "required": ["m", "n"] - }, - { - "properties": { - "mastery_model": { - "enum": [ - "do_all", - "num_correct_in_a_row_2", - "num_correct_in_a_row_3", - "num_correct_in_a_row_5", - "num_correct_in_a_row_10" - ] - }, - "m": { - "type": "null" - }, - "n": { - "type": "null" - } - } - } - ] - } + "mastery_criteria": { "$ref": "/schemas/mastery_criteria" } }, "properties": { "model": { @@ -105,6 +57,21 @@ export const SCHEMA = { }, "required": ["threshold"] }, + { + "properties": { + "model": { + "const": "pages" + }, + "threshold": { + "type": "string", + "pattern": "^(100|[1-9][0-9]?)%$", + "description": "A percentage", + "minLength": 2, + "maxLength": 4 + } + }, + "required": ["threshold"] + }, { "properties": { "model": { diff --git a/js/MasteryCriteria.js b/js/MasteryCriteria.js new file mode 100644 index 0000000..5553722 --- /dev/null +++ b/js/MasteryCriteria.js @@ -0,0 +1,71 @@ +// -*- coding: utf-8 -*- +// Generated by scripts/generate_from_specs.py +// MasteryCriteria + +export default { + DO_ALL: "do_all", + M_OF_N: "m_of_n", + NUM_CORRECT_IN_A_ROW_10: "num_correct_in_a_row_10", + 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", +}; + +export const SCHEMA = { + "$id": "/schemas/mastery_criteria", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "description": "Schema for mastery criteria of exercise content types", + "additionalProperties": false, + "required": ["mastery_model"], + "definitions": { + "mastery_model": { + "type": "string", + "$exportConstants": "mastery_criteria", + "enum": [ + "do_all", + "m_of_n", + "num_correct_in_a_row_2", + "num_correct_in_a_row_3", + "num_correct_in_a_row_5", + "num_correct_in_a_row_10" + ] + } + }, + "properties": { + "m": true, + "n": true, + "mastery_model": { + "$ref": "#/definitions/mastery_model" + } + }, + "anyOf": [ + { + "properties": { + "mastery_model": { + "const": "m_of_n" + } + }, + "required": ["m", "n"] + }, + { + "properties": { + "mastery_model": { + "enum": [ + "do_all", + "num_correct_in_a_row_2", + "num_correct_in_a_row_3", + "num_correct_in_a_row_5", + "num_correct_in_a_row_10" + ] + }, + "m": { + "type": "null" + }, + "n": { + "type": "null" + } + } + } + ] +}; diff --git a/le_utils/constants/completion_criteria.py b/le_utils/constants/completion_criteria.py index 724d207..230ac28 100644 --- a/le_utils/constants/completion_criteria.py +++ b/le_utils/constants/completion_criteria.py @@ -27,12 +27,28 @@ ] SCHEMA = { + "$id": "/schemas/completion_criteria", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", "description": "Schema for completion criteria of content nodes", + "additionalProperties": False, + "definitions": { + "model": { + "type": "string", + "$exportConstants": "completion_criteria", + "enum": ["time", "approx_time", "pages", "mastery", "reference"], + }, + "mastery_criteria": {"$ref": "/schemas/mastery_criteria"}, + }, + "properties": { + "model": {"$ref": "#/definitions/model"}, + "learner_managed": {"type": "boolean"}, + "threshold": True, + }, + "required": ["model"], "anyOf": [ { - "required": ["threshold"], "properties": { - "threshold": {"exclusiveMinimum": 0, "type": "number"}, "model": { "anyOf": [ {"const": "time"}, @@ -40,79 +56,36 @@ {"const": "pages"}, ] }, + "threshold": {"type": "number", "exclusiveMinimum": 0}, }, + "required": ["threshold"], }, { + "properties": { + "model": {"const": "pages"}, + "threshold": { + "type": "string", + "pattern": "^(100|[1-9][0-9]?)%$", + "description": "A percentage", + "minLength": 2, + "maxLength": 4, + }, + }, "required": ["threshold"], + }, + { "properties": { - "threshold": {"$ref": "#/definitions/mastery_criteria"}, "model": {"const": "mastery"}, + "threshold": {"$ref": "#/definitions/mastery_criteria"}, }, + "required": ["threshold"], }, { - "required": [], "properties": { - "threshold": {"type": "null"}, "model": {"const": "reference"}, + "threshold": {"type": "null"}, }, + "required": [], }, ], - "required": ["model"], - "additionalProperties": False, - "definitions": { - "model": { - "$exportConstants": "completion_criteria", - "enum": ["time", "approx_time", "pages", "mastery", "reference"], - "type": "string", - }, - "mastery_criteria": { - "type": "object", - "required": ["mastery_model"], - "additionalProperties": False, - "$comment": "TODO move to separate schema", - "anyOf": [ - { - "required": ["m", "n"], - "properties": {"mastery_model": {"const": "m_of_n"}}, - }, - { - "properties": { - "m": {"type": "null"}, - "mastery_model": { - "enum": [ - "do_all", - "num_correct_in_a_row_2", - "num_correct_in_a_row_3", - "num_correct_in_a_row_5", - "num_correct_in_a_row_10", - ] - }, - "n": {"type": "null"}, - } - }, - ], - "properties": { - "mastery_model": { - "enum": [ - "do_all", - "m_of_n", - "num_correct_in_a_row_2", - "num_correct_in_a_row_3", - "num_correct_in_a_row_5", - "num_correct_in_a_row_10", - ], - "type": "string", - }, - "m": True, - "n": True, - }, - }, - }, - "$schema": "http://json-schema.org/draft-07/schema#", - "type": "object", - "properties": { - "threshold": True, - "model": {"$ref": "#/definitions/model"}, - "learner_managed": {"type": "boolean"}, - }, } diff --git a/le_utils/constants/mastery_criteria.py b/le_utils/constants/mastery_criteria.py new file mode 100644 index 0000000..353e1fc --- /dev/null +++ b/le_utils/constants/mastery_criteria.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +# Generated by scripts/generate_from_specs.py +from __future__ import unicode_literals + +# MasteryCriteria + +DO_ALL = "do_all" +M_OF_N = "m_of_n" +NUM_CORRECT_IN_A_ROW_10 = "num_correct_in_a_row_10" +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" + +choices = ( + (DO_ALL, "Do All"), + (M_OF_N, "M Of N"), + (NUM_CORRECT_IN_A_ROW_10, "Num Correct In A Row 10"), + (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"), +) + +MASTERYCRITERIALIST = [ + DO_ALL, + M_OF_N, + NUM_CORRECT_IN_A_ROW_10, + NUM_CORRECT_IN_A_ROW_2, + NUM_CORRECT_IN_A_ROW_3, + NUM_CORRECT_IN_A_ROW_5, +] + +SCHEMA = { + "$id": "/schemas/mastery_criteria", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "description": "Schema for mastery criteria of exercise content types", + "additionalProperties": False, + "required": ["mastery_model"], + "definitions": { + "mastery_model": { + "type": "string", + "$exportConstants": "mastery_criteria", + "enum": [ + "do_all", + "m_of_n", + "num_correct_in_a_row_2", + "num_correct_in_a_row_3", + "num_correct_in_a_row_5", + "num_correct_in_a_row_10", + ], + } + }, + "properties": { + "m": True, + "n": True, + "mastery_model": {"$ref": "#/definitions/mastery_model"}, + }, + "anyOf": [ + {"properties": {"mastery_model": {"const": "m_of_n"}}, "required": ["m", "n"]}, + { + "properties": { + "mastery_model": { + "enum": [ + "do_all", + "num_correct_in_a_row_2", + "num_correct_in_a_row_3", + "num_correct_in_a_row_5", + "num_correct_in_a_row_10", + ] + }, + "m": {"type": "null"}, + "n": {"type": "null"}, + } + }, + ], +} diff --git a/spec/schema-completion_criteria.json b/spec/schema-completion_criteria.json index ca947f0..de7969b 100644 --- a/spec/schema-completion_criteria.json +++ b/spec/schema-completion_criteria.json @@ -1,4 +1,5 @@ { + "$id": "/schemas/completion_criteria", "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "description": "Schema for completion criteria of content nodes", @@ -15,56 +16,7 @@ "reference" ] }, - "mastery_criteria": { - "type": "object", - "$comment": "TODO move to separate schema", - "additionalProperties": false, - "required": ["mastery_model"], - "properties": { - "m": true, - "n": true, - "mastery_model": { - "type": "string", - "enum": [ - "do_all", - "m_of_n", - "num_correct_in_a_row_2", - "num_correct_in_a_row_3", - "num_correct_in_a_row_5", - "num_correct_in_a_row_10" - ] - } - }, - "anyOf": [ - { - "properties": { - "mastery_model": { - "const": "m_of_n" - } - }, - "required": ["m", "n"] - }, - { - "properties": { - "mastery_model": { - "enum": [ - "do_all", - "num_correct_in_a_row_2", - "num_correct_in_a_row_3", - "num_correct_in_a_row_5", - "num_correct_in_a_row_10" - ] - }, - "m": { - "type": "null" - }, - "n": { - "type": "null" - } - } - } - ] - } + "mastery_criteria": { "$ref": "/schemas/mastery_criteria" } }, "properties": { "model": { @@ -93,6 +45,21 @@ }, "required": ["threshold"] }, + { + "properties": { + "model": { + "const": "pages" + }, + "threshold": { + "type": "string", + "pattern": "^(100|[1-9][0-9]?)%$", + "description": "A percentage", + "minLength": 2, + "maxLength": 4 + } + }, + "required": ["threshold"] + }, { "properties": { "model": { diff --git a/spec/schema-mastery_criteria.json b/spec/schema-mastery_criteria.json new file mode 100644 index 0000000..ef6d459 --- /dev/null +++ b/spec/schema-mastery_criteria.json @@ -0,0 +1,58 @@ +{ + "$id": "/schemas/mastery_criteria", + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "description": "Schema for mastery criteria of exercise content types", + "additionalProperties": false, + "required": ["mastery_model"], + "definitions": { + "mastery_model": { + "type": "string", + "$exportConstants": "mastery_criteria", + "enum": [ + "do_all", + "m_of_n", + "num_correct_in_a_row_2", + "num_correct_in_a_row_3", + "num_correct_in_a_row_5", + "num_correct_in_a_row_10" + ] + } + }, + "properties": { + "m": true, + "n": true, + "mastery_model": { + "$ref": "#/definitions/mastery_model" + } + }, + "anyOf": [ + { + "properties": { + "mastery_model": { + "const": "m_of_n" + } + }, + "required": ["m", "n"] + }, + { + "properties": { + "mastery_model": { + "enum": [ + "do_all", + "num_correct_in_a_row_2", + "num_correct_in_a_row_3", + "num_correct_in_a_row_5", + "num_correct_in_a_row_10" + ] + }, + "m": { + "type": "null" + }, + "n": { + "type": "null" + } + } + } + ] +} \ No newline at end of file diff --git a/tests/test_schemas.py b/tests/test_schemas.py index bf5bff5..6eb053e 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -5,7 +5,8 @@ import pytest -from le_utils.constants.completion_criteria import SCHEMA +from le_utils.constants import completion_criteria +from le_utils.constants import mastery_criteria try: @@ -15,12 +16,24 @@ jsonschema = None +resolver = None +if jsonschema is not None: + # this is an example of how to include the mastery criteria schema, which is referenced by the + # completion criteria schema, in the schema resolver so that it validates + resolver = jsonschema.RefResolver.from_schema(mastery_criteria.SCHEMA) + resolver.store.update( + jsonschema.RefResolver.from_schema(completion_criteria.SCHEMA).store + ) + + def _validate(data): """ :param data: Dictionary of data to validate :raises: jsonschema.ValidationError: When invalid """ - jsonschema.validate(instance=data, schema=SCHEMA) + jsonschema.validate( + instance=data, schema=completion_criteria.SCHEMA, resolver=resolver + ) @contextlib.contextmanager From 4ec40e1d442cc6eb6f79cd2b0cb1c97061ce29a0 Mon Sep 17 00:00:00 2001 From: Blaine Jester Date: Mon, 21 Mar 2022 16:27:35 -0700 Subject: [PATCH 2/4] Fix git:// issue --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b0e98c3..91144f0 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ exclude: (\.git/|\.tox/|\.venv/|le_utils\.egg-info) repos: -- repo: git://github.com/pre-commit/pre-commit-hooks +- repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.0.0 hooks: - id: trailing-whitespace From 6a0dffdd9349cb836094449ba412a08fdc6d8f49 Mon Sep 17 00:00:00 2001 From: Blaine Jester Date: Tue, 22 Mar 2022 08:01:05 -0700 Subject: [PATCH 3/4] Add tests for percentage validation --- tests/test_schemas.py | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/test_schemas.py b/tests/test_schemas.py index 6eb053e..4553173 100644 --- a/tests/test_schemas.py +++ b/tests/test_schemas.py @@ -122,6 +122,23 @@ def test_completion_criteria__pages_model__valid(): ) +@pytest.mark.skipif(jsonschema is None, reason="jsonschema package is unavailable") +def test_completion_criteria__pages_model__percentage__valid(): + with _assert_not_raises(jsonschema.ValidationError): + _validate( + { + "model": "pages", + "threshold": "99%", + } + ) + _validate( + { + "model": "pages", + "threshold": "1%", + } + ) + + @pytest.mark.skipif(jsonschema is None, reason="jsonschema package is unavailable") def test_completion_criteria__pages_model__invalid(): with pytest.raises(jsonschema.ValidationError): @@ -141,6 +158,25 @@ def test_completion_criteria__pages_model__invalid(): ) +@pytest.mark.skipif(jsonschema is None, reason="jsonschema package is unavailable") +def test_completion_criteria__pages_model__percentage__invalid(): + with pytest.raises(jsonschema.ValidationError): + _validate( + { + "model": "pages", + "threshold": "0%", + "learner_managed": False, + } + ) + with pytest.raises(jsonschema.ValidationError): + _validate( + { + "model": "pages", + "threshold": "101%", + } + ) + + @pytest.mark.skipif(jsonschema is None, reason="jsonschema package is unavailable") def test_completion_criteria__mastery_model__valid(): with _assert_not_raises(jsonschema.ValidationError): From d49cc6a25ec63e7b4f2268afd9cbefe912289d65 Mon Sep 17 00:00:00 2001 From: Blaine Jester Date: Tue, 29 Mar 2022 12:55:24 -0700 Subject: [PATCH 4/4] Bump version --- js/package.json | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/js/package.json b/js/package.json index 76972f6..37482e1 100644 --- a/js/package.json +++ b/js/package.json @@ -27,5 +27,5 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, - "version": "0.1.39" + "version": "0.1.40" } \ No newline at end of file diff --git a/setup.py b/setup.py index 47c4739..634248b 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name="le-utils", packages=find_packages(), - version="0.1.39", + version="0.1.40", description="LE-Utils contains shared constants used in Kolibri, Ricecooker, and Kolibri Studio.", long_description=long_description, long_description_content_type="text/markdown",