From 2bbb27ddb2fb79f4b01c9742acf45143b1a9fbdb Mon Sep 17 00:00:00 2001 From: Sanu Ann Date: Thu, 18 Mar 2021 22:26:53 -0400 Subject: [PATCH 01/69] update examples --- .../tests/data/activities/activity1.jsonld | 21 +++++++++---------- .../data/activities/activity1_embed.jsonld | 4 ++-- .../activities/items/activity1_total_score | 2 +- .../tests/data/activities/items/item1.jsonld | 3 +-- .../tests/data/activities/items/item2.jsonld | 2 +- .../tests/data/protocols/protocol1.jsonld | 3 ++- .../data/protocols/protocol1_embed.jsonld | 6 +++--- 7 files changed, 20 insertions(+), 21 deletions(-) diff --git a/reproschema/tests/data/activities/activity1.jsonld b/reproschema/tests/data/activities/activity1.jsonld index b64fb5d..03d18f7 100644 --- a/reproschema/tests/data/activities/activity1.jsonld +++ b/reproschema/tests/data/activities/activity1.jsonld @@ -4,7 +4,7 @@ "@id": "activity1.jsonld", "prefLabel": "Example 1", "description": "Activity example 1", - "schemaVersion": "1.0.0-rc2", + "schemaVersion": "1.0.0-rc3", "version": "0.0.1", "citation": "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", "image": { @@ -15,18 +15,18 @@ "en": "Over the last 2 weeks, how often have you been bothered by any of the following problems?", "es": "Durante las últimas 2 semanas, ¿con qué frecuencia le han molestado los siguintes problemas?" }, - "messages": [ - { - "message": "Test message: Triggered when item1 value is greater than 1", - "jsExpression": "item1 > 1" - } - ], "compute": [ { - "variableName": "activity1_total_score", - "jsExpression": "item1 + item2" + "variableName": "phq9_total_score", + "jsExpression": "phq9_1 + phq9_2 + phq9_3 + phq9_4 + phq9_5 + phq9_6 + phq9_7 + phq9_8 + phq9_9" } ], + "messages": [ + { + "message": "Test message: Triggered when item1 value is greater than 1", + "jsExpression": "item1 > 1" + } + ], "ui": { "addProperties": [ { "isAbout": "items/item1.jsonld", @@ -46,8 +46,7 @@ ], "order": [ "items/item1.jsonld", - "items/item2.jsonld", - "items/activity1_total_score" + "items/item2.jsonld" ], "shuffle": false } diff --git a/reproschema/tests/data/activities/activity1_embed.jsonld b/reproschema/tests/data/activities/activity1_embed.jsonld index 1a08595..dc775f3 100644 --- a/reproschema/tests/data/activities/activity1_embed.jsonld +++ b/reproschema/tests/data/activities/activity1_embed.jsonld @@ -4,7 +4,7 @@ "@id": "activity1.jsonld", "prefLabel": "Example 1", "description": "Activity example 1", - "schemaVersion": "1.0.0-rc2", + "schemaVersion": "1.0.0-rc3", "version": "0.0.1", "citation": "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", "preamble": { @@ -24,7 +24,7 @@ "@id": "items/item1.jsonld", "prefLabel": "item1", "description": "Q1 of example 1", - "schemaVersion": "1.0.0-rc2", + "schemaVersion": "1.0.0-rc3", "version": "0.0.1", "question": { "en": "Little interest or pleasure in doing things", diff --git a/reproschema/tests/data/activities/items/activity1_total_score b/reproschema/tests/data/activities/items/activity1_total_score index b67faf0..c02f542 100644 --- a/reproschema/tests/data/activities/items/activity1_total_score +++ b/reproschema/tests/data/activities/items/activity1_total_score @@ -4,7 +4,7 @@ "@id": "activity1_total_score", "prefLabel": "activity1_total_score", "description": "Score item for Activity 1", - "schemaVersion": "1.0.0-rc2", + "schemaVersion": "1.0.0-rc3", "version": "0.0.1", "ui": { "inputType": "number", diff --git a/reproschema/tests/data/activities/items/item1.jsonld b/reproschema/tests/data/activities/items/item1.jsonld index 7f171e4..ce719e1 100644 --- a/reproschema/tests/data/activities/items/item1.jsonld +++ b/reproschema/tests/data/activities/items/item1.jsonld @@ -4,7 +4,7 @@ "@id": "item1.jsonld", "prefLabel": "item1", "description": "Q1 of example 1", - "schemaVersion": "1.0.0-rc1.post", + "schemaVersion": "1.0.0-rc3", "version": "0.0.1", "audio": { "@type": "AudioObject", @@ -69,5 +69,4 @@ "value": {"@id": "http://example.com/iri-example"} } ] - } diff --git a/reproschema/tests/data/activities/items/item2.jsonld b/reproschema/tests/data/activities/items/item2.jsonld index 0b1d4cc..dbcbcf3 100644 --- a/reproschema/tests/data/activities/items/item2.jsonld +++ b/reproschema/tests/data/activities/items/item2.jsonld @@ -4,7 +4,7 @@ "@id": "item2.jsonld", "prefLabel": "item2", "description": "Q2 of example 1", - "schemaVersion": "0.0.1", + "schemaVersion": "1.0.0-rc3", "version": "0.0.1", "question": { "en": "Current temperature.", diff --git a/reproschema/tests/data/protocols/protocol1.jsonld b/reproschema/tests/data/protocols/protocol1.jsonld index c9fc432..c11b145 100644 --- a/reproschema/tests/data/protocols/protocol1.jsonld +++ b/reproschema/tests/data/protocols/protocol1.jsonld @@ -7,8 +7,9 @@ "es": "Protocol1_es" }, "description": "example Protocol", - "schemaVersion": "1.0.0-rc2", + "schemaVersion": "1.0.0-rc3", "version": "0.0.1", + "landingPage": "http://example.com/sample-readme.md", "messages": [ { "message": "Test message: Triggered when item1 value is greater than 0", diff --git a/reproschema/tests/data/protocols/protocol1_embed.jsonld b/reproschema/tests/data/protocols/protocol1_embed.jsonld index 3681040..804bd47 100644 --- a/reproschema/tests/data/protocols/protocol1_embed.jsonld +++ b/reproschema/tests/data/protocols/protocol1_embed.jsonld @@ -7,7 +7,7 @@ "es": "Protocol1_es" }, "description": "example Protocol", - "schemaVersion": "1.0.0-rc2", + "schemaVersion": "1.0.0-rc3", "version": "0.0.1", "ui": { "addProperties": [ @@ -27,7 +27,7 @@ "@id": "../activities/activity1.jsonld", "prefLabel": "Example 1", "description": "Activity example 1", - "schemaVersion": "1.0.0-rc2", + "schemaVersion": "1.0.0-rc3", "version": "0.0.1", "citation": "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", "preamble": { @@ -49,7 +49,7 @@ "@id": "../activities/items/item1.jsonld", "prefLabel": "item1", "description": "Q1 of example 1", - "schemaVersion": "1.0.0-rc2", + "schemaVersion": "1.0.0-rc3", "version": "0.0.1", "question": { "en": "Little interest or pleasure in doing things", From 385eed51ace74cdaadd9356d243766a896300271 Mon Sep 17 00:00:00 2001 From: Sanu Ann Date: Thu, 18 Mar 2021 22:44:03 -0400 Subject: [PATCH 02/69] update shacl shape too --- reproschema/tests/reproschema-shacl.ttl | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/reproschema/tests/reproschema-shacl.ttl b/reproschema/tests/reproschema-shacl.ttl index a62498f..d6841e7 100644 --- a/reproschema/tests/reproschema-shacl.ttl +++ b/reproschema/tests/reproschema-shacl.ttl @@ -158,7 +158,8 @@ reproschema:ProtocolShape a sh:NodeShape ; [ sh:node reproschema:MediaObjectShape ; sh:path schema:video ], - [ sh:nodeKind sh:IRI ; + [ sh:datatype xsd:anyURI ; + sh:pattern "((http|https)://)(www.)?[a-zA-Z0-9@:%._\\+~#?&//=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%._\\+~#?&//=]*)" ; sh:path reproschema:landingPage ], [ sh:datatype rdf:langString ; @@ -353,8 +354,9 @@ reproschema:IsVisShape a sh:NodeShape ; sh:minCount 1 ; sh:path schema:method ], - [ sh:datatype rdf:langString ; + [ sh:datatype xsd:anyURI ; sh:minCount 1 ; + sh:pattern "((http|https)://)(www.)?[a-zA-Z0-9@:%._\\+~#?&//=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%._\\+~#?&//=]*)" ; sh:path schema:url ], [ sh:datatype rdf:langString ; @@ -447,7 +449,8 @@ prov:SoftwareAgentShape a sh:NodeShape ; sh:property [ sh:datatype rdf:langString ; sh:path schema:version ], - [ sh:nodeKind sh:IRI ; + [ sh:datatype xsd:anyURI ; + sh:pattern "((http|https)://)(www.)?[a-zA-Z0-9@:%._\\+~#?&//=]{2,256}\\.[a-z]{2,6}\\b([-a-zA-Z0-9@:%._\\+~#?&//=]*)" ; sh:path schema:url ] ; sh:targetClass reproschema:SoftwareAgent . From 139eb23b4e9da0b2ee01e7c25caa10869f7a7d08 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 5 Jun 2021 19:54:42 +0200 Subject: [PATCH 03/69] fix call to base class set default method --- reproschema/models/activity.py | 2 +- reproschema/models/item.py | 2 +- reproschema/models/protocol.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index 25d27d2..ca2350a 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -28,7 +28,7 @@ def get_URI(self): # image def set_defaults(self, name): - self._ReproschemaSchema__set_defaults(name) # this looks wrong + self._SchemaBase__set_defaults(name) self.set_ui_shuffle(False) def update_activity(self, item_info): diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 595d67d..1c14f2c 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -24,7 +24,7 @@ def set_URI(self, URI): # readonlyValue def set_defaults(self, name): - self._ReproschemaSchema__set_defaults(name) # this looks wrong + self._SchemaBase__set_defaults(name) self.schema_file = name self.schema["@id"] = name self.set_input_type_as_char() diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index 284f2c8..21971e6 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -38,7 +38,7 @@ def set_ui_shuffle(self, shuffle=False): self.schema["ui"]["shuffle"] = shuffle def set_defaults(self, name): - self._ReproschemaSchema__set_defaults(name) # this looks wrong + self._SchemaBase__set_defaults(name) self.set_landing_page("../../README-en.md") self.set_ui_allow() self.set_ui_shuffle(False) From 418eb409a9e829d4577c4b757ee044480a7f8109 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 5 Jun 2021 19:55:14 +0200 Subject: [PATCH 04/69] update default schema version --- reproschema/models/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index a2d59db..3c90990 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -12,7 +12,7 @@ class to deal with reproschema schemas def __init__(self, version): URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" - VERSION = version or "1.0.0-rc2" + VERSION = version or "1.0.0-rc4" self.schema = { "@context": URL + VERSION + "/contexts/generic", From ab9ff7fd8a28b2e19bf0fd613814d4f5da546074 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 5 Jun 2021 19:57:16 +0200 Subject: [PATCH 05/69] add possibility to choose extension of filename --- reproschema/models/base.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 3c90990..b5d7f1e 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -21,9 +21,9 @@ def __init__(self, version): "version": "0.0.1", } - def set_filename(self, name): - self.schema_file = name + "_schema" - self.schema["@id"] = name + "_schema" + def set_filename(self, name, ext=".jsonld"): + self.schema_file = name + "_schema" + ext + self.schema["@id"] = name + "_schema" + ext def get_name(self): return self.schema_file.replace("_schema", "") From 471c8bde8ee55995f891d1044c237eae5fff326d Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 5 Jun 2021 23:36:55 +0200 Subject: [PATCH 06/69] add some jsonld to test against --- reproschema/models/tests/.gitignore | 1 + .../models/tests/data/items/default.jsonld | 17 +++++++++ .../models/tests/data/items/radio.jsonld | 35 +++++++++++++++++++ .../models/tests/data/items/text.jsonld | 17 +++++++++ 4 files changed, 70 insertions(+) create mode 100644 reproschema/models/tests/.gitignore create mode 100644 reproschema/models/tests/data/items/default.jsonld create mode 100644 reproschema/models/tests/data/items/radio.jsonld create mode 100644 reproschema/models/tests/data/items/text.jsonld diff --git a/reproschema/models/tests/.gitignore b/reproschema/models/tests/.gitignore new file mode 100644 index 0000000..a6fbd9a --- /dev/null +++ b/reproschema/models/tests/.gitignore @@ -0,0 +1 @@ +items/*.jsonld diff --git a/reproschema/models/tests/data/items/default.jsonld b/reproschema/models/tests/data/items/default.jsonld new file mode 100644 index 0000000..9ab8eca --- /dev/null +++ b/reproschema/models/tests/data/items/default.jsonld @@ -0,0 +1,17 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "default.jsonld", + "prefLabel": "default", + "description": "default", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "text" + }, + "question": {}, + "responseOptions": { + "valueType": "xsd:string", + "maxLength": 300 + } +} diff --git a/reproschema/models/tests/data/items/radio.jsonld b/reproschema/models/tests/data/items/radio.jsonld new file mode 100644 index 0000000..dc5658d --- /dev/null +++ b/reproschema/models/tests/data/items/radio.jsonld @@ -0,0 +1,35 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "radio.jsonld", + "prefLabel": "radio", + "description": "radio", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "question for radio item" + }, + "ui": { + "inputType": "radio" + }, + "responseOptions": { + "valueType": "xsd:integer", + "minValue": 0, + "maxValue": 1, + "multipleChoice": false, + "choices": [ + { + "name": { + "en": "Not at all" + }, + "value": 0 + }, + { + "name": { + "en": "Several days" + }, + "value": 1 + } + ] + } +} diff --git a/reproschema/models/tests/data/items/text.jsonld b/reproschema/models/tests/data/items/text.jsonld new file mode 100644 index 0000000..460d8b4 --- /dev/null +++ b/reproschema/models/tests/data/items/text.jsonld @@ -0,0 +1,17 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "text.jsonld", + "prefLabel": "text", + "description": "text", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "text" + }, + "question": {}, + "responseOptions": { + "valueType": "xsd:string", + "maxLength": 100 + } +} From b4c1c2ca744b2384d8ae05ff86b9e57e1bb131df Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 5 Jun 2021 23:38:07 +0200 Subject: [PATCH 07/69] add response options class --- reproschema/models/item.py | 49 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 1c14f2c..16a2159 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -156,3 +156,52 @@ def sort(self): "responseOptions", ] self.sort_schema(schema_order) + + +class ResponseOption: + """ + class to deal with reproschema response options + """ + + def __init__(self): + self.options = { + "valueType": "", + "minValue": 0, + "maxValue": 0, + "choices": [], + "multipleChoice": False, + } + + # "readonlyValue": False, + # "maxLength": 0, + + def set_type(self, type): + if type == "int": + self.options["valueType"] = "xsd:integer" + elif type == "str": + self.options["valueType"] = "xsd:string" + elif type == "float": + self.options["valueType"] = "xsd:float" + elif type == "date": + self.options["valueType"] = "xsd:date" + elif type == "datetime": + self.options["valueType"] = "datetime" + + def set_input_type_as_date(self): + self.set_input_type("date") + self.set_response_options({"valueType": "xsd:date"}) + + def set_min(self, value): + self.options["minValue"] = value + + def set_max(self, value): + self.options["maxValue"] = value + + def set_length(self, value): + self.options["maxLength"] = value + + def set_multiple_choice(self, value): + self.options["multipleChoice"] = value + + def add_choice(self, choice): + self.options["choices"].append(choice) From a41106a5d3ba04635944d9b6953231e5db2572d8 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 5 Jun 2021 23:39:53 +0200 Subject: [PATCH 08/69] implement response option class --- reproschema/models/item.py | 67 +++++++++++++------- reproschema/models/tests/test_item.py | 90 +++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 24 deletions(-) create mode 100644 reproschema/models/tests/test_item.py diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 16a2159..88f25c9 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -1,9 +1,12 @@ from .base import SchemaBase +DEFAULT_LANG = "en" + + class Item(SchemaBase): """ - class to deal with reproschema activities + class to deal with reproschema items """ schema_type = "reproschema:Field" @@ -13,8 +16,9 @@ def __init__(self, version=None): self.schema["ui"] = {"inputType": []} self.schema["question"] = {} self.schema["responseOptions"] = {} - # default input type is "char" - self.set_input_type_as_char() + self.response_options = ResponseOption() + # default input type is "text" + self.set_input_type_as_text() def set_URI(self, URI): self.URI = URI @@ -27,16 +31,20 @@ def set_defaults(self, name): self._SchemaBase__set_defaults(name) self.schema_file = name self.schema["@id"] = name - self.set_input_type_as_char() + self.set_input_type_as_text() + + def set_filename(self, name, ext=".jsonld"): + self.schema_file = name + ext + self.schema["@id"] = name + ext - def set_question(self, question, lang="en"): + def set_question(self, question, lang=DEFAULT_LANG): self.schema["question"][lang] = question def set_input_type(self, input_type): self.schema["ui"]["inputType"] = input_type - def set_response_options(self, response_options): - self.schema["responseOptions"] = response_options + def set_response_options(self): + self.schema["responseOptions"] = self.response_options.options """ @@ -46,14 +54,16 @@ def set_response_options(self, response_options): def set_input_type_as_radio(self, response_options): self.set_input_type("radio") - self.set_response_options(response_options) + self.response_options = response_options + self.set_response_options() def set_input_type_as_select(self, response_options): self.set_input_type("select") - self.set_response_options(response_options) + self.response_options = response_options + self.set_response_options() def set_input_type_as_slider(self): - self.set_input_type_as_char() # until the slide item of the ui is fixed + self.set_input_type_as_text() # until the slide item of the ui is fixed # self.set_input_type("slider") # self.set_response_options({"valueType": "xsd:string"}) @@ -63,12 +73,13 @@ def set_input_type_as_language(self): self.set_input_type("selectLanguage") - response_options = { - "valueType": "xsd:string", - "multipleChoice": True, - "choices": URL + "master/resources/languages.json", - } - self.set_response_options(response_options) + self.response_options.set_type("str") + self.response_options.set_multiple_choice(True) + self.response_options["options"]["choices"] = ( + URL + "master/resources/languages.json" + ) + + self.set_response_options() """ @@ -76,17 +87,13 @@ def set_input_type_as_language(self): """ - def set_input_type_as_char(self): - self.set_input_type("text") - self.set_response_options({"valueType": "xsd:string"}) - def set_input_type_as_int(self): self.set_input_type("number") - self.set_response_options({"valueType": "xsd:integer"}) + self.response_options.set_type("int") def set_input_type_as_float(self): self.set_input_type("float") - self.set_response_options({"valueType": "xsd:float"}) + self.response_options.set_type("float") def set_input_type_as_time_range(self): self.set_input_type("timeRange") @@ -102,9 +109,21 @@ def set_input_type_as_date(self): """ - def set_input_type_as_multitext(self, max_length=300): + def set_input_type_as_text(self, length=300): self.set_input_type("text") - self.set_response_options({"valueType": "xsd:string", "maxLength": max_length}) + self.response_options.set_type("str") + self.response_options.set_length(length) + self.response_options.options.pop("maxValue", None) + self.response_options.options.pop("minValue", None) + self.response_options.options.pop("multipleChoice", None) + self.response_options.options.pop("choices", None) + self.set_response_options() + + def set_input_type_as_multitext(self, length=300): + self.set_input_type("multitext") + self.response_options.set_type("str") + self.response_options.set_length(length) + self.set_response_options() # TODO # email: EmailInput/EmailInput.vue diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py new file mode 100644 index 0000000..f9dde97 --- /dev/null +++ b/reproschema/models/tests/test_item.py @@ -0,0 +1,90 @@ +import os, sys, json + +from ..item import Item, ResponseOption + +my_path = os.path.dirname(os.path.abspath(__file__)) +sys.path.insert(0, my_path + "/../") + +item_dir = os.path.join(my_path, "items") +if not os.path.exists(item_dir): + os.makedirs(os.path.join(item_dir)) + +# integer +# float +# select +# multiradio +# multiselect +# char +# time range +# date +# slider +# language + + +def test_default(): + + item = Item("1.0.0-rc4") + item.set_defaults("default") + item.set_filename("default") + + item.sort() + item.write(item_dir) + + item_content, expected = load_jsons(item) + + assert item_content == expected + + +def test_text(): + + item = Item("1.0.0-rc4") + item.set_defaults("text") + item.set_filename("text") + item.set_input_type_as_text(100) + + item.sort() + item.write(item_dir) + + item_content, expected = load_jsons(item) + + assert item_content == expected + + +def test_radio(): + + item = Item("1.0.0-rc4") + item.set_defaults("radio") + item.set_filename("radio") + + item.set_question("question for radio item", "en") + + response_options = ResponseOption() + response_options.set_type("int") + response_options.add_choice({"name": {"en": "Not at all"}, "value": 0}) + response_options.add_choice({"name": {"en": "Several days"}, "value": 1}) + response_options.set_max(1) + + item.set_input_type_as_radio(response_options) + + item.sort() + item.write(item_dir) + + item_content, expected = load_jsons(item) + + assert item_content == expected + + +def load_jsons(item): + output_file = os.path.join(item_dir, item.get_filename()) + item_content = read_json(output_file) + + data_file = os.path.join(my_path, "data", "items", item.get_filename()) + expected = read_json(data_file) + + return item_content, expected + + +def read_json(file): + + with open(file, "r") as ff: + return json.load(ff) From 53d97e74b545a011600bf20369741c67e7694de8 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 00:07:46 +0200 Subject: [PATCH 09/69] implement float and integer and refactor --- reproschema/models/base.py | 2 +- reproschema/models/item.py | 21 +++++---- .../models/tests/data/items/float.jsonld | 18 ++++++++ .../models/tests/data/items/integer.jsonld | 18 ++++++++ .../models/tests/data/items/text.jsonld | 4 +- reproschema/models/tests/test_item.py | 43 +++++++++++++++---- 6 files changed, 86 insertions(+), 20 deletions(-) create mode 100644 reproschema/models/tests/data/items/float.jsonld create mode 100644 reproschema/models/tests/data/items/integer.jsonld diff --git a/reproschema/models/base.py b/reproschema/models/base.py index b5d7f1e..ac9c7ac 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -56,7 +56,7 @@ def sort_ui(self, ui_order): reordered_dict = {k: self.schema["ui"][k] for k in ui_order} self.schema["ui"] = reordered_dict - def write(self, output_dir): + def __write(self, output_dir): with open(os.path.join(output_dir, self.schema_file), "w") as ff: json.dump(self.schema, ff, sort_keys=False, indent=4) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 88f25c9..b657624 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -27,10 +27,9 @@ def set_URI(self, URI): # image # readonlyValue - def set_defaults(self, name): + def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) - self.schema_file = name - self.schema["@id"] = name + self.set_filename(name) self.set_input_type_as_text() def set_filename(self, name, ext=".jsonld"): @@ -47,9 +46,7 @@ def set_response_options(self): self.schema["responseOptions"] = self.response_options.options """ - input types with different response choices - """ def set_input_type_as_radio(self, response_options): @@ -82,18 +79,18 @@ def set_input_type_as_language(self): self.set_response_options() """ - input types with no response choice - """ def set_input_type_as_int(self): self.set_input_type("number") self.response_options.set_type("int") + self.response_options.options.pop("maxLength", None) def set_input_type_as_float(self): self.set_input_type("float") self.response_options.set_type("float") + self.response_options.options.pop("maxLength", None) def set_input_type_as_time_range(self): self.set_input_type("timeRange") @@ -104,9 +101,7 @@ def set_input_type_as_date(self): self.set_response_options({"valueType": "xsd:date"}) """ - input types with no response choice but with some parameters - """ def set_input_type_as_text(self, length=300): @@ -161,6 +156,14 @@ def set_basic_response_type(self, response_type): elif response_type == "language": self.set_input_type_as_language() + """ + writing and sorting of dictionaries + """ + + def write(self, output_dir): + self.sort() + self._SchemaBase__write(output_dir) + def sort(self): schema_order = [ "@context", diff --git a/reproschema/models/tests/data/items/float.jsonld b/reproschema/models/tests/data/items/float.jsonld new file mode 100644 index 0000000..62aa9c6 --- /dev/null +++ b/reproschema/models/tests/data/items/float.jsonld @@ -0,0 +1,18 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "float.jsonld", + "prefLabel": "float", + "description": "This is a float item.", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "This is an item where the user can input a float." + }, + "ui": { + "inputType": "float" + }, + "responseOptions": { + "valueType": "xsd:float" + } +} diff --git a/reproschema/models/tests/data/items/integer.jsonld b/reproschema/models/tests/data/items/integer.jsonld new file mode 100644 index 0000000..f4af728 --- /dev/null +++ b/reproschema/models/tests/data/items/integer.jsonld @@ -0,0 +1,18 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "integer.jsonld", + "prefLabel": "integer", + "description": "This is a integer item.", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "This is an item where the user can input a integer." + }, + "ui": { + "inputType": "number" + }, + "responseOptions": { + "valueType": "xsd:integer" + } +} diff --git a/reproschema/models/tests/data/items/text.jsonld b/reproschema/models/tests/data/items/text.jsonld index 460d8b4..854a8c1 100644 --- a/reproschema/models/tests/data/items/text.jsonld +++ b/reproschema/models/tests/data/items/text.jsonld @@ -9,7 +9,9 @@ "ui": { "inputType": "text" }, - "question": {}, + "question": { + "en": "question for text item" + }, "responseOptions": { "valueType": "xsd:string", "maxLength": 100 diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index f9dde97..9f8c674 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -23,11 +23,8 @@ def test_default(): - item = Item("1.0.0-rc4") - item.set_defaults("default") - item.set_filename("default") - - item.sort() + item = Item() + item.set_defaults() item.write(item_dir) item_content, expected = load_jsons(item) @@ -39,10 +36,40 @@ def test_text(): item = Item("1.0.0-rc4") item.set_defaults("text") - item.set_filename("text") item.set_input_type_as_text(100) - item.sort() + item.set_question("question for text item", "en") + + item.write(item_dir) + + item_content, expected = load_jsons(item) + + assert item_content == expected + + +def test_float(): + + item = Item("1.0.0-rc4") + item.set_defaults("float") + item.set_description("This is a float item.") + item.set_input_type_as_float() + item.set_question("This is an item where the user can input a float.", "en") + + item.write(item_dir) + + item_content, expected = load_jsons(item) + + assert item_content == expected + + +def test_integer(): + + item = Item() + item.set_defaults("integer") + item.set_description("This is a integer item.") + item.set_input_type_as_int() + item.set_question("This is an item where the user can input a integer.", "en") + item.write(item_dir) item_content, expected = load_jsons(item) @@ -54,7 +81,6 @@ def test_radio(): item = Item("1.0.0-rc4") item.set_defaults("radio") - item.set_filename("radio") item.set_question("question for radio item", "en") @@ -66,7 +92,6 @@ def test_radio(): item.set_input_type_as_radio(response_options) - item.sort() item.write(item_dir) item_content, expected = load_jsons(item) From c235e3a73283083005d32ea11c2fcfc0ed912f14 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 00:36:34 +0200 Subject: [PATCH 10/69] add tests for select and languages --- reproschema/models/item.py | 7 +- .../models/tests/data/items/language.jsonld | 20 ++++++ .../models/tests/data/items/select.jsonld | 41 ++++++++++++ reproschema/models/tests/test_item.py | 64 ++++++++++++++----- 4 files changed, 115 insertions(+), 17 deletions(-) create mode 100644 reproschema/models/tests/data/items/language.jsonld create mode 100644 reproschema/models/tests/data/items/select.jsonld diff --git a/reproschema/models/item.py b/reproschema/models/item.py index b657624..9cb2503 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -51,11 +51,13 @@ def set_response_options(self): def set_input_type_as_radio(self, response_options): self.set_input_type("radio") + response_options.set_type("int") self.response_options = response_options self.set_response_options() def set_input_type_as_select(self, response_options): self.set_input_type("select") + response_options.set_type("int") self.response_options = response_options self.set_response_options() @@ -66,15 +68,16 @@ def set_input_type_as_slider(self): def set_input_type_as_language(self): - URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" + URL = "https://raw.githubusercontent.com/ReproNim/reproschema-library/" self.set_input_type("selectLanguage") self.response_options.set_type("str") self.response_options.set_multiple_choice(True) - self.response_options["options"]["choices"] = ( + self.response_options.options["choices"] = ( URL + "master/resources/languages.json" ) + self.response_options.options.pop("maxLength", None) self.set_response_options() diff --git a/reproschema/models/tests/data/items/language.jsonld b/reproschema/models/tests/data/items/language.jsonld new file mode 100644 index 0000000..982a6ca --- /dev/null +++ b/reproschema/models/tests/data/items/language.jsonld @@ -0,0 +1,20 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "language.jsonld", + "prefLabel": "language", + "description": "language", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "This is an item where the user can select several language." + }, + "ui": { + "inputType": "selectLanguage" + }, + "responseOptions": { + "valueType": "xsd:string", + "multipleChoice": true, + "choices": "https://raw.githubusercontent.com/ReproNim/reproschema-library/master/resources/languages.json" + } +} diff --git a/reproschema/models/tests/data/items/select.jsonld b/reproschema/models/tests/data/items/select.jsonld new file mode 100644 index 0000000..72c3f02 --- /dev/null +++ b/reproschema/models/tests/data/items/select.jsonld @@ -0,0 +1,41 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "select.jsonld", + "prefLabel": "select", + "description": "select", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "question for select item" + }, + "ui": { + "inputType": "select" + }, + "responseOptions": { + "valueType": "xsd:integer", + "minValue": 0, + "maxValue": 2, + "multipleChoice": false, + "choices": [ + { + "name": { + "en": "Response option 1" + }, + "value": 0 + }, + { + "name": { + "en": "Response option 2" + }, + "value": 1 + }, + { + "name": { + "en": "Response option 3" + }, + "value": 2 + } + ] + } +} diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index 9f8c674..d12aec3 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -3,22 +3,20 @@ from ..item import Item, ResponseOption my_path = os.path.dirname(os.path.abspath(__file__)) -sys.path.insert(0, my_path + "/../") + +# sys.path.insert(0, my_path + "/../") item_dir = os.path.join(my_path, "items") if not os.path.exists(item_dir): os.makedirs(os.path.join(item_dir)) -# integer -# float -# select -# multiradio -# multiselect -# char -# time range -# date -# slider -# language +# TODO: add test for +# select +# multiradio +# multiselect +# slider +# time range +# date def test_default(): @@ -38,7 +36,7 @@ def test_text(): item.set_defaults("text") item.set_input_type_as_text(100) - item.set_question("question for text item", "en") + item.set_question("question for text item") item.write(item_dir) @@ -53,7 +51,7 @@ def test_float(): item.set_defaults("float") item.set_description("This is a float item.") item.set_input_type_as_float() - item.set_question("This is an item where the user can input a float.", "en") + item.set_question("This is an item where the user can input a float.") item.write(item_dir) @@ -68,7 +66,21 @@ def test_integer(): item.set_defaults("integer") item.set_description("This is a integer item.") item.set_input_type_as_int() - item.set_question("This is an item where the user can input a integer.", "en") + item.set_question("This is an item where the user can input a integer.") + + item.write(item_dir) + + item_content, expected = load_jsons(item) + + assert item_content == expected + + +def test_language(): + + item = Item() + item.set_defaults("language") + item.set_input_type_as_language() + item.set_question("This is an item where the user can select several language.") item.write(item_dir) @@ -85,7 +97,6 @@ def test_radio(): item.set_question("question for radio item", "en") response_options = ResponseOption() - response_options.set_type("int") response_options.add_choice({"name": {"en": "Not at all"}, "value": 0}) response_options.add_choice({"name": {"en": "Several days"}, "value": 1}) response_options.set_max(1) @@ -99,6 +110,29 @@ def test_radio(): assert item_content == expected +def test_select(): + + item = Item() + item.set_defaults("select") + + item.set_question("question for select item", "en") + + response_options = ResponseOption() + response_options.set_type("int") + response_options.add_choice({"name": {"en": "Response option 1"}, "value": 0}) + response_options.add_choice({"name": {"en": "Response option 2"}, "value": 1}) + response_options.add_choice({"name": {"en": "Response option 3"}, "value": 2}) + response_options.set_max(2) + + item.set_input_type_as_select(response_options) + + item.write(item_dir) + + item_content, expected = load_jsons(item) + + assert item_content == expected + + def load_jsons(item): output_file = os.path.join(item_dir, item.get_filename()) item_content = read_json(output_file) From 1d0c64b58e309177ff79cbf99e49dfd8b51ea411 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 08:19:17 +0200 Subject: [PATCH 11/69] add tests for multiresponse items --- reproschema/models/item.py | 1 + .../tests/data/items/radio_multiple.jsonld | 35 ++++++++++++++++ .../tests/data/items/select_multiple.jsonld | 41 +++++++++++++++++++ reproschema/models/tests/test_item.py | 38 +++++++++-------- 4 files changed, 99 insertions(+), 16 deletions(-) create mode 100644 reproschema/models/tests/data/items/radio_multiple.jsonld create mode 100644 reproschema/models/tests/data/items/select_multiple.jsonld diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 9cb2503..42af637 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -33,6 +33,7 @@ def set_defaults(self, name="default"): self.set_input_type_as_text() def set_filename(self, name, ext=".jsonld"): + name = name.replace(" ", "_") self.schema_file = name + ext self.schema["@id"] = name + ext diff --git a/reproschema/models/tests/data/items/radio_multiple.jsonld b/reproschema/models/tests/data/items/radio_multiple.jsonld new file mode 100644 index 0000000..cbc24c8 --- /dev/null +++ b/reproschema/models/tests/data/items/radio_multiple.jsonld @@ -0,0 +1,35 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "radio_multiple.jsonld", + "prefLabel": "radio multiple", + "description": "radio multiple", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "question for radio item with multiple responses" + }, + "ui": { + "inputType": "radio" + }, + "responseOptions": { + "valueType": "xsd:integer", + "minValue": 0, + "maxValue": 1, + "multipleChoice": true, + "choices": [ + { + "name": { + "en": "Not at all" + }, + "value": 0 + }, + { + "name": { + "en": "Several days" + }, + "value": 1 + } + ] + } +} diff --git a/reproschema/models/tests/data/items/select_multiple.jsonld b/reproschema/models/tests/data/items/select_multiple.jsonld new file mode 100644 index 0000000..8a7f800 --- /dev/null +++ b/reproschema/models/tests/data/items/select_multiple.jsonld @@ -0,0 +1,41 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "select_multiple.jsonld", + "prefLabel": "select multiple", + "description": "select multiple", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "question for select item with multiple responses" + }, + "ui": { + "inputType": "select" + }, + "responseOptions": { + "valueType": "xsd:integer", + "minValue": 0, + "maxValue": 2, + "multipleChoice": true, + "choices": [ + { + "name": { + "en": "Response option 1" + }, + "value": 0 + }, + { + "name": { + "en": "Response option 2" + }, + "value": 1 + }, + { + "name": { + "en": "Response option 3" + }, + "value": 2 + } + ] + } +} diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index d12aec3..a94f761 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -11,9 +11,6 @@ os.makedirs(os.path.join(item_dir)) # TODO: add test for -# select -# multiradio -# multiselect # slider # time range # date @@ -23,10 +20,9 @@ def test_default(): item = Item() item.set_defaults() - item.write(item_dir) + item.write(item_dir) item_content, expected = load_jsons(item) - assert item_content == expected @@ -39,9 +35,7 @@ def test_text(): item.set_question("question for text item") item.write(item_dir) - item_content, expected = load_jsons(item) - assert item_content == expected @@ -54,9 +48,7 @@ def test_float(): item.set_question("This is an item where the user can input a float.") item.write(item_dir) - item_content, expected = load_jsons(item) - assert item_content == expected @@ -69,9 +61,7 @@ def test_integer(): item.set_question("This is an item where the user can input a integer.") item.write(item_dir) - item_content, expected = load_jsons(item) - assert item_content == expected @@ -83,9 +73,7 @@ def test_language(): item.set_question("This is an item where the user can select several language.") item.write(item_dir) - item_content, expected = load_jsons(item) - assert item_content == expected @@ -104,9 +92,18 @@ def test_radio(): item.set_input_type_as_radio(response_options) item.write(item_dir) - item_content, expected = load_jsons(item) + assert item_content == expected + item.set_filename("radio multiple") + item.set_description("radio multiple") + item.set_pref_label("radio multiple") + item.set_question("question for radio item with multiple responses") + response_options.set_multiple_choice(True) + item.set_input_type_as_radio(response_options) + item.write(item_dir) + + item_content, expected = load_jsons(item) assert item_content == expected @@ -114,7 +111,6 @@ def test_select(): item = Item() item.set_defaults("select") - item.set_question("question for select item", "en") response_options = ResponseOption() @@ -127,13 +123,23 @@ def test_select(): item.set_input_type_as_select(response_options) item.write(item_dir) - item_content, expected = load_jsons(item) + assert item_content == expected + + item.set_filename("select multiple") + item.set_description("select multiple") + item.set_pref_label("select multiple") + item.set_question("question for select item with multiple responses") + response_options.set_multiple_choice(True) + item.set_input_type_as_select(response_options) + item.write(item_dir) + item_content, expected = load_jsons(item) assert item_content == expected def load_jsons(item): + output_file = os.path.join(item_dir, item.get_filename()) item_content = read_json(output_file) From 43395e738de2703a351514eca4141cd4e547dc94 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 08:41:53 +0200 Subject: [PATCH 12/69] add test for slider and multitext --- reproschema/models/item.py | 19 +++---- .../models/tests/data/items/multitext.jsonld | 19 +++++++ .../models/tests/data/items/slider.jsonld | 53 +++++++++++++++++++ reproschema/models/tests/test_item.py | 34 ++++++++++++ 4 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 reproschema/models/tests/data/items/multitext.jsonld create mode 100644 reproschema/models/tests/data/items/slider.jsonld diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 42af637..bf1886e 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -62,10 +62,11 @@ def set_input_type_as_select(self, response_options): self.response_options = response_options self.set_response_options() - def set_input_type_as_slider(self): - self.set_input_type_as_text() # until the slide item of the ui is fixed - # self.set_input_type("slider") - # self.set_response_options({"valueType": "xsd:string"}) + def set_input_type_as_slider(self, response_options): + response_options.set_type("int") + self.set_input_type("slider") + self.response_options = response_options + self.set_response_options() def set_input_type_as_language(self): @@ -112,6 +113,9 @@ def set_input_type_as_text(self, length=300): self.set_input_type("text") self.response_options.set_type("str") self.response_options.set_length(length) + # TODO removing all these key pairs is horrible + # either change the way the default is initialised or create another + # method that handles the cleaning of unecessary keys self.response_options.options.pop("maxValue", None) self.response_options.options.pop("minValue", None) self.response_options.options.pop("multipleChoice", None) @@ -142,8 +146,8 @@ def set_input_type_as_multitext(self, length=300): def set_basic_response_type(self, response_type): - # default (also valid for "char" input type) - self.set_input_type_as_char() + # default (also valid for "text" input type) + self.set_input_type_as_text() if response_type == "int": self.set_input_type_as_int() @@ -198,9 +202,6 @@ def __init__(self): "multipleChoice": False, } - # "readonlyValue": False, - # "maxLength": 0, - def set_type(self, type): if type == "int": self.options["valueType"] = "xsd:integer" diff --git a/reproschema/models/tests/data/items/multitext.jsonld b/reproschema/models/tests/data/items/multitext.jsonld new file mode 100644 index 0000000..bf5abbf --- /dev/null +++ b/reproschema/models/tests/data/items/multitext.jsonld @@ -0,0 +1,19 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "multitext.jsonld", + "prefLabel": "multitext", + "description": "multitext", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "This is an item where the user can input several text field." + }, + "ui": { + "inputType": "multitext" + }, + "responseOptions": { + "valueType": "xsd:string", + "maxLength": 50 + } +} diff --git a/reproschema/models/tests/data/items/slider.jsonld b/reproschema/models/tests/data/items/slider.jsonld new file mode 100644 index 0000000..73774ec --- /dev/null +++ b/reproschema/models/tests/data/items/slider.jsonld @@ -0,0 +1,53 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "slider.jsonld", + "prefLabel": "slider", + "description": "slider", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "question for slider item" + }, + "ui": { + "inputType": "slider" + }, + "responseOptions": { + "valueType": "xsd:integer", + "minValue": 0, + "maxValue": 4, + "multipleChoice": false, + "choices": [ + { + "name": { + "en": "not at all" + }, + "value": 0 + }, + { + "name": { + "en": "a bit" + }, + "value": 1 + }, + { + "name": { + "en": "so so" + }, + "value": 2 + }, + { + "name": { + "en": "a lot" + }, + "value": 3 + }, + { + "name": { + "en": "very much" + }, + "value": 4 + } + ] + } +} diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index a94f761..b982068 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -39,6 +39,19 @@ def test_text(): assert item_content == expected +def test_multitext(): + + item = Item("1.0.0-rc4") + item.set_defaults("multitext") + item.set_input_type_as_multitext(50) + + item.set_question("This is an item where the user can input several text field.") + + item.write(item_dir) + item_content, expected = load_jsons(item) + assert item_content == expected + + def test_float(): item = Item("1.0.0-rc4") @@ -138,6 +151,27 @@ def test_select(): assert item_content == expected +def test_slider(): + + item = Item() + item.set_defaults("slider") + item.set_question("question for slider item", "en") + + response_options = ResponseOption() + response_options.add_choice({"name": {"en": "not at all"}, "value": 0}) + response_options.add_choice({"name": {"en": "a bit"}, "value": 1}) + response_options.add_choice({"name": {"en": "so so"}, "value": 2}) + response_options.add_choice({"name": {"en": "a lot"}, "value": 3}) + response_options.add_choice({"name": {"en": "very much"}, "value": 4}) + response_options.set_max(4) + + item.set_input_type_as_slider(response_options) + + item.write(item_dir) + item_content, expected = load_jsons(item) + assert item_content == expected + + def load_jsons(item): output_file = os.path.join(item_dir, item.get_filename()) From 19768c2cabc7c6da05918a89a45ed135a6f20579 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 08:48:34 +0200 Subject: [PATCH 13/69] simplify add_choice API --- reproschema/models/item.py | 4 ++-- reproschema/models/tests/test_item.py | 22 +++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index bf1886e..56a8144 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -230,5 +230,5 @@ def set_length(self, value): def set_multiple_choice(self, value): self.options["multipleChoice"] = value - def add_choice(self, choice): - self.options["choices"].append(choice) + def add_choice(self, choice, value, lang=DEFAULT_LANG): + self.options["choices"].append({"name": {lang: choice}, "value": value}) diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index b982068..1221fca 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -98,8 +98,8 @@ def test_radio(): item.set_question("question for radio item", "en") response_options = ResponseOption() - response_options.add_choice({"name": {"en": "Not at all"}, "value": 0}) - response_options.add_choice({"name": {"en": "Several days"}, "value": 1}) + response_options.add_choice("Not at all", 0, "en") + response_options.add_choice("Several days", 1, "en") response_options.set_max(1) item.set_input_type_as_radio(response_options) @@ -124,13 +124,13 @@ def test_select(): item = Item() item.set_defaults("select") - item.set_question("question for select item", "en") + item.set_question("question for select item") response_options = ResponseOption() response_options.set_type("int") - response_options.add_choice({"name": {"en": "Response option 1"}, "value": 0}) - response_options.add_choice({"name": {"en": "Response option 2"}, "value": 1}) - response_options.add_choice({"name": {"en": "Response option 3"}, "value": 2}) + response_options.add_choice("Response option 1", 0) + response_options.add_choice("Response option 2", 1) + response_options.add_choice("Response option 3", 2) response_options.set_max(2) item.set_input_type_as_select(response_options) @@ -158,11 +158,11 @@ def test_slider(): item.set_question("question for slider item", "en") response_options = ResponseOption() - response_options.add_choice({"name": {"en": "not at all"}, "value": 0}) - response_options.add_choice({"name": {"en": "a bit"}, "value": 1}) - response_options.add_choice({"name": {"en": "so so"}, "value": 2}) - response_options.add_choice({"name": {"en": "a lot"}, "value": 3}) - response_options.add_choice({"name": {"en": "very much"}, "value": 4}) + response_options.add_choice("not at all", 0) + response_options.add_choice("a bit", 1) + response_options.add_choice("so so", 2) + response_options.add_choice("a lot", 3) + response_options.add_choice("very much", 4) response_options.set_max(4) item.set_input_type_as_slider(response_options) From f5eb4c148738d7bf7c6af9442149acd4442abb7f Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 09:06:45 +0200 Subject: [PATCH 14/69] create set_context method --- reproschema/models/base.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index ac9c7ac..d08f46a 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -15,12 +15,16 @@ def __init__(self, version): VERSION = version or "1.0.0-rc4" self.schema = { - "@context": URL + VERSION + "/contexts/generic", "@type": self.schema_type, "schemaVersion": VERSION, "version": "0.0.1", } + self.set_context(URL + VERSION + "/contexts/generic") + + def set_context(self, context): + self.schema["@context"] = context + def set_filename(self, name, ext=".jsonld"): self.schema_file = name + "_schema" + ext self.schema["@id"] = name + "_schema" + ext From df561741385bdbae3bfbd51529fdc46a9e7ae182 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 10:00:29 +0200 Subject: [PATCH 15/69] refactor sorting --- reproschema/models/base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index d08f46a..29a94a3 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -1,5 +1,6 @@ import json import os +from collections import OrderedDict class SchemaBase: @@ -52,12 +53,12 @@ def __set_defaults(self, name): def sort_schema(self, schema_order): - reordered_dict = {k: self.schema[k] for k in schema_order} + reordered_dict = reorder_dict_skip_missing(self.schema, schema_order) self.schema = reordered_dict def sort_ui(self, ui_order): - reordered_dict = {k: self.schema["ui"][k] for k in ui_order} + reordered_dict = reorder_dict_skip_missing(self.schema["ui"], ui_order) self.schema["ui"] = reordered_dict def __write(self, output_dir): @@ -81,3 +82,7 @@ def from_file(cls, filepath): if "@type" not in data: raise ValueError("Missing @type key") return cls.from_data(data) + + +def reorder_dict_skip_missing(old_dict, key_list): + return OrderedDict((k, old_dict[k]) for k in key_list if k in old_dict) From 62226fd9e5b2d7a9d24a909d08d7dbee3c9c817e Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 10:00:59 +0200 Subject: [PATCH 16/69] add read only item --- reproschema/models/item.py | 46 ++++++++++++++++----------- reproschema/models/tests/test_item.py | 29 +++++++++++++++++ 2 files changed, 57 insertions(+), 18 deletions(-) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 56a8144..1a8c601 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -40,9 +40,20 @@ def set_filename(self, name, ext=".jsonld"): def set_question(self, question, lang=DEFAULT_LANG): self.schema["question"][lang] = question + """ + UI + """ + def set_input_type(self, input_type): self.schema["ui"]["inputType"] = input_type + def set_read_only_value(self, value): + self.schema["ui"]["readonlyValue"] = value + + """ + RESPONSE CHOICES + """ + def set_response_options(self): self.schema["responseOptions"] = self.response_options.options @@ -54,19 +65,16 @@ def set_input_type_as_radio(self, response_options): self.set_input_type("radio") response_options.set_type("int") self.response_options = response_options - self.set_response_options() def set_input_type_as_select(self, response_options): self.set_input_type("select") response_options.set_type("int") self.response_options = response_options - self.set_response_options() def set_input_type_as_slider(self, response_options): - response_options.set_type("int") self.set_input_type("slider") + response_options.set_type("int") self.response_options = response_options - self.set_response_options() def set_input_type_as_language(self): @@ -79,9 +87,7 @@ def set_input_type_as_language(self): self.response_options.options["choices"] = ( URL + "master/resources/languages.json" ) - self.response_options.options.pop("maxLength", None) - - self.set_response_options() + self.response_options.unset(["maxLength"]) """ input types with no response choice @@ -90,12 +96,13 @@ def set_input_type_as_language(self): def set_input_type_as_int(self): self.set_input_type("number") self.response_options.set_type("int") - self.response_options.options.pop("maxLength", None) + self.response_options.unset(["maxLength"]) def set_input_type_as_float(self): self.set_input_type("float") self.response_options.set_type("float") self.response_options.options.pop("maxLength", None) + self.response_options.unset(["maxLength"]) def set_input_type_as_time_range(self): self.set_input_type("timeRange") @@ -113,20 +120,14 @@ def set_input_type_as_text(self, length=300): self.set_input_type("text") self.response_options.set_type("str") self.response_options.set_length(length) - # TODO removing all these key pairs is horrible - # either change the way the default is initialised or create another - # method that handles the cleaning of unecessary keys - self.response_options.options.pop("maxValue", None) - self.response_options.options.pop("minValue", None) - self.response_options.options.pop("multipleChoice", None) - self.response_options.options.pop("choices", None) - self.set_response_options() + self.response_options.unset( + ["maxValue", "minValue", "multipleChoice", "choices"] + ) def set_input_type_as_multitext(self, length=300): self.set_input_type("multitext") self.response_options.set_type("str") self.response_options.set_length(length) - self.set_response_options() # TODO # email: EmailInput/EmailInput.vue @@ -165,11 +166,16 @@ def set_basic_response_type(self, response_type): self.set_input_type_as_language() """ - writing and sorting of dictionaries + writing, reading, sorting, unsetting """ + def unset(self, keys): + for i in keys: + self.schema.pop(i, None) + def write(self, output_dir): self.sort() + self.set_response_options() self._SchemaBase__write(output_dir) def sort(self): @@ -202,6 +208,10 @@ def __init__(self): "multipleChoice": False, } + def unset(self, keys): + for i in keys: + self.options.pop(i, None) + def set_type(self, type): if type == "int": self.options["valueType"] = "xsd:integer" diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index 1221fca..f6cc2de 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -10,6 +10,8 @@ if not os.path.exists(item_dir): os.makedirs(os.path.join(item_dir)) +reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") + # TODO: add test for # slider # time range @@ -172,6 +174,33 @@ def test_slider(): assert item_content == expected +def test_read_only(): + + item = Item() + item.set_defaults("activity1_total_score") + item.set_context("../../../contexts/generic") + item.set_filename("activity1_total_score", "") + item.set_pref_label("activity1_total_score") + item.set_description("Score item for Activity 1") + item.set_read_only_value(True) + item.set_input_type_as_int() + item.response_options.set_max(3) + item.response_options.set_min(0) + item.unset(["question"]) + + item.write(item_dir) + + output_file = os.path.join(item_dir, item.get_filename()) + item_content = read_json(output_file) + + # test against one of the pre existing files + data_file = os.path.join( + reproschema_test_data, "activities", "items", "activity1_total_score" + ) + expected = read_json(data_file) + assert item_content == expected + + def load_jsons(item): output_file = os.path.join(item_dir, item.get_filename()) From fe7dc95baf40e3b64a52702bf9f05f3d50cd288e Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 10:39:56 +0200 Subject: [PATCH 17/69] add test for email, id, date, year and time range --- reproschema/models/item.py | 24 +++++-- .../models/tests/data/items/date.jsonld | 18 +++++ .../models/tests/data/items/email.jsonld | 18 +++++ .../tests/data/items/participant_id.jsonld | 16 +++++ .../models/tests/data/items/time_range.jsonld | 18 +++++ .../models/tests/data/items/year.jsonld | 18 +++++ reproschema/models/tests/test_item.py | 66 ++++++++++++++++++- 7 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 reproschema/models/tests/data/items/date.jsonld create mode 100644 reproschema/models/tests/data/items/email.jsonld create mode 100644 reproschema/models/tests/data/items/participant_id.jsonld create mode 100644 reproschema/models/tests/data/items/time_range.jsonld create mode 100644 reproschema/models/tests/data/items/year.jsonld diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 1a8c601..df0fe02 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -25,7 +25,6 @@ def set_URI(self, URI): # TODO # image - # readonlyValue def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) @@ -106,14 +105,21 @@ def set_input_type_as_float(self): def set_input_type_as_time_range(self): self.set_input_type("timeRange") - self.set_response_options({"valueType": "datetime"}) + self.response_options.options.pop("maxLength", None) + self.response_options.set_type("datetime") def set_input_type_as_date(self): self.set_input_type("date") - self.set_response_options({"valueType": "xsd:date"}) + self.response_options.options.pop("maxLength", None) + self.response_options.set_type("date") + + def set_input_type_as_year(self): + self.set_input_type("year") + self.response_options.options.pop("maxLength", None) + self.response_options.set_type("date") """ - input types with no response choice but with some parameters + input types requiring user typed input """ def set_input_type_as_text(self, length=300): @@ -129,6 +135,14 @@ def set_input_type_as_multitext(self, length=300): self.response_options.set_type("str") self.response_options.set_length(length) + def set_input_type_as_email(self): + self.set_input_type("email") + self.response_options.options.pop("maxLength", None) + + def set_input_type_as_id(self): + self.set_input_type("pid") + self.response_options.options.pop("maxLength", None) + # TODO # email: EmailInput/EmailInput.vue # audioCheck: AudioCheck/AudioCheck.vue @@ -222,7 +236,7 @@ def set_type(self, type): elif type == "date": self.options["valueType"] = "xsd:date" elif type == "datetime": - self.options["valueType"] = "datetime" + self.options["valueType"] = "xsd:datetime" def set_input_type_as_date(self): self.set_input_type("date") diff --git a/reproschema/models/tests/data/items/date.jsonld b/reproschema/models/tests/data/items/date.jsonld new file mode 100644 index 0000000..335024f --- /dev/null +++ b/reproschema/models/tests/data/items/date.jsonld @@ -0,0 +1,18 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "date.jsonld", + "prefLabel": "date", + "description": "date", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "input a date" + }, + "ui": { + "inputType": "date" + }, + "responseOptions": { + "valueType": "xsd:date" + } +} diff --git a/reproschema/models/tests/data/items/email.jsonld b/reproschema/models/tests/data/items/email.jsonld new file mode 100644 index 0000000..37dcb0d --- /dev/null +++ b/reproschema/models/tests/data/items/email.jsonld @@ -0,0 +1,18 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "email.jsonld", + "prefLabel": "email", + "description": "email", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "input email address" + }, + "ui": { + "inputType": "email" + }, + "responseOptions": { + "valueType": "xsd:string" + } +} diff --git a/reproschema/models/tests/data/items/participant_id.jsonld b/reproschema/models/tests/data/items/participant_id.jsonld new file mode 100644 index 0000000..0ab99bc --- /dev/null +++ b/reproschema/models/tests/data/items/participant_id.jsonld @@ -0,0 +1,16 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "participant_id.jsonld", + "prefLabel": "participant id", + "description": "participant id", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": {"en": "input the participant id number"}, + "ui": { + "inputType": "pid" + }, + "responseOptions": { + "valueType": "xsd:string" + } +} diff --git a/reproschema/models/tests/data/items/time_range.jsonld b/reproschema/models/tests/data/items/time_range.jsonld new file mode 100644 index 0000000..9a84deb --- /dev/null +++ b/reproschema/models/tests/data/items/time_range.jsonld @@ -0,0 +1,18 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "time_range.jsonld", + "prefLabel": "time range", + "description": "time range", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "input a time range" + }, + "ui": { + "inputType": "timeRange" + }, + "responseOptions": { + "valueType": "xsd:datetime" + } +} diff --git a/reproschema/models/tests/data/items/year.jsonld b/reproschema/models/tests/data/items/year.jsonld new file mode 100644 index 0000000..62dd48e --- /dev/null +++ b/reproschema/models/tests/data/items/year.jsonld @@ -0,0 +1,18 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "year.jsonld", + "prefLabel": "year", + "description": "year", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "input a year" + }, + "ui": { + "inputType": "year" + }, + "responseOptions": { + "valueType": "xsd:date" + } +} diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index f6cc2de..f21086e 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -54,6 +54,71 @@ def test_multitext(): assert item_content == expected +def test_email(): + + item = Item("1.0.0-rc4") + item.set_defaults("email") + item.set_input_type_as_email() + + item.set_question("input email address") + + item.write(item_dir) + item_content, expected = load_jsons(item) + assert item_content == expected + + +def test_participant_id(): + + item = Item("1.0.0-rc4") + item.set_defaults("participant id") + item.set_input_type_as_id() + + item.set_question("input the participant id number") + + item.write(item_dir) + item_content, expected = load_jsons(item) + assert item_content == expected + + +def test_date(): + + item = Item("1.0.0-rc4") + item.set_defaults("date") + item.set_input_type_as_date() + + item.set_question("input a date") + + item.write(item_dir) + item_content, expected = load_jsons(item) + assert item_content == expected + + +def test_time_range(): + + item = Item("1.0.0-rc4") + item.set_defaults("time range") + item.set_input_type_as_time_range() + + item.set_question("input a time range") + + item.write(item_dir) + item_content, expected = load_jsons(item) + assert item_content == expected + + +def test_year(): + + item = Item("1.0.0-rc4") + item.set_defaults("year") + item.set_input_type_as_year() + + item.set_question("input a year") + + item.write(item_dir) + item_content, expected = load_jsons(item) + assert item_content == expected + + def test_float(): item = Item("1.0.0-rc4") @@ -129,7 +194,6 @@ def test_select(): item.set_question("question for select item") response_options = ResponseOption() - response_options.set_type("int") response_options.add_choice("Response option 1", 0) response_options.add_choice("Response option 2", 1) response_options.add_choice("Response option 3", 2) From 4118fbe4ef7a28c6f493682b213bb1af9d80f0d1 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 10:48:28 +0200 Subject: [PATCH 18/69] refactor --- reproschema/models/item.py | 40 ++++++++++++-------------------------- 1 file changed, 12 insertions(+), 28 deletions(-) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index df0fe02..2f5f1ff 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -62,17 +62,17 @@ def set_response_options(self): def set_input_type_as_radio(self, response_options): self.set_input_type("radio") - response_options.set_type("int") + response_options.set_type("integer") self.response_options = response_options def set_input_type_as_select(self, response_options): self.set_input_type("select") - response_options.set_type("int") + response_options.set_type("integer") self.response_options = response_options def set_input_type_as_slider(self, response_options): self.set_input_type("slider") - response_options.set_type("int") + response_options.set_type("integer") self.response_options = response_options def set_input_type_as_language(self): @@ -81,7 +81,7 @@ def set_input_type_as_language(self): self.set_input_type("selectLanguage") - self.response_options.set_type("str") + self.response_options.set_type("string") self.response_options.set_multiple_choice(True) self.response_options.options["choices"] = ( URL + "master/resources/languages.json" @@ -94,23 +94,22 @@ def set_input_type_as_language(self): def set_input_type_as_int(self): self.set_input_type("number") - self.response_options.set_type("int") + self.response_options.set_type("integer") self.response_options.unset(["maxLength"]) def set_input_type_as_float(self): self.set_input_type("float") self.response_options.set_type("float") - self.response_options.options.pop("maxLength", None) self.response_options.unset(["maxLength"]) def set_input_type_as_time_range(self): self.set_input_type("timeRange") - self.response_options.options.pop("maxLength", None) + self.response_options.unset(["maxLength"]) self.response_options.set_type("datetime") def set_input_type_as_date(self): self.set_input_type("date") - self.response_options.options.pop("maxLength", None) + self.response_options.unset(["maxLength"]) self.response_options.set_type("date") def set_input_type_as_year(self): @@ -124,7 +123,7 @@ def set_input_type_as_year(self): def set_input_type_as_text(self, length=300): self.set_input_type("text") - self.response_options.set_type("str") + self.response_options.set_type("string") self.response_options.set_length(length) self.response_options.unset( ["maxValue", "minValue", "multipleChoice", "choices"] @@ -132,26 +131,24 @@ def set_input_type_as_text(self, length=300): def set_input_type_as_multitext(self, length=300): self.set_input_type("multitext") - self.response_options.set_type("str") + self.response_options.set_type("string") self.response_options.set_length(length) def set_input_type_as_email(self): self.set_input_type("email") - self.response_options.options.pop("maxLength", None) + self.response_options.unset(["maxLength"]) def set_input_type_as_id(self): self.set_input_type("pid") - self.response_options.options.pop("maxLength", None) + self.response_options.unset(["maxLength"]) # TODO - # email: EmailInput/EmailInput.vue # audioCheck: AudioCheck/AudioCheck.vue # audioRecord: WebAudioRecord/Audio.vue # audioPassageRecord: WebAudioRecord/Audio.vue # audioImageRecord: WebAudioRecord/Audio.vue # audioRecordNumberTask: WebAudioRecord/Audio.vue # audioAutoRecord: AudioCheckRecord/AudioCheckRecord.vue - # year: YearInput/YearInput.vue # selectCountry: SelectInput/SelectInput.vue # selectState: SelectInput/SelectInput.vue # documentUpload: DocumentUpload/DocumentUpload.vue @@ -227,20 +224,7 @@ def unset(self, keys): self.options.pop(i, None) def set_type(self, type): - if type == "int": - self.options["valueType"] = "xsd:integer" - elif type == "str": - self.options["valueType"] = "xsd:string" - elif type == "float": - self.options["valueType"] = "xsd:float" - elif type == "date": - self.options["valueType"] = "xsd:date" - elif type == "datetime": - self.options["valueType"] = "xsd:datetime" - - def set_input_type_as_date(self): - self.set_input_type("date") - self.set_response_options({"valueType": "xsd:date"}) + self.options["valueType"] = "xsd:" + type def set_min(self, value): self.options["minValue"] = value From 1b656a55f36ebb9e0b14746032435d2703e5c741 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 11:00:00 +0200 Subject: [PATCH 19/69] add test for state and country --- reproschema/models/item.py | 26 +++++++++++++++-- .../models/tests/data/items/country.jsonld | 20 +++++++++++++ .../models/tests/data/items/state.jsonld | 19 ++++++++++++ reproschema/models/tests/test_item.py | 29 +++++++++++++++---- 4 files changed, 87 insertions(+), 7 deletions(-) create mode 100644 reproschema/models/tests/data/items/country.jsonld create mode 100644 reproschema/models/tests/data/items/state.jsonld diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 2f5f1ff..5ee6299 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -75,6 +75,10 @@ def set_input_type_as_slider(self, response_options): response_options.set_type("integer") self.response_options = response_options + """ + input types with preset response choices + """ + def set_input_type_as_language(self): URL = "https://raw.githubusercontent.com/ReproNim/reproschema-library/" @@ -88,6 +92,26 @@ def set_input_type_as_language(self): ) self.response_options.unset(["maxLength"]) + def set_input_type_as_country(self): + + URL = "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json" + + self.set_input_type("selectCountry") + + self.response_options.set_type("string") + self.response_options.options["choices"] = URL + self.response_options.set_length(50) + + def set_input_type_as_state(self): + + URL = "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json" + + self.set_input_type("selectState") + + self.response_options.set_type("string") + self.response_options.options["choices"] = URL + self.response_options.unset(["maxLength"]) + """ input types with no response choice """ @@ -149,8 +173,6 @@ def set_input_type_as_id(self): # audioImageRecord: WebAudioRecord/Audio.vue # audioRecordNumberTask: WebAudioRecord/Audio.vue # audioAutoRecord: AudioCheckRecord/AudioCheckRecord.vue - # selectCountry: SelectInput/SelectInput.vue - # selectState: SelectInput/SelectInput.vue # documentUpload: DocumentUpload/DocumentUpload.vue # save: SaveData/SaveData.vue # static: Static/Static.vue diff --git a/reproschema/models/tests/data/items/country.jsonld b/reproschema/models/tests/data/items/country.jsonld new file mode 100644 index 0000000..45ddecb --- /dev/null +++ b/reproschema/models/tests/data/items/country.jsonld @@ -0,0 +1,20 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "country.jsonld", + "prefLabel": "country", + "description": "country", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "select a country" + }, + "ui": { + "inputType": "selectCountry" + }, + "responseOptions": { + "valueType": "xsd:string", + "maxLength": 50, + "choices": "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json" + } +} diff --git a/reproschema/models/tests/data/items/state.jsonld b/reproschema/models/tests/data/items/state.jsonld new file mode 100644 index 0000000..ab4a3e1 --- /dev/null +++ b/reproschema/models/tests/data/items/state.jsonld @@ -0,0 +1,19 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "state.jsonld", + "prefLabel": "state", + "description": "state", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "select a USA state" + }, + "ui": { + "inputType": "selectState" + }, + "responseOptions": { + "valueType": "xsd:string", + "choices": "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json" + } +} diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index f21086e..1b00cd3 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -12,11 +12,6 @@ reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") -# TODO: add test for -# slider -# time range -# date - def test_default(): @@ -157,6 +152,30 @@ def test_language(): assert item_content == expected +def test_country(): + + item = Item() + item.set_defaults("country") + item.set_input_type_as_country() + item.set_question("select a country") + + item.write(item_dir) + item_content, expected = load_jsons(item) + assert item_content == expected + + +def test_state(): + + item = Item() + item.set_defaults("state") + item.set_input_type_as_state() + item.set_question("select a USA state") + + item.write(item_dir) + item_content, expected = load_jsons(item) + assert item_content == expected + + def test_radio(): item = Item("1.0.0-rc4") From 13c0de483d840c4bea8ec6a5eac031b17b9b0707 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 11:03:12 +0200 Subject: [PATCH 20/69] refactor using preset response choices --- reproschema/models/item.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 5ee6299..6f69256 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -87,9 +87,7 @@ def set_input_type_as_language(self): self.response_options.set_type("string") self.response_options.set_multiple_choice(True) - self.response_options.options["choices"] = ( - URL + "master/resources/languages.json" - ) + self.response_options.use_preset(URL + "master/resources/languages.json") self.response_options.unset(["maxLength"]) def set_input_type_as_country(self): @@ -99,7 +97,7 @@ def set_input_type_as_country(self): self.set_input_type("selectCountry") self.response_options.set_type("string") - self.response_options.options["choices"] = URL + self.response_options.use_preset(URL) self.response_options.set_length(50) def set_input_type_as_state(self): @@ -109,7 +107,7 @@ def set_input_type_as_state(self): self.set_input_type("selectState") self.response_options.set_type("string") - self.response_options.options["choices"] = URL + self.response_options.use_preset(URL) self.response_options.unset(["maxLength"]) """ @@ -260,5 +258,8 @@ def set_length(self, value): def set_multiple_choice(self, value): self.options["multipleChoice"] = value + def use_preset(self, URI): + self.options["choices"] = URI + def add_choice(self, choice, value, lang=DEFAULT_LANG): self.options["choices"].append({"name": {lang: choice}, "value": value}) From d72da8ac4ead54c52b74059046ee3f073ddfb1dd Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 13:03:46 +0200 Subject: [PATCH 21/69] add tests for activity creation --- reproschema/models/activity.py | 77 ++++++++++++------ reproschema/models/base.py | 11 +++ reproschema/models/item.py | 6 +- reproschema/models/tests/.gitignore | 1 + .../data/activities/activity1_schema.jsonld | 58 +++++++++++++ .../data/activities/default_schema.jsonld | 21 +++++ reproschema/models/tests/test_activity.py | 81 +++++++++++++++++++ 7 files changed, 227 insertions(+), 28 deletions(-) create mode 100644 reproschema/models/tests/data/activities/activity1_schema.jsonld create mode 100644 reproschema/models/tests/data/activities/default_schema.jsonld create mode 100644 reproschema/models/tests/test_activity.py diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index ca2350a..8ff4337 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -1,5 +1,7 @@ from .base import SchemaBase +DEFAULT_LANG = "en" + class Activity(SchemaBase): """ @@ -10,10 +12,6 @@ class to deal with reproschema activities def __init__(self, version=None): super().__init__(version) - self.schema["ui"] = {"shuffle": [], "order": [], "addProperties": []} - - def set_ui_shuffle(self, shuffle=False): - self.schema["ui"]["shuffle"] = shuffle def set_URI(self, URI): self.URI = URI @@ -21,34 +19,55 @@ def set_URI(self, URI): def get_URI(self): return self.URI - # TODO - # preamble - # compute - # citation - # image - - def set_defaults(self, name): + def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) - self.set_ui_shuffle(False) + self.set_preamble() + self.set_ui_default() - def update_activity(self, item_info): + def set_compute(self, variable, expression): + self.schema["compute"] = [ + {"variableName": variable, "jsExpression": expression} + ] - # TODO - # - remove the hard coding on visibility and valueRequired + def append_item(self, item): - # update the content of the activity schema with new item + property = { + "variableName": item.schema["prefLabel"].replace(" ", "_"), + "isAbout": item.URI, + "isVis": item.visible, + "requiredValue": item.required, + } + if item.skippable: + property["allow"] = ["reproschema:Skipped"] - item_info["URI"] = "items/" + item_info["name"] + self.schema["ui"]["order"].append(item.URI) + self.schema["ui"]["addProperties"].append(property) - append_to_activity = { - "variableName": item_info["name"], - "isAbout": item_info["URI"], - "isVis": item_info["visibility"], - "valueRequired": False, + """ + UI + """ + + def set_ui_default(self): + self.schema["ui"] = { + "shuffle": [], + "order": [], + "addProperties": [], + "allow": [], } + self.set_ui_shuffle() + self.set_ui_allow() + + def set_ui_shuffle(self, shuffle=False): + self.schema["ui"]["shuffle"] = shuffle - self.schema["ui"]["order"].append(item_info["URI"]) - self.schema["ui"]["addProperties"].append(append_to_activity) + def set_ui_allow( + self, value=["reproschema:AutoAdvance", "reproschema:AllowExport"] + ): + self.schema["ui"]["allow"] = value + + """ + writing, reading, sorting, unsetting + """ def sort(self): schema_order = [ @@ -59,9 +78,17 @@ def sort(self): "description", "schemaVersion", "version", + "preamble", + "citation", + "image", + "compute", "ui", ] self.sort_schema(schema_order) - ui_order = ["shuffle", "order", "addProperties"] + ui_order = ["shuffle", "order", "addProperties", "allow"] self.sort_ui(ui_order) + + def write(self, output_dir): + self.sort() + self._SchemaBase__write(output_dir) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 29a94a3..8e8b0a5 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -2,6 +2,8 @@ import os from collections import OrderedDict +DEFAULT_LANG = "en" + class SchemaBase: """ @@ -26,6 +28,15 @@ def __init__(self, version): def set_context(self, context): self.schema["@context"] = context + def set_preamble(self, preamble="", lang=DEFAULT_LANG): + self.schema["preamble"] = {lang: preamble} + + def set_citation(self, citation): + self.schema["citation"] = citation + + def set_image(self, image): + self.schema["image"] = image + def set_filename(self, name, ext=".jsonld"): self.schema_file = name + "_schema" + ext self.schema["@id"] = name + "_schema" + ext diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 6f69256..d4c672f 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -17,15 +17,15 @@ def __init__(self, version=None): self.schema["question"] = {} self.schema["responseOptions"] = {} self.response_options = ResponseOption() + self.visible = True + self.required = True + self.skippable = True # default input type is "text" self.set_input_type_as_text() def set_URI(self, URI): self.URI = URI - # TODO - # image - def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) self.set_filename(name) diff --git a/reproschema/models/tests/.gitignore b/reproschema/models/tests/.gitignore index a6fbd9a..a37b48e 100644 --- a/reproschema/models/tests/.gitignore +++ b/reproschema/models/tests/.gitignore @@ -1 +1,2 @@ items/*.jsonld +activities/*.jsonld diff --git a/reproschema/models/tests/data/activities/activity1_schema.jsonld b/reproschema/models/tests/data/activities/activity1_schema.jsonld new file mode 100644 index 0000000..2faa60e --- /dev/null +++ b/reproschema/models/tests/data/activities/activity1_schema.jsonld @@ -0,0 +1,58 @@ +{ + "@context": "../../contexts/generic", + "@type": "reproschema:Activity", + "@id": "activity1_schema.jsonld", + "prefLabel": "Example 1", + "description": "Activity example 1", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "citation": "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", + "image": { + "@type": "AudioObject", + "contentUrl": "http://example.com/sample-image.png" + }, + "preamble": { + "en": "Over the last 2 weeks, how often have you been bothered by any of the following problems?" + }, + "compute": [ + { + "variableName": "activity1_total_score", + "jsExpression": "item1 + item2" + } + ], + "ui": { + "addProperties": [ + { + "isAbout": "items/item1.jsonld", + "variableName": "item1", + "requiredValue": true, + "isVis": true + }, + { + "isAbout": "items/item2.jsonld", + "variableName": "item2", + "requiredValue": true, + "isVis": true, + "allow": [ + "reproschema:Skipped" + ] + }, + { + "isAbout": "items/activity1_total_score", + "variableName": "activity1_total_score", + "requiredValue": true, + "isVis": false + } + ], + "order": [ + "items/item1.jsonld", + "items/item2.jsonld", + "items/activity1_total_score" + ], + "shuffle": false, + "allow": [ + "reproschema:AutoAdvance", + "reproschema:AllowExport" + ] + } +} diff --git a/reproschema/models/tests/data/activities/default_schema.jsonld b/reproschema/models/tests/data/activities/default_schema.jsonld new file mode 100644 index 0000000..0e89e0a --- /dev/null +++ b/reproschema/models/tests/data/activities/default_schema.jsonld @@ -0,0 +1,21 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Activity", + "@id": "default_schema.jsonld", + "prefLabel": "default", + "description": "default", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "preamble": { + "en": "" + }, + "ui": { + "shuffle": false, + "order": [], + "addProperties": [], + "allow": [ + "reproschema:AutoAdvance", + "reproschema:AllowExport" + ] + } +} diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py new file mode 100644 index 0000000..0123407 --- /dev/null +++ b/reproschema/models/tests/test_activity.py @@ -0,0 +1,81 @@ +import os, sys, json + +from ..activity import Activity +from ..item import Item + +my_path = os.path.dirname(os.path.abspath(__file__)) + +# sys.path.insert(0, my_path + "/../") + +activity_dir = os.path.join(my_path, "activities") +if not os.path.exists(activity_dir): + os.makedirs(os.path.join(activity_dir)) + +reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") + + +def test_default(): + + activity = Activity() + activity.set_defaults() + + activity.write(activity_dir) + activity_content, expected = load_jsons(activity) + assert activity_content == expected + + +def test_activity_local(): + + activity = Activity() + activity.set_defaults("activity1") + activity.set_context("../../contexts/generic") + activity.set_description("Activity example 1") + activity.set_pref_label("Example 1") + activity.set_preamble( + "Over the last 2 weeks, how often have you been bothered by any of the following problems?" + ) + activity.set_citation("https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/") + activity.set_image( + {"@type": "AudioObject", "contentUrl": "http://example.com/sample-image.png"} + ) + activity.set_compute("activity1_total_score", "item1 + item2") + + item_1 = Item() + item_1.set_defaults("item1") + item_1.set_URI(os.path.join("items", item_1.get_filename())) + item_1.skippable = False + activity.append_item(item_1) + + item_2 = Item() + item_2.set_defaults("item2") + item_2.set_URI(os.path.join("items", item_2.get_filename())) + activity.append_item(item_2) + + item_3 = Item() + item_3.set_defaults("activity1_total_score") + item_3.set_filename("activity1_total_score", "") + item_3.set_URI(os.path.join("items", item_3.get_filename())) + item_3.skippable = False + item_3.visible = False + activity.append_item(item_3) + + activity.write(activity_dir) + activity_content, expected = load_jsons(activity) + assert activity_content == expected + + +def load_jsons(obj): + + output_file = os.path.join(activity_dir, obj.get_filename()) + content = read_json(output_file) + + data_file = os.path.join(my_path, "data", "activities", obj.get_filename()) + expected = read_json(data_file) + + return content, expected + + +def read_json(file): + + with open(file, "r") as ff: + return json.load(ff) From f633b89aaa5b3a965be15cf6d9c125f22ed91029 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 13:14:54 +0200 Subject: [PATCH 22/69] refactor --- reproschema/models/activity.py | 10 ++----- reproschema/models/base.py | 48 +++++++++++++++++++++++++--------- reproschema/models/item.py | 3 --- 3 files changed, 37 insertions(+), 24 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index 8ff4337..c80e8f6 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -13,12 +13,6 @@ class to deal with reproschema activities def __init__(self, version=None): super().__init__(version) - def set_URI(self, URI): - self.URI = URI - - def get_URI(self): - return self.URI - def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) self.set_preamble() @@ -33,14 +27,14 @@ def append_item(self, item): property = { "variableName": item.schema["prefLabel"].replace(" ", "_"), - "isAbout": item.URI, + "isAbout": item.get_URI(), "isVis": item.visible, "requiredValue": item.required, } if item.skippable: property["allow"] = ["reproschema:Skipped"] - self.schema["ui"]["order"].append(item.URI) + self.schema["ui"]["order"].append(item.get_URI()) self.schema["ui"]["addProperties"].append(property) """ diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 8e8b0a5..f685424 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -25,6 +25,26 @@ def __init__(self, version): self.set_context(URL + VERSION + "/contexts/generic") + def __set_defaults(self, name): + self.set_filename(name) + self.set_directory(name) + self.set_pref_label(name.replace("_", " ")) + self.set_description(name.replace("_", " ")) + + """ + setters + """ + + def set_directory(self, output_directory): + self.dir = output_directory + + def set_URI(self, URI): + self.URI = URI + + """ + schema related setters + """ + def set_context(self, context): self.schema["@context"] = context @@ -41,26 +61,28 @@ def set_filename(self, name, ext=".jsonld"): self.schema_file = name + "_schema" + ext self.schema["@id"] = name + "_schema" + ext - def get_name(self): - return self.schema_file.replace("_schema", "") - - def get_filename(self): - return self.schema_file - def set_pref_label(self, pref_label): self.schema["prefLabel"] = pref_label def set_description(self, description): self.schema["description"] = description - def set_directory(self, output_directory): - self.dir = output_directory + """ + getters + """ - def __set_defaults(self, name): - self.set_filename(name) - self.set_directory(name) - self.set_pref_label(name.replace("_", " ")) - self.set_description(name.replace("_", " ")) + def get_name(self): + return self.schema_file.replace("_schema", "") + + def get_filename(self): + return self.schema_file + + def get_URI(self): + return self.URI + + """ + writing, reading, sorting, unsetting + """ def sort_schema(self, schema_order): diff --git a/reproschema/models/item.py b/reproschema/models/item.py index d4c672f..451636a 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -23,9 +23,6 @@ def __init__(self, version=None): # default input type is "text" self.set_input_type_as_text() - def set_URI(self, URI): - self.URI = URI - def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) self.set_filename(name) From e4b1085380e35e29fff8eb5b94a5bf26de09b0e0 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 13:32:20 +0200 Subject: [PATCH 23/69] use default context for activity test --- .../models/tests/data/activities/activity1_schema.jsonld | 2 +- reproschema/models/tests/test_activity.py | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/reproschema/models/tests/data/activities/activity1_schema.jsonld b/reproschema/models/tests/data/activities/activity1_schema.jsonld index 2faa60e..9f59f8f 100644 --- a/reproschema/models/tests/data/activities/activity1_schema.jsonld +++ b/reproschema/models/tests/data/activities/activity1_schema.jsonld @@ -1,5 +1,5 @@ { - "@context": "../../contexts/generic", + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Activity", "@id": "activity1_schema.jsonld", "prefLabel": "Example 1", diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index 0123407..7e9d253 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -16,6 +16,10 @@ def test_default(): + """ + FYI: The default activity does not conform to the schema + """ + activity = Activity() activity.set_defaults() @@ -24,11 +28,10 @@ def test_default(): assert activity_content == expected -def test_activity_local(): +def test_activity(): activity = Activity() activity.set_defaults("activity1") - activity.set_context("../../contexts/generic") activity.set_description("Activity example 1") activity.set_pref_label("Example 1") activity.set_preamble( @@ -42,6 +45,7 @@ def test_activity_local(): item_1 = Item() item_1.set_defaults("item1") + # probably want to have items/item_name be a default item_1.set_URI(os.path.join("items", item_1.get_filename())) item_1.skippable = False activity.append_item(item_1) From 892e8ee9e7f1a9df15baec21de0c3af7fe3268ae Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 14:29:44 +0200 Subject: [PATCH 24/69] change item default value --- reproschema/models/base.py | 5 +++++ reproschema/models/item.py | 6 +++--- reproschema/models/tests/test_activity.py | 3 +++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index f685424..6789d89 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -118,4 +118,9 @@ def from_file(cls, filepath): def reorder_dict_skip_missing(old_dict, key_list): + """ + reorders dictionary according to ``key_list`` + removing any key with no associated value + or that is not in the key list + """ return OrderedDict((k, old_dict[k]) for k in key_list if k in old_dict) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 451636a..1f535a3 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -10,6 +10,9 @@ class to deal with reproschema items """ schema_type = "reproschema:Field" + visible = True + required = False + skippable = True def __init__(self, version=None): super().__init__(version) @@ -17,9 +20,6 @@ def __init__(self, version=None): self.schema["question"] = {} self.schema["responseOptions"] = {} self.response_options = ResponseOption() - self.visible = True - self.required = True - self.skippable = True # default input type is "text" self.set_input_type_as_text() diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index 7e9d253..9d32cb3 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -48,11 +48,13 @@ def test_activity(): # probably want to have items/item_name be a default item_1.set_URI(os.path.join("items", item_1.get_filename())) item_1.skippable = False + item_1.required = True activity.append_item(item_1) item_2 = Item() item_2.set_defaults("item2") item_2.set_URI(os.path.join("items", item_2.get_filename())) + item_2.required = True activity.append_item(item_2) item_3 = Item() @@ -60,6 +62,7 @@ def test_activity(): item_3.set_filename("activity1_total_score", "") item_3.set_URI(os.path.join("items", item_3.get_filename())) item_3.skippable = False + item_3.required = True item_3.visible = False activity.append_item(item_3) From 0cc859963fc74b13fcc8b4549e928c0ad4fb94af Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 15:40:01 +0200 Subject: [PATCH 25/69] add tests for response options --- reproschema/models/base.py | 18 +++++- reproschema/models/item.py | 49 +++++++++++++++- reproschema/models/tests/.gitignore | 1 + .../data/response_options/example.jsonld | 46 +++++++++++++++ .../response_options/valueConstraints.jsonld | 10 ++++ .../models/tests/test_response_options.py | 58 +++++++++++++++++++ 6 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 reproschema/models/tests/data/response_options/example.jsonld create mode 100644 reproschema/models/tests/data/response_options/valueConstraints.jsonld create mode 100644 reproschema/models/tests/test_response_options.py diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 6789d89..120cff4 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -3,6 +3,13 @@ from collections import OrderedDict DEFAULT_LANG = "en" +DEFAULT_VERSION = "1.0.0-rc4" + + +def default_context(version): + URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" + VERSION = version or DEFAULT_VERSION + return URL + VERSION + "/contexts/generic" class SchemaBase: @@ -14,8 +21,7 @@ class to deal with reproschema schemas def __init__(self, version): - URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" - VERSION = version or "1.0.0-rc4" + VERSION = version or DEFAULT_VERSION self.schema = { "@type": self.schema_type, @@ -23,7 +29,13 @@ def __init__(self, version): "version": "0.0.1", } - self.set_context(URL + VERSION + "/contexts/generic") + URL = self.get_default_context(version) + self.set_context(URL) + + # This probably needs some cleaning but is at the moment necessary to pass + # the context to the ResponseOption class + def get_default_context(self, version): + return default_context(version) def __set_defaults(self, name): self.set_filename(name) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 1f535a3..93d60ed 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -222,11 +222,13 @@ def sort(self): self.sort_schema(schema_order) -class ResponseOption: +class ResponseOption(SchemaBase): """ class to deal with reproschema response options """ + schema_type = "reproschema:ResponseOption" + def __init__(self): self.options = { "valueType": "", @@ -236,7 +238,20 @@ def __init__(self): "multipleChoice": False, } + def set_defaults(self, name="valueConstraints", version=None): + super().__init__(version) + self.options["@context"] = self.schema["@context"] + self.options["@type"] = self.schema_type + self.set_filename(name) + + def set_filename(self, name, ext=".jsonld"): + name = name.replace(" ", "_") + self.schema_file = name + ext + self.options["@id"] = name + ext + def unset(self, keys): + if type(keys) == str: + keys = [keys] for i in keys: self.options.pop(i, None) @@ -260,3 +275,35 @@ def use_preset(self, URI): def add_choice(self, choice, value, lang=DEFAULT_LANG): self.options["choices"].append({"name": {lang: choice}, "value": value}) + + def sort(self): + options_order = [ + "@context", + "@type", + "@id", + "valueType", + "minValue", + "maxValue", + "multipleChoice", + "choices", + ] + reordered_dict = reorder_dict_skip_missing(self.options, options_order) + self.options = reordered_dict + + def write(self, output_dir): + self.sort() + self.schema = self.options + self._SchemaBase__write(output_dir) + + +# DUPLICATE from the base class: needs refactoring +from collections import OrderedDict + + +def reorder_dict_skip_missing(old_dict, key_list): + """ + reorders dictionary according to ``key_list`` + removing any key with no associated value + or that is not in the key list + """ + return OrderedDict((k, old_dict[k]) for k in key_list if k in old_dict) diff --git a/reproschema/models/tests/.gitignore b/reproschema/models/tests/.gitignore index a37b48e..9b6d53a 100644 --- a/reproschema/models/tests/.gitignore +++ b/reproschema/models/tests/.gitignore @@ -1,2 +1,3 @@ items/*.jsonld activities/*.jsonld +response_options/*.jsonld diff --git a/reproschema/models/tests/data/response_options/example.jsonld b/reproschema/models/tests/data/response_options/example.jsonld new file mode 100644 index 0000000..7dfe3bc --- /dev/null +++ b/reproschema/models/tests/data/response_options/example.jsonld @@ -0,0 +1,46 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@id": "example.jsonld", + "@type": "reproschema:ResponseOption", + "valueType": "xsd:integer", + "minValue": 0, + "maxValue": 6, + "choices": [ + { + "name": { + "en": "Not at all" + }, + "value": 0 + }, + { + "name": { + "en": "" + }, + "value": 1 + }, + { + "name": { + "en": "" + }, + "value": 2 + }, + { + "name": { + "en": "" + }, + "value": 3 + }, + { + "name": { + "en": "" + }, + "value": 4 + }, + { + "name": { + "en": "Completely" + }, + "value": 6 + } + ] +} diff --git a/reproschema/models/tests/data/response_options/valueConstraints.jsonld b/reproschema/models/tests/data/response_options/valueConstraints.jsonld new file mode 100644 index 0000000..b935e9a --- /dev/null +++ b/reproschema/models/tests/data/response_options/valueConstraints.jsonld @@ -0,0 +1,10 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@id": "valueConstraints.jsonld", + "@type": "reproschema:ResponseOption", + "valueType": "", + "minValue": 0, + "maxValue": 0, + "choices": [], + "multipleChoice": false +} diff --git a/reproschema/models/tests/test_response_options.py b/reproschema/models/tests/test_response_options.py new file mode 100644 index 0000000..1bfcf32 --- /dev/null +++ b/reproschema/models/tests/test_response_options.py @@ -0,0 +1,58 @@ +import os, sys, json + +from ..item import ResponseOption + +my_path = os.path.dirname(os.path.abspath(__file__)) + +# sys.path.insert(0, my_path + "/../") + +response_options_dir = os.path.join(my_path, "response_options") +if not os.path.exists(response_options_dir): + os.makedirs(os.path.join(response_options_dir)) + +reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") + + +def test_default(): + + response_options = ResponseOption() + response_options.set_defaults() + + response_options.write(response_options_dir) + content, expected = load_jsons(response_options) + assert content == expected + + +def test_example(): + + response_options = ResponseOption() + response_options.set_defaults() + response_options.set_filename("example") + response_options.set_type("integer") + response_options.unset("multipleChoice") + response_options.add_choice("Not at all", 0) + for i in range(1, 5): + response_options.add_choice("", i) + response_options.add_choice("Completely", 6) + response_options.set_max(6) + + response_options.write(response_options_dir) + content, expected = load_jsons(response_options) + assert content == expected + + +def load_jsons(obj): + + output_file = os.path.join(response_options_dir, obj.get_filename()) + content = read_json(output_file) + + data_file = os.path.join(my_path, "data", "response_options", obj.get_filename()) + expected = read_json(data_file) + + return content, expected + + +def read_json(file): + + with open(file, "r") as ff: + return json.load(ff) From 77e36e4a47b98ccdda99aa78f3191315d5a5ad0b Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 16:36:44 +0200 Subject: [PATCH 26/69] add default test for protocols --- reproschema/models/item.py | 3 +- reproschema/models/protocol.py | 18 ++++---- reproschema/models/tests/.gitignore | 1 + .../data/protocols/default_schema.jsonld | 22 ++++++++++ reproschema/models/tests/test_protocol.py | 44 +++++++++++++++++++ 5 files changed, 77 insertions(+), 11 deletions(-) create mode 100644 reproschema/models/tests/data/protocols/default_schema.jsonld create mode 100644 reproschema/models/tests/test_protocol.py diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 93d60ed..4895ef8 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -296,7 +296,8 @@ def write(self, output_dir): self._SchemaBase__write(output_dir) -# DUPLICATE from the base class: needs refactoring +# DUPLICATE from the base class to be used for ResponseOptions sorting of the options +# needs refactoring from collections import OrderedDict diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index 21971e6..fe79991 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -1,5 +1,7 @@ from .base import SchemaBase +DEFAULT_LANG = "en" + class Protocol(SchemaBase): """ @@ -20,14 +22,6 @@ def __init__(self, version=None): def set_landing_page(self, landing_page_url, lang="en"): self.schema["landingPage"] = {"@id": landing_page_url, "inLanguage": lang} - # TODO - # def add_landing_page(self, landing_page_url, lang="en"): - # preamble - # compute - - def set_image(self, image_url): - self.schema["image"] = image_url - def set_ui_allow(self): self.schema["ui"]["allow"] = [ "reproschema:AutoAdvance", @@ -37,9 +31,9 @@ def set_ui_allow(self): def set_ui_shuffle(self, shuffle=False): self.schema["ui"]["shuffle"] = shuffle - def set_defaults(self, name): + def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) - self.set_landing_page("../../README-en.md") + self.set_landing_page("README-en.md") self.set_ui_allow() self.set_ui_shuffle(False) @@ -76,3 +70,7 @@ def sort(self): ui_order = ["allow", "shuffle", "order", "addProperties"] self.sort_ui(ui_order) + + def write(self, output_dir): + self.sort() + self._SchemaBase__write(output_dir) diff --git a/reproschema/models/tests/.gitignore b/reproschema/models/tests/.gitignore index 9b6d53a..2f9a1e0 100644 --- a/reproschema/models/tests/.gitignore +++ b/reproschema/models/tests/.gitignore @@ -1,3 +1,4 @@ items/*.jsonld activities/*.jsonld response_options/*.jsonld +protocols/*.jsonld diff --git a/reproschema/models/tests/data/protocols/default_schema.jsonld b/reproschema/models/tests/data/protocols/default_schema.jsonld new file mode 100644 index 0000000..0770a44 --- /dev/null +++ b/reproschema/models/tests/data/protocols/default_schema.jsonld @@ -0,0 +1,22 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Protocol", + "@id": "default_schema.jsonld", + "prefLabel": "default", + "description": "default", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "landingPage": { + "@id": "README-en.md", + "inLanguage": "en" + }, + "ui": { + "allow": [ + "reproschema:AutoAdvance", + "reproschema:AllowExport" + ], + "shuffle": false, + "order": [], + "addProperties": [] + } +} diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py new file mode 100644 index 0000000..be78198 --- /dev/null +++ b/reproschema/models/tests/test_protocol.py @@ -0,0 +1,44 @@ +import os, sys, json + +from ..protocol import Protocol +from ..activity import Activity + +my_path = os.path.dirname(os.path.abspath(__file__)) + +# sys.path.insert(0, my_path + "/../") + +protocol_dir = os.path.join(my_path, "protocols") +if not os.path.exists(protocol_dir): + os.makedirs(os.path.join(protocol_dir)) + +reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") + + +def test_default(): + + protocol = Protocol() + protocol.set_defaults() + + protocol.write(protocol_dir) + protocol_content, expected = load_jsons(protocol) + assert protocol_content == expected + + +# def test_protocol(): + + +def load_jsons(obj): + + output_file = os.path.join(protocol_dir, obj.get_filename()) + content = read_json(output_file) + + data_file = os.path.join(my_path, "data", "protocols", obj.get_filename()) + expected = read_json(data_file) + + return content, expected + + +def read_json(file): + + with open(file, "r") as ff: + return json.load(ff) From 8c1482977a24c3d792d642cc5ad0c8c2734f13e9 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 17:09:58 +0200 Subject: [PATCH 27/69] use language for preferred label by default --- reproschema/models/base.py | 4 ++-- .../models/tests/data/activities/activity1_schema.jsonld | 4 +++- .../models/tests/data/activities/default_schema.jsonld | 4 +++- reproschema/models/tests/data/items/country.jsonld | 4 +++- reproschema/models/tests/data/items/date.jsonld | 4 +++- reproschema/models/tests/data/items/default.jsonld | 4 +++- reproschema/models/tests/data/items/email.jsonld | 4 +++- reproschema/models/tests/data/items/float.jsonld | 4 +++- reproschema/models/tests/data/items/integer.jsonld | 4 +++- reproschema/models/tests/data/items/language.jsonld | 4 +++- reproschema/models/tests/data/items/multitext.jsonld | 4 +++- reproschema/models/tests/data/items/participant_id.jsonld | 8 ++++++-- reproschema/models/tests/data/items/radio.jsonld | 4 +++- reproschema/models/tests/data/items/radio_multiple.jsonld | 4 +++- reproschema/models/tests/data/items/select.jsonld | 4 +++- .../models/tests/data/items/select_multiple.jsonld | 4 +++- reproschema/models/tests/data/items/slider.jsonld | 4 +++- reproschema/models/tests/data/items/state.jsonld | 4 +++- reproschema/models/tests/data/items/text.jsonld | 4 +++- reproschema/models/tests/data/items/time_range.jsonld | 4 +++- reproschema/models/tests/data/items/year.jsonld | 4 +++- .../models/tests/data/protocols/default_schema.jsonld | 4 +++- 22 files changed, 68 insertions(+), 24 deletions(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 120cff4..1d5d4d7 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -73,8 +73,8 @@ def set_filename(self, name, ext=".jsonld"): self.schema_file = name + "_schema" + ext self.schema["@id"] = name + "_schema" + ext - def set_pref_label(self, pref_label): - self.schema["prefLabel"] = pref_label + def set_pref_label(self, pref_label, lang=DEFAULT_LANG): + self.schema["prefLabel"] = {lang: pref_label} def set_description(self, description): self.schema["description"] = description diff --git a/reproschema/models/tests/data/activities/activity1_schema.jsonld b/reproschema/models/tests/data/activities/activity1_schema.jsonld index 9f59f8f..c0f3fe3 100644 --- a/reproschema/models/tests/data/activities/activity1_schema.jsonld +++ b/reproschema/models/tests/data/activities/activity1_schema.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Activity", "@id": "activity1_schema.jsonld", - "prefLabel": "Example 1", + "prefLabel": { + "en": "Example 1" + }, "description": "Activity example 1", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/activities/default_schema.jsonld b/reproschema/models/tests/data/activities/default_schema.jsonld index 0e89e0a..9efd947 100644 --- a/reproschema/models/tests/data/activities/default_schema.jsonld +++ b/reproschema/models/tests/data/activities/default_schema.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Activity", "@id": "default_schema.jsonld", - "prefLabel": "default", + "prefLabel": { + "en": "default" + }, "description": "default", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/country.jsonld b/reproschema/models/tests/data/items/country.jsonld index 45ddecb..15c0677 100644 --- a/reproschema/models/tests/data/items/country.jsonld +++ b/reproschema/models/tests/data/items/country.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "country.jsonld", - "prefLabel": "country", + "prefLabel": { + "en": "country" + }, "description": "country", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/date.jsonld b/reproschema/models/tests/data/items/date.jsonld index 335024f..85efd8f 100644 --- a/reproschema/models/tests/data/items/date.jsonld +++ b/reproschema/models/tests/data/items/date.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "date.jsonld", - "prefLabel": "date", + "prefLabel": { + "en": "date" + }, "description": "date", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/default.jsonld b/reproschema/models/tests/data/items/default.jsonld index 9ab8eca..f4bbbfe 100644 --- a/reproschema/models/tests/data/items/default.jsonld +++ b/reproschema/models/tests/data/items/default.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "default.jsonld", - "prefLabel": "default", + "prefLabel": { + "en": "default" + }, "description": "default", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/email.jsonld b/reproschema/models/tests/data/items/email.jsonld index 37dcb0d..1d4540e 100644 --- a/reproschema/models/tests/data/items/email.jsonld +++ b/reproschema/models/tests/data/items/email.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "email.jsonld", - "prefLabel": "email", + "prefLabel": { + "en": "email" + }, "description": "email", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/float.jsonld b/reproschema/models/tests/data/items/float.jsonld index 62aa9c6..e7200b5 100644 --- a/reproschema/models/tests/data/items/float.jsonld +++ b/reproschema/models/tests/data/items/float.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "float.jsonld", - "prefLabel": "float", + "prefLabel": { + "en": "float" + }, "description": "This is a float item.", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/integer.jsonld b/reproschema/models/tests/data/items/integer.jsonld index f4af728..b184085 100644 --- a/reproschema/models/tests/data/items/integer.jsonld +++ b/reproschema/models/tests/data/items/integer.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "integer.jsonld", - "prefLabel": "integer", + "prefLabel": { + "en": "integer" + }, "description": "This is a integer item.", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/language.jsonld b/reproschema/models/tests/data/items/language.jsonld index 982a6ca..71a0dac 100644 --- a/reproschema/models/tests/data/items/language.jsonld +++ b/reproschema/models/tests/data/items/language.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "language.jsonld", - "prefLabel": "language", + "prefLabel": { + "en": "language" + }, "description": "language", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/multitext.jsonld b/reproschema/models/tests/data/items/multitext.jsonld index bf5abbf..04329b9 100644 --- a/reproschema/models/tests/data/items/multitext.jsonld +++ b/reproschema/models/tests/data/items/multitext.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "multitext.jsonld", - "prefLabel": "multitext", + "prefLabel": { + "en": "multitext" + }, "description": "multitext", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/participant_id.jsonld b/reproschema/models/tests/data/items/participant_id.jsonld index 0ab99bc..3d3ad12 100644 --- a/reproschema/models/tests/data/items/participant_id.jsonld +++ b/reproschema/models/tests/data/items/participant_id.jsonld @@ -2,11 +2,15 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "participant_id.jsonld", - "prefLabel": "participant id", + "prefLabel": { + "en": "participant id" + }, "description": "participant id", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", - "question": {"en": "input the participant id number"}, + "question": { + "en": "input the participant id number" + }, "ui": { "inputType": "pid" }, diff --git a/reproschema/models/tests/data/items/radio.jsonld b/reproschema/models/tests/data/items/radio.jsonld index dc5658d..5ead18c 100644 --- a/reproschema/models/tests/data/items/radio.jsonld +++ b/reproschema/models/tests/data/items/radio.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "radio.jsonld", - "prefLabel": "radio", + "prefLabel": { + "en": "radio" + }, "description": "radio", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/radio_multiple.jsonld b/reproschema/models/tests/data/items/radio_multiple.jsonld index cbc24c8..694e099 100644 --- a/reproschema/models/tests/data/items/radio_multiple.jsonld +++ b/reproschema/models/tests/data/items/radio_multiple.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "radio_multiple.jsonld", - "prefLabel": "radio multiple", + "prefLabel": { + "en": "radio multiple" + }, "description": "radio multiple", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/select.jsonld b/reproschema/models/tests/data/items/select.jsonld index 72c3f02..b7e63f8 100644 --- a/reproschema/models/tests/data/items/select.jsonld +++ b/reproschema/models/tests/data/items/select.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "select.jsonld", - "prefLabel": "select", + "prefLabel": { + "en": "select" + }, "description": "select", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/select_multiple.jsonld b/reproschema/models/tests/data/items/select_multiple.jsonld index 8a7f800..ab855ee 100644 --- a/reproschema/models/tests/data/items/select_multiple.jsonld +++ b/reproschema/models/tests/data/items/select_multiple.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "select_multiple.jsonld", - "prefLabel": "select multiple", + "prefLabel": { + "en": "select multiple" + }, "description": "select multiple", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/slider.jsonld b/reproschema/models/tests/data/items/slider.jsonld index 73774ec..80114dd 100644 --- a/reproschema/models/tests/data/items/slider.jsonld +++ b/reproschema/models/tests/data/items/slider.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "slider.jsonld", - "prefLabel": "slider", + "prefLabel": { + "en": "slider" + }, "description": "slider", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/state.jsonld b/reproschema/models/tests/data/items/state.jsonld index ab4a3e1..b09c421 100644 --- a/reproschema/models/tests/data/items/state.jsonld +++ b/reproschema/models/tests/data/items/state.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "state.jsonld", - "prefLabel": "state", + "prefLabel": { + "en": "state" + }, "description": "state", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/text.jsonld b/reproschema/models/tests/data/items/text.jsonld index 854a8c1..d27c641 100644 --- a/reproschema/models/tests/data/items/text.jsonld +++ b/reproschema/models/tests/data/items/text.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "text.jsonld", - "prefLabel": "text", + "prefLabel": { + "en": "text" + }, "description": "text", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/time_range.jsonld b/reproschema/models/tests/data/items/time_range.jsonld index 9a84deb..14c1350 100644 --- a/reproschema/models/tests/data/items/time_range.jsonld +++ b/reproschema/models/tests/data/items/time_range.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "time_range.jsonld", - "prefLabel": "time range", + "prefLabel": { + "en": "time range" + }, "description": "time range", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/year.jsonld b/reproschema/models/tests/data/items/year.jsonld index 62dd48e..e4e3039 100644 --- a/reproschema/models/tests/data/items/year.jsonld +++ b/reproschema/models/tests/data/items/year.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Field", "@id": "year.jsonld", - "prefLabel": "year", + "prefLabel": { + "en": "year" + }, "description": "year", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/protocols/default_schema.jsonld b/reproschema/models/tests/data/protocols/default_schema.jsonld index 0770a44..076e3ca 100644 --- a/reproschema/models/tests/data/protocols/default_schema.jsonld +++ b/reproschema/models/tests/data/protocols/default_schema.jsonld @@ -2,7 +2,9 @@ "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@type": "reproschema:Protocol", "@id": "default_schema.jsonld", - "prefLabel": "default", + "prefLabel": { + "en": "default" + }, "description": "default", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", From cf1b3ce5f1b53c5649a4566cad12ec76bbdd170d Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 17:11:15 +0200 Subject: [PATCH 28/69] get variable name for item in activity from their basename --- reproschema/models/activity.py | 2 +- reproschema/models/base.py | 4 ++++ .../models/tests/data/activities/activity1_schema.jsonld | 6 +++--- reproschema/models/tests/test_activity.py | 3 ++- reproschema/models/tests/test_item.py | 2 +- 5 files changed, 11 insertions(+), 6 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index c80e8f6..d99d846 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -26,7 +26,7 @@ def set_compute(self, variable, expression): def append_item(self, item): property = { - "variableName": item.schema["prefLabel"].replace(" ", "_"), + "variableName": item.get_basename(), "isAbout": item.get_URI(), "isVis": item.visible, "requiredValue": item.required, diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 1d5d4d7..e3f95e3 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -1,5 +1,6 @@ import json import os +from pathlib import Path from collections import OrderedDict DEFAULT_LANG = "en" @@ -89,6 +90,9 @@ def get_name(self): def get_filename(self): return self.schema_file + def get_basename(self): + return Path(self.schema_file).stem + def get_URI(self): return self.URI diff --git a/reproschema/models/tests/data/activities/activity1_schema.jsonld b/reproschema/models/tests/data/activities/activity1_schema.jsonld index c0f3fe3..e3c4aee 100644 --- a/reproschema/models/tests/data/activities/activity1_schema.jsonld +++ b/reproschema/models/tests/data/activities/activity1_schema.jsonld @@ -31,8 +31,8 @@ "isVis": true }, { - "isAbout": "items/item2.jsonld", - "variableName": "item2", + "variableName": "item_two", + "isAbout": "../other_dir/item_two.jsonld", "requiredValue": true, "isVis": true, "allow": [ @@ -48,7 +48,7 @@ ], "order": [ "items/item1.jsonld", - "items/item2.jsonld", + "../other_dir/item_two.jsonld", "items/activity1_total_score" ], "shuffle": false, diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index 9d32cb3..61a9439 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -53,7 +53,8 @@ def test_activity(): item_2 = Item() item_2.set_defaults("item2") - item_2.set_URI(os.path.join("items", item_2.get_filename())) + item_2.set_filename("item_two") + item_2.set_URI(os.path.join("..", "other_dir", item_2.get_filename())) item_2.required = True activity.append_item(item_2) diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index 1b00cd3..9f78423 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -263,7 +263,7 @@ def test_read_only(): item.set_defaults("activity1_total_score") item.set_context("../../../contexts/generic") item.set_filename("activity1_total_score", "") - item.set_pref_label("activity1_total_score") + item.schema["prefLabel"] = "activity1_total_score" item.set_description("Score item for Activity 1") item.set_read_only_value(True) item.set_input_type_as_int() From b7804cc85b6d78146fb63cabce271ea4e99a1d84 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 17:58:51 +0200 Subject: [PATCH 29/69] add test for protocols --- reproschema/models/activity.py | 30 ++--------- reproschema/models/base.py | 32 ++++++++++- reproschema/models/protocol.py | 53 ++++++++++--------- .../data/protocols/default_schema.jsonld | 3 ++ .../data/protocols/protocol1_schema.jsonld | 43 +++++++++++++++ reproschema/models/tests/test_protocol.py | 31 ++++++++++- 6 files changed, 140 insertions(+), 52 deletions(-) create mode 100644 reproschema/models/tests/data/protocols/protocol1_schema.jsonld diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index d99d846..f70b0ac 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -10,6 +10,10 @@ class to deal with reproschema activities schema_type = "reproschema:Activity" + visible = True + required = False + skippable = True + def __init__(self, version=None): super().__init__(version) @@ -37,28 +41,6 @@ def append_item(self, item): self.schema["ui"]["order"].append(item.get_URI()) self.schema["ui"]["addProperties"].append(property) - """ - UI - """ - - def set_ui_default(self): - self.schema["ui"] = { - "shuffle": [], - "order": [], - "addProperties": [], - "allow": [], - } - self.set_ui_shuffle() - self.set_ui_allow() - - def set_ui_shuffle(self, shuffle=False): - self.schema["ui"]["shuffle"] = shuffle - - def set_ui_allow( - self, value=["reproschema:AutoAdvance", "reproschema:AllowExport"] - ): - self.schema["ui"]["allow"] = value - """ writing, reading, sorting, unsetting """ @@ -79,9 +61,7 @@ def sort(self): "ui", ] self.sort_schema(schema_order) - - ui_order = ["shuffle", "order", "addProperties", "allow"] - self.sort_ui(ui_order) + self.sort_ui() def write(self, output_dir): self.sort() diff --git a/reproschema/models/base.py b/reproschema/models/base.py index e3f95e3..7445951 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -93,9 +93,39 @@ def get_filename(self): def get_basename(self): return Path(self.schema_file).stem + def get_pref_label(self): + return self.schema["prefLabel"] + def get_URI(self): return self.URI + """ + UI + """ + + def set_ui_default(self): + self.schema["ui"] = { + "shuffle": [], + "order": [], + "addProperties": [], + "allow": [], + } + self.set_ui_shuffle() + self.set_ui_allow() + + def set_ui_shuffle(self, shuffle=False): + self.schema["ui"]["shuffle"] = shuffle + + def set_ui_allow(self, auto_advance=True, allow_export=True, disable_back=False): + allow = [] + if auto_advance: + allow.append("reproschema:AutoAdvance") + if allow_export: + allow.append("reproschema:AllowExport") + if disable_back: + allow.append("reproschema:DisableBack") + self.schema["ui"]["allow"] = allow + """ writing, reading, sorting, unsetting """ @@ -105,7 +135,7 @@ def sort_schema(self, schema_order): reordered_dict = reorder_dict_skip_missing(self.schema, schema_order) self.schema = reordered_dict - def sort_ui(self, ui_order): + def sort_ui(self, ui_order=["shuffle", "order", "addProperties", "allow"]): reordered_dict = reorder_dict_skip_missing(self.schema["ui"], ui_order) self.schema["ui"] = reordered_dict diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index fe79991..a2cc4be 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -19,40 +19,41 @@ def __init__(self, version=None): "addProperties": [], } - def set_landing_page(self, landing_page_url, lang="en"): - self.schema["landingPage"] = {"@id": landing_page_url, "inLanguage": lang} - - def set_ui_allow(self): - self.schema["ui"]["allow"] = [ - "reproschema:AutoAdvance", - "reproschema:AllowExport", - ] - - def set_ui_shuffle(self, shuffle=False): - self.schema["ui"]["shuffle"] = shuffle - def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) self.set_landing_page("README-en.md") - self.set_ui_allow() - self.set_ui_shuffle(False) + self.set_preamble() + self.set_ui_default() - def append_activity(self, activity): + def set_landing_page(self, landing_page_uri, lang=DEFAULT_LANG): + self.schema["landingPage"] = {"@id": landing_page_uri, "inLanguage": lang} - # TODO - # - remove the hard coding on visibility and valueRequired + def append_activity(self, activity): - # update the content of the protocol with this new activity append_to_protocol = { - "variableName": activity.get_name(), + "variableName": activity.get_basename().replace("_schema", ""), "isAbout": activity.get_URI(), - "prefLabel": {"en": activity.schema["prefLabel"]}, + "prefLabel": activity.get_pref_label(), "isVis": True, "valueRequired": False, } - self.schema["ui"]["order"].append(activity.URI) - self.schema["ui"]["addProperties"].append(append_to_protocol) + property = { + "variableName": activity.get_basename().replace("_schema", ""), + "isAbout": activity.get_URI(), + "prefLabel": activity.get_pref_label(), + "isVis": activity.visible, + "requiredValue": activity.required, + } + if activity.skippable: + property["allow"] = ["reproschema:Skipped"] + + self.schema["ui"]["order"].append(activity.get_URI()) + self.schema["ui"]["addProperties"].append(property) + + """ + writing, reading, sorting, unsetting + """ def sort(self): schema_order = [ @@ -64,12 +65,14 @@ def sort(self): "schemaVersion", "version", "landingPage", + "preamble", + "citation", + "image", + "compute", "ui", ] self.sort_schema(schema_order) - - ui_order = ["allow", "shuffle", "order", "addProperties"] - self.sort_ui(ui_order) + self.sort_ui() def write(self, output_dir): self.sort() diff --git a/reproschema/models/tests/data/protocols/default_schema.jsonld b/reproschema/models/tests/data/protocols/default_schema.jsonld index 076e3ca..7612d91 100644 --- a/reproschema/models/tests/data/protocols/default_schema.jsonld +++ b/reproschema/models/tests/data/protocols/default_schema.jsonld @@ -12,6 +12,9 @@ "@id": "README-en.md", "inLanguage": "en" }, + "preamble": { + "en": "" + }, "ui": { "allow": [ "reproschema:AutoAdvance", diff --git a/reproschema/models/tests/data/protocols/protocol1_schema.jsonld b/reproschema/models/tests/data/protocols/protocol1_schema.jsonld new file mode 100644 index 0000000..38426ce --- /dev/null +++ b/reproschema/models/tests/data/protocols/protocol1_schema.jsonld @@ -0,0 +1,43 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Protocol", + "@id": "protocol1_schema.jsonld", + "prefLabel": { + "en": "Protocol1" + }, + "description": "example Protocol", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "landingPage": { + "@id": "http://example.com/sample-readme.md", + "inLanguage": "en" + }, + "preamble": { + "en": "" + }, + "ui": { + "addProperties": [ + { + "isAbout": "../activities/activity1_schema.jsonld", + "variableName": "activity1", + "prefLabel": { + "en": "Screening" + }, + "isVis": true, + "requiredValue": false, + "allow": [ + "reproschema:Skipped" + ] + } + ], + "order": [ + "../activities/activity1_schema.jsonld" + ], + "shuffle": false, + "allow": [ + "reproschema:AutoAdvance", + "reproschema:AllowExport", + "reproschema:DisableBack" + ] + } +} diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py index be78198..9cf60eb 100644 --- a/reproschema/models/tests/test_protocol.py +++ b/reproschema/models/tests/test_protocol.py @@ -23,8 +23,33 @@ def test_default(): protocol_content, expected = load_jsons(protocol) assert protocol_content == expected + clean_up(protocol) -# def test_protocol(): + +def test_protocol(): + + protocol = Protocol() + protocol.set_defaults("protocol1") + protocol.set_pref_label("Protocol1") + protocol.set_description("example Protocol") + protocol.set_landing_page("http://example.com/sample-readme.md") + + auto_advance = True + allow_export = True + disable_back = True + protocol.set_ui_allow(auto_advance, allow_export, disable_back) + + activity_1 = Activity() + activity_1.set_defaults("activity1") + activity_1.set_pref_label("Screening") + activity_1.set_URI(os.path.join("..", "activities", activity_1.get_filename())) + protocol.append_activity(activity_1) + + protocol.write(protocol_dir) + protocol_content, expected = load_jsons(protocol) + assert protocol_content == expected + + clean_up(protocol) def load_jsons(obj): @@ -42,3 +67,7 @@ def read_json(file): with open(file, "r") as ff: return json.load(ff) + + +def clean_up(obj): + os.remove(os.path.join(protocol_dir, obj.get_filename())) From 5cf45875ccb2d90f18bafccafb44c4f19b572eed Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 17:59:05 +0200 Subject: [PATCH 30/69] add clean up --- reproschema/models/tests/test_activity.py | 8 +++++ reproschema/models/tests/test_item.py | 42 +++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index 61a9439..461704c 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -27,6 +27,8 @@ def test_default(): activity_content, expected = load_jsons(activity) assert activity_content == expected + clean_up(activity) + def test_activity(): @@ -71,6 +73,8 @@ def test_activity(): activity_content, expected = load_jsons(activity) assert activity_content == expected + clean_up(activity) + def load_jsons(obj): @@ -87,3 +91,7 @@ def read_json(file): with open(file, "r") as ff: return json.load(ff) + + +def clean_up(obj): + os.remove(os.path.join(activity_dir, obj.get_filename())) diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index 9f78423..0e10d18 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -22,6 +22,8 @@ def test_default(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_text(): @@ -35,6 +37,8 @@ def test_text(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_multitext(): @@ -48,6 +52,8 @@ def test_multitext(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_email(): @@ -61,6 +67,8 @@ def test_email(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_participant_id(): @@ -74,6 +82,8 @@ def test_participant_id(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_date(): @@ -87,6 +97,8 @@ def test_date(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_time_range(): @@ -100,6 +112,8 @@ def test_time_range(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_year(): @@ -113,6 +127,8 @@ def test_year(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_float(): @@ -126,6 +142,8 @@ def test_float(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_integer(): @@ -139,6 +157,8 @@ def test_integer(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_language(): @@ -151,6 +171,8 @@ def test_language(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_country(): @@ -163,6 +185,8 @@ def test_country(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_state(): @@ -175,6 +199,8 @@ def test_state(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_radio(): @@ -194,6 +220,8 @@ def test_radio(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + item.set_filename("radio multiple") item.set_description("radio multiple") item.set_pref_label("radio multiple") @@ -205,6 +233,8 @@ def test_radio(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_select(): @@ -224,6 +254,8 @@ def test_select(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + item.set_filename("select multiple") item.set_description("select multiple") item.set_pref_label("select multiple") @@ -235,6 +267,8 @@ def test_select(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_slider(): @@ -256,6 +290,8 @@ def test_slider(): item_content, expected = load_jsons(item) assert item_content == expected + clean_up(item) + def test_read_only(): @@ -283,6 +319,8 @@ def test_read_only(): expected = read_json(data_file) assert item_content == expected + clean_up(item) + def load_jsons(item): @@ -299,3 +337,7 @@ def read_json(file): with open(file, "r") as ff: return json.load(ff) + + +def clean_up(obj): + os.remove(os.path.join(item_dir, obj.get_filename())) From 122a5f69ef8580752dc13c251db81cbf5fb137ee Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 6 Jun 2021 17:59:52 +0200 Subject: [PATCH 31/69] fix copy paste error --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a2b714..b96c493 100755 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ #!/usr/bin/env python # emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- # vi: set ft=python sts=4 ts=4 sw=4 et: -"""Pydra: Dataflow Engine +"""Reproschema """ import sys From fb29ac2854dc9801a21c91099c46a5d29355402b Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 7 Jun 2021 17:50:11 +0200 Subject: [PATCH 32/69] Add comments to tests --- reproschema/models/item.py | 1 + reproschema/models/tests/README.md | 48 ++++++++ reproschema/models/tests/test_activity.py | 30 ++++- reproschema/models/tests/test_item.py | 107 +++++++++++++----- reproschema/models/tests/test_protocol.py | 17 +++ .../models/tests/test_response_options.py | 12 ++ 6 files changed, 188 insertions(+), 27 deletions(-) create mode 100644 reproschema/models/tests/README.md diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 4895ef8..d4b5b6c 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -296,6 +296,7 @@ def write(self, output_dir): self._SchemaBase__write(output_dir) +# TODO # DUPLICATE from the base class to be used for ResponseOptions sorting of the options # needs refactoring from collections import OrderedDict diff --git a/reproschema/models/tests/README.md b/reproschema/models/tests/README.md new file mode 100644 index 0000000..0ef4b7c --- /dev/null +++ b/reproschema/models/tests/README.md @@ -0,0 +1,48 @@ +# Tests for reproschema-py "models" + +## Philosophy + +Most of the test are trying to be step by step "recipes" to create the different +files in a schema. + +Each test tries to create an item from 'scratch' by using the `Protocol`, +`Activity`, `Item` and `ResponseOptions` classes and writes the resulting +`.jsonld` to the disk. + +The file is then read and its content compared to the "expected file" in the +`data` folder. + +When testing the Protocol and Activity classes, the output tries to get very +very close to the `jsonld` found in: + +``` +reproschema/tests/data/activities/items/activity1_total_score +``` + +Ideally this would avoided having 2 sets of `.jsonld` to test against. + +## Running the tests + +Requires `pytest` you can `pip install`. + +If you are developping the code, also make sure you have installed the +reproschema package locally and not from pypi. + +Run this from the root folder of where you cloned the reproschema package: + +``` +pip install -e . +``` + +More [here](../../README.md) + +## TODO + +- a lot of repeats in the test code base can be refactored + - especially for `test_items.py` a lot of those tests can be parametrized + (since apparently pytest allows this). + - the "clean up" after each passed test could be handle by a pytest fixture + - the helper functions are also nearly identical in all 3 test modules and + should be refactored + - some of the methods of the base class should probably be test on their own + rather than having tests for the sub classes that also "test" them. diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index 461704c..b4c72d0 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -5,12 +5,19 @@ my_path = os.path.dirname(os.path.abspath(__file__)) +# Left here in case Remi and python path or import can't be friends once again. # sys.path.insert(0, my_path + "/../") +# TODO +# refactor across the different test modules activity_dir = os.path.join(my_path, "activities") if not os.path.exists(activity_dir): os.makedirs(os.path.join(activity_dir)) +""" +Only for the few cases when we want to check against some of the files in +reproschema/tests/data +""" reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") @@ -18,6 +25,7 @@ def test_default(): """ FYI: The default activity does not conform to the schema + so `reproschema validate` will complain if you run it in this """ activity = Activity() @@ -47,22 +55,37 @@ def test_activity(): item_1 = Item() item_1.set_defaults("item1") + # TODO # probably want to have items/item_name be a default item_1.set_URI(os.path.join("items", item_1.get_filename())) + # TODO + # We probably want a method to change those values rather that modifying + # the instance directly item_1.skippable = False item_1.required = True + """ + Items are appended and this updates the the ``ui`` ``order`` and ``addProperties`` + """ activity.append_item(item_1) item_2 = Item() item_2.set_defaults("item2") item_2.set_filename("item_two") + + """ + In this case the URI is relative to where the activity file will be saved + """ item_2.set_URI(os.path.join("..", "other_dir", item_2.get_filename())) item_2.required = True activity.append_item(item_2) item_3 = Item() item_3.set_defaults("activity1_total_score") - item_3.set_filename("activity1_total_score", "") + """ + By default all files are save with a json.ld extension but this can be changed + """ + file_ext = "" + item_3.set_filename("activity1_total_score", file_ext) item_3.set_URI(os.path.join("items", item_3.get_filename())) item_3.skippable = False item_3.required = True @@ -76,6 +99,11 @@ def test_activity(): clean_up(activity) +""" +HELPER FUNCTIONS +""" + + def load_jsons(obj): output_file = os.path.join(activity_dir, obj.get_filename()) diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index 0e10d18..65bdb77 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -4,12 +4,19 @@ my_path = os.path.dirname(os.path.abspath(__file__)) +# Left here in case Remi and python path or import can't be friends once again. # sys.path.insert(0, my_path + "/../") +# TODO +# refactor across the different test modules item_dir = os.path.join(my_path, "items") if not os.path.exists(item_dir): os.makedirs(os.path.join(item_dir)) +""" +Only for the few cases when we want to check against some of the files in +reproschema/tests/data +""" reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") @@ -25,11 +32,18 @@ def test_default(): clean_up(item) +""" +text items +""" + + def test_text(): + text_length = 100 + item = Item("1.0.0-rc4") item.set_defaults("text") - item.set_input_type_as_text(100) + item.set_input_type_as_text(text_length) item.set_question("question for text item") @@ -42,9 +56,11 @@ def test_text(): def test_multitext(): + text_length = 50 + item = Item("1.0.0-rc4") item.set_defaults("multitext") - item.set_input_type_as_multitext(50) + item.set_input_type_as_multitext(text_length) item.set_question("This is an item where the user can input several text field.") @@ -55,6 +71,11 @@ def test_multitext(): clean_up(item) +""" +items with a specific "type" +""" + + def test_email(): item = Item("1.0.0-rc4") @@ -130,13 +151,17 @@ def test_year(): clean_up(item) -def test_float(): +""" +Items that refer to a preset list of responses choices +""" - item = Item("1.0.0-rc4") - item.set_defaults("float") - item.set_description("This is a float item.") - item.set_input_type_as_float() - item.set_question("This is an item where the user can input a float.") + +def test_language(): + + item = Item() + item.set_defaults("language") + item.set_input_type_as_language() + item.set_question("This is an item where the user can select several language.") item.write(item_dir) item_content, expected = load_jsons(item) @@ -145,13 +170,12 @@ def test_float(): clean_up(item) -def test_integer(): +def test_country(): item = Item() - item.set_defaults("integer") - item.set_description("This is a integer item.") - item.set_input_type_as_int() - item.set_question("This is an item where the user can input a integer.") + item.set_defaults("country") + item.set_input_type_as_country() + item.set_question("select a country") item.write(item_dir) item_content, expected = load_jsons(item) @@ -160,12 +184,12 @@ def test_integer(): clean_up(item) -def test_language(): +def test_state(): item = Item() - item.set_defaults("language") - item.set_input_type_as_language() - item.set_question("This is an item where the user can select several language.") + item.set_defaults("state") + item.set_input_type_as_state() + item.set_question("select a USA state") item.write(item_dir) item_content, expected = load_jsons(item) @@ -174,12 +198,18 @@ def test_language(): clean_up(item) -def test_country(): +""" +NUMERICAL ITEMS +""" - item = Item() - item.set_defaults("country") - item.set_input_type_as_country() - item.set_question("select a country") + +def test_float(): + + item = Item("1.0.0-rc4") + item.set_defaults("float") + item.set_description("This is a float item.") + item.set_input_type_as_float() + item.set_question("This is an item where the user can input a float.") item.write(item_dir) item_content, expected = load_jsons(item) @@ -188,12 +218,13 @@ def test_country(): clean_up(item) -def test_state(): +def test_integer(): item = Item() - item.set_defaults("state") - item.set_input_type_as_state() - item.set_question("select a USA state") + item.set_defaults("integer") + item.set_description("This is a integer item.") + item.set_input_type_as_int() + item.set_question("This is an item where the user can input a integer.") item.write(item_dir) item_content, expected = load_jsons(item) @@ -202,6 +233,14 @@ def test_state(): clean_up(item) +""" +SELECTION ITEMS: radio and select +tested both with: +- only one response allowed +- multiple responses allowed +""" + + def test_radio(): item = Item("1.0.0-rc4") @@ -212,6 +251,9 @@ def test_radio(): response_options = ResponseOption() response_options.add_choice("Not at all", 0, "en") response_options.add_choice("Several days", 1, "en") + # TODO + # set_min and set_max cold probably be combined into a single method that gets + # those values from the content of the choice key response_options.set_max(1) item.set_input_type_as_radio(response_options) @@ -293,6 +335,14 @@ def test_slider(): clean_up(item) +""" +Just to check that item with read only values + +Tries to recreate the item from +reproschema/tests/data/activities/items/activity1_total_score +""" + + def test_read_only(): item = Item() @@ -322,6 +372,11 @@ def test_read_only(): clean_up(item) +""" +HELPER FUNCTIONS +""" + + def load_jsons(item): output_file = os.path.join(item_dir, item.get_filename()) diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py index 9cf60eb..ddc391f 100644 --- a/reproschema/models/tests/test_protocol.py +++ b/reproschema/models/tests/test_protocol.py @@ -5,17 +5,29 @@ my_path = os.path.dirname(os.path.abspath(__file__)) +# Left here in case Remi and python path or import can't be friends once again. # sys.path.insert(0, my_path + "/../") +# TODO +# refactor across the different test modules protocol_dir = os.path.join(my_path, "protocols") if not os.path.exists(protocol_dir): os.makedirs(os.path.join(protocol_dir)) +""" +Only for the few cases when we want to check against some of the files in +reproschema/tests/data +""" reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") def test_default(): + """ + FYI: The default protocol does not conform to the schema + so `reproschema validate` will complain if you run it in this + """ + protocol = Protocol() protocol.set_defaults() @@ -52,6 +64,11 @@ def test_protocol(): clean_up(protocol) +""" +HELPER FUNCTIONS +""" + + def load_jsons(obj): output_file = os.path.join(protocol_dir, obj.get_filename()) diff --git a/reproschema/models/tests/test_response_options.py b/reproschema/models/tests/test_response_options.py index 1bfcf32..5cf13cc 100644 --- a/reproschema/models/tests/test_response_options.py +++ b/reproschema/models/tests/test_response_options.py @@ -4,12 +4,19 @@ my_path = os.path.dirname(os.path.abspath(__file__)) +# Left here in case Remi and python path or import can't be friends once again. # sys.path.insert(0, my_path + "/../") +# TODO +# refactor across the different test modules response_options_dir = os.path.join(my_path, "response_options") if not os.path.exists(response_options_dir): os.makedirs(os.path.join(response_options_dir)) +""" +Only for the few cases when we want to check against some of the files in +reproschema/tests/data +""" reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") @@ -41,6 +48,11 @@ def test_example(): assert content == expected +""" +HELPER FUNCTIONS +""" + + def load_jsons(obj): output_file = os.path.join(response_options_dir, obj.get_filename()) From 92427b6e4320553d50daa6d19bb878d67b4acce7 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 7 Jun 2021 20:23:46 +0200 Subject: [PATCH 33/69] comment code --- reproschema/models/README.md | 8 + reproschema/models/activity.py | 1 + reproschema/models/base.py | 98 +++++++++++- reproschema/models/item.py | 214 +++++++++++++++++--------- reproschema/models/protocol.py | 35 +++-- reproschema/models/tests/test_item.py | 3 + 6 files changed, 266 insertions(+), 93 deletions(-) create mode 100644 reproschema/models/README.md diff --git a/reproschema/models/README.md b/reproschema/models/README.md new file mode 100644 index 0000000..48d833f --- /dev/null +++ b/reproschema/models/README.md @@ -0,0 +1,8 @@ +# README + +## reproschema file creation + +The creation reproschema protocols, activities and items (or Field) are handled +by 3 python classes with the same names contained in their dedicated modules. + +They all inherit from the same base class. diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index f70b0ac..f7cc7cb 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -28,6 +28,7 @@ def set_compute(self, variable, expression): ] def append_item(self, item): + # See comment and TODO of the append_activity Protocol class property = { "variableName": item.get_basename(), diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 7445951..4cbb2bb 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -3,11 +3,21 @@ from pathlib import Path from collections import OrderedDict +""" +For any key that can be 'translated' we set english as the default language +in case the user does not provide it. +""" DEFAULT_LANG = "en" + +""" +""" DEFAULT_VERSION = "1.0.0-rc4" def default_context(version): + """ + For now we assume that the github repo will be where schema will be read from. + """ URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" VERSION = version or DEFAULT_VERSION return URL + VERSION + "/contexts/generic" @@ -15,13 +25,34 @@ def default_context(version): class SchemaBase: """ - class to deal with reproschema schemas + base class to deal with reproschema schemas + + The content of the schema is stored in the dictionnary ``self.schema`` + + self.schema["@context"] + self.schema["@type"] + self.schema["schemaVersion"] + self.schema["version"] + self.schema["preamble"] + self.schema["citation"] + self.schema["image"] + ... + + When the output file is created, its content is simply dumped into a json + by the ``write`` method. + """ + # TODO might be more convenient to have some of the properties not centrlized in a single dictionnary + # + # Could be more practical to only create part or all of the dictionnary when write is called + # + schema_type = None def __init__(self, version): + # TODO the version handling could probably be refactored VERSION = version or DEFAULT_VERSION self.schema = { @@ -41,6 +72,7 @@ def get_default_context(self, version): def __set_defaults(self, name): self.set_filename(name) self.set_directory(name) + "We use the ``name`` of this class instance for schema keys minus some underscore" self.set_pref_label(name.replace("_", " ")) self.set_description(name.replace("_", " ")) @@ -49,16 +81,30 @@ def __set_defaults(self, name): """ def set_directory(self, output_directory): + """ + Where the file will be written by the ``write`` method + """ self.dir = output_directory def set_URI(self, URI): + """ + In case we need to keep where the output file is located, + we can set it with this method. + This can be useful if we have just read or created an item + and want to add it to an activity. + """ self.URI = URI """ schema related setters + + use them to set several of the common keys of the schema """ def set_context(self, context): + """ + In case we want to overide the default context + """ self.schema["@context"] = context def set_preamble(self, preamble="", lang=DEFAULT_LANG): @@ -71,6 +117,14 @@ def set_image(self, image): self.schema["image"] = image def set_filename(self, name, ext=".jsonld"): + """ + By default all files are given: + - the ``.jsold`` extension + - have ``_schema`` suffix appended to them + + For item files their name won't have the schema prefix. + """ + # TODO figure out if the latter is a desirable behavior self.schema_file = name + "_schema" + ext self.schema["@id"] = name + "_schema" + ext @@ -82,6 +136,9 @@ def set_description(self, description): """ getters + + to access the content of some of the keys of the schema + or some of the instance properties """ def get_name(self): @@ -100,7 +157,12 @@ def get_URI(self): return self.URI """ - UI + UI: setters specific to the user interface keys of the schema + + The ui has its own dictionnary. + + Mostly used by the Protocol and Activity class. + """ def set_ui_default(self): @@ -117,6 +179,14 @@ def set_ui_shuffle(self, shuffle=False): self.schema["ui"]["shuffle"] = shuffle def set_ui_allow(self, auto_advance=True, allow_export=True, disable_back=False): + # TODO + # Could be more convenient to have one method for each property + # + # Also currently the way this is handled makes it hard to update a single value: + # all 3 have to be reset everytime!!! + # + # Could be useful to have a UI class with dictionnary content that is only + # generated when the file is written allow = [] if auto_advance: allow.append("reproschema:AutoAdvance") @@ -128,19 +198,39 @@ def set_ui_allow(self, auto_advance=True, allow_export=True, disable_back=False) """ writing, reading, sorting, unsetting + + Editing and appending things to the dictionnary tends to give json output + that is not standardized: the @context can end up at the bottom for one file + and stay at the top for another. + So there are a couple of sorting methods to rearrange the keys of + the different dictionaries and those are called right before writing the jsonld file. + + Those methods enforces a certain order or keys in the output and + also remove any empty or unknown keys. """ - def sort_schema(self, schema_order): + # TODO allow for unknown keys to be added because otherwise adding new fields in the json always + # forces you to go and change those `schema_order` and `ui_order` otherwise they will not be added. + # + # This will require modifying the helper function `reorder_dict_skip_missing` + # + def sort_schema(self, schema_order): + """ + The ``schema_order`` is specific to each "level" of the reproschema + (protocol, activity, item) so that each can be reordered in its own way + """ reordered_dict = reorder_dict_skip_missing(self.schema, schema_order) self.schema = reordered_dict def sort_ui(self, ui_order=["shuffle", "order", "addProperties", "allow"]): - reordered_dict = reorder_dict_skip_missing(self.schema["ui"], ui_order) self.schema["ui"] = reordered_dict def __write(self, output_dir): + """ + Reused by the write method of the children classes + """ with open(os.path.join(output_dir, self.schema_file), "w") as ff: json.dump(self.schema, ff, sort_keys=False, indent=4) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index d4b5b6c..b719c83 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -18,8 +18,15 @@ def __init__(self, version=None): super().__init__(version) self.schema["ui"] = {"inputType": []} self.schema["question"] = {} + """ + The responseOptions dictionnary is kept empty until the file has to be written + then it gets its content wit the method `set_response_options` + from the `options` dictionnary of an instance of the ResponseOptions class that is kept in + ``self.response_options`` + """ self.schema["responseOptions"] = {} self.response_options = ResponseOption() + # default input type is "text" self.set_input_type_as_text() @@ -29,48 +36,95 @@ def set_defaults(self, name="default"): self.set_input_type_as_text() def set_filename(self, name, ext=".jsonld"): + """ + Note there is no _schema suffix for items names + """ name = name.replace(" ", "_") self.schema_file = name + ext self.schema["@id"] = name + ext def set_question(self, question, lang=DEFAULT_LANG): + # TODO add test to check adding several questions to an item self.schema["question"][lang] = question """ - UI + CREATE DIFFERENT ITEMS """ + # TODO: items not yet covered + # audioCheck: AudioCheck/AudioCheck.vue + # audioRecord: WebAudioRecord/Audio.vue + # audioPassageRecord: WebAudioRecord/Audio.vue + # audioImageRecord: WebAudioRecord/Audio.vue + # audioRecordNumberTask: WebAudioRecord/Audio.vue + # audioAutoRecord: AudioCheckRecord/AudioCheckRecord.vue + # documentUpload: DocumentUpload/DocumentUpload.vue + # save: SaveData/SaveData.vue + # static: Static/Static.vue + # StaticReadOnly: Static/Static.vue - def set_input_type(self, input_type): - self.schema["ui"]["inputType"] = input_type + def set_basic_response_type(self, response_type): + """ + Handles the dispatching to other methods for specific item creations + Does not cover all items types (maybe it should as this would help + from an API point of view to pass everything through this function) + The default is "text" input type + """ + self.set_input_type_as_text() - def set_read_only_value(self, value): - self.schema["ui"]["readonlyValue"] = value + if response_type == "int": + self.set_input_type_as_int() - """ - RESPONSE CHOICES - """ + elif response_type == "float": + self.set_input_type_as_float() - def set_response_options(self): - self.schema["responseOptions"] = self.response_options.options + elif response_type == "date": + self.set_input_type_as_date() + + elif response_type == "time range": + self.set_input_type_as_time_range() + + elif response_type == "language": + self.set_input_type_as_language() """ input types with different response choices + + For many items it is necessary to call + + self.response_options.unset + + To remove unecessary or unwanted keys from the response_options + dictionary. + Many of those are put there by the constructor of that set + the default input type as ``self.set_input_type_as_text()`` + so it might be better to maybe have a more minimalistic constructor. + """ - def set_input_type_as_radio(self, response_options): - self.set_input_type("radio") - response_options.set_type("integer") - self.response_options = response_options + def set_input_type_as_int(self): + self.set_input_type("number") + self.response_options.set_type("integer") + self.response_options.unset(["maxLength"]) - def set_input_type_as_select(self, response_options): - self.set_input_type("select") - response_options.set_type("integer") - self.response_options = response_options + def set_input_type_as_float(self): + self.set_input_type("float") + self.response_options.set_type("float") + self.response_options.unset(["maxLength"]) - def set_input_type_as_slider(self, response_options): - self.set_input_type("slider") - response_options.set_type("integer") - self.response_options = response_options + def set_input_type_as_date(self): + self.set_input_type("date") + self.response_options.unset(["maxLength"]) + self.response_options.set_type("date") + + def set_input_type_as_time_range(self): + self.set_input_type("timeRange") + self.response_options.unset(["maxLength"]) + self.response_options.set_type("datetime") + + def set_input_type_as_year(self): + self.set_input_type("year") + self.response_options.unset(["maxLength"]) + self.response_options.set_type("date") """ input types with preset response choices @@ -107,35 +161,6 @@ def set_input_type_as_state(self): self.response_options.use_preset(URL) self.response_options.unset(["maxLength"]) - """ - input types with no response choice - """ - - def set_input_type_as_int(self): - self.set_input_type("number") - self.response_options.set_type("integer") - self.response_options.unset(["maxLength"]) - - def set_input_type_as_float(self): - self.set_input_type("float") - self.response_options.set_type("float") - self.response_options.unset(["maxLength"]) - - def set_input_type_as_time_range(self): - self.set_input_type("timeRange") - self.response_options.unset(["maxLength"]) - self.response_options.set_type("datetime") - - def set_input_type_as_date(self): - self.set_input_type("date") - self.response_options.unset(["maxLength"]) - self.response_options.set_type("date") - - def set_input_type_as_year(self): - self.set_input_type("year") - self.response_options.options.pop("maxLength", None) - self.response_options.set_type("date") - """ input types requiring user typed input """ @@ -158,46 +183,68 @@ def set_input_type_as_email(self): self.response_options.unset(["maxLength"]) def set_input_type_as_id(self): + """ + for participant id items + """ self.set_input_type("pid") self.response_options.unset(["maxLength"]) - # TODO - # audioCheck: AudioCheck/AudioCheck.vue - # audioRecord: WebAudioRecord/Audio.vue - # audioPassageRecord: WebAudioRecord/Audio.vue - # audioImageRecord: WebAudioRecord/Audio.vue - # audioRecordNumberTask: WebAudioRecord/Audio.vue - # audioAutoRecord: AudioCheckRecord/AudioCheckRecord.vue - # documentUpload: DocumentUpload/DocumentUpload.vue - # save: SaveData/SaveData.vue - # static: Static/Static.vue - # StaticReadOnly: Static/Static.vue + """ + input types with 'different response choices' - def set_basic_response_type(self, response_type): + Those methods require an instance of ResponseOptions as input and + it will replace the one initialized in the construction. - # default (also valid for "text" input type) - self.set_input_type_as_text() + Most likely a bad idea and a confusing API from the user perpective: + probably better to set the input type and then let the user construct + the response choices via calls to the methods of - if response_type == "int": - self.set_input_type_as_int() + self.response_options + """ - elif response_type == "float": - self.set_input_type_as_float() + def set_input_type_as_radio(self, response_options): + self.set_input_type("radio") + response_options.set_type("integer") + self.response_options = response_options - elif response_type == "date": - self.set_input_type_as_date() + def set_input_type_as_select(self, response_options): + self.set_input_type("select") + response_options.set_type("integer") + self.response_options = response_options - elif response_type == "time range": - self.set_input_type_as_time_range() + def set_input_type_as_slider(self, response_options): + self.set_input_type("slider") + response_options.set_type("integer") + self.response_options = response_options - elif response_type == "language": - self.set_input_type_as_language() + """ + UI + """ + # are input_type and read_only specific properties to items + # or should they be brought up into the base class? + # or be made part of an UI class? + + def set_input_type(self, input_type): + self.schema["ui"]["inputType"] = input_type + + def set_read_only_value(self, value): + self.schema["ui"]["readonlyValue"] = value """ writing, reading, sorting, unsetting """ + def set_response_options(self): + """ + Passes the content of the response options to the schema of the item. + To be done before writing the item + """ + self.schema["responseOptions"] = self.response_options.options + def unset(self, keys): + """ + Mostly used to remove some empty keys from the schema. Rarely used. + """ for i in keys: self.schema.pop(i, None) @@ -227,6 +274,13 @@ class ResponseOption(SchemaBase): class to deal with reproschema response options """ + # TODO + # the dictionnary that keeps track of the content of the response options should + # be called "schema" and not "options" so as to be able to make proper use of the + # methods of the parent class and avoid copying content between + # + # self.options and self.schema + schema_type = "reproschema:ResponseOption" def __init__(self): @@ -258,6 +312,9 @@ def unset(self, keys): def set_type(self, type): self.options["valueType"] = "xsd:" + type + # TODO a nice thing to do would be to read the min and max value + # from the rest of the content of self.options + # could avoid having the user to input those def set_min(self, value): self.options["minValue"] = value @@ -271,6 +328,10 @@ def set_multiple_choice(self, value): self.options["multipleChoice"] = value def use_preset(self, URI): + """ + In case the list response options are read from another file + like for languages, country, state... + """ self.options["choices"] = URI def add_choice(self, choice, value, lang=DEFAULT_LANG): @@ -298,7 +359,8 @@ def write(self, output_dir): # TODO # DUPLICATE from the base class to be used for ResponseOptions sorting of the options -# needs refactoring +# needs refactoring that will be made easier if the name the ResponseOptions dictionary is +# schema and note options from collections import OrderedDict diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index a2cc4be..dded1cd 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -11,17 +11,16 @@ class to deal with reproschema protocols schema_type = "reproschema:Protocol" def __init__(self, version=None): + """ + Rely on the parent class for construction of the instance + """ super().__init__(version) - self.schema["ui"] = { - "allow": [], - "shuffle": [], - "order": [], - "addProperties": [], - } def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) self.set_landing_page("README-en.md") + # does it make sense to give a preamble by default to protocols since + # they already have a landing page? self.set_preamble() self.set_ui_default() @@ -29,16 +28,26 @@ def set_landing_page(self, landing_page_uri, lang=DEFAULT_LANG): self.schema["landingPage"] = {"@id": landing_page_uri, "inLanguage": lang} def append_activity(self, activity): + """ + We get from an activity instance the info we need to update the protocol scheme. - append_to_protocol = { - "variableName": activity.get_basename().replace("_schema", ""), - "isAbout": activity.get_URI(), - "prefLabel": activity.get_pref_label(), - "isVis": True, - "valueRequired": False, - } + This appends the activity after all the other ones. + + So this means the order of the activities will be dependent + on the order in which they are "read". + + This implementation assumes that the activities are read + from a list and added one after the other. + """ + # TODO + # - find a way to reorder, remove or add an activity + # at any point in the protocol + # - this method is nearly identical to the append_item method of Activity + # and should probably be refactored into a single method of the parent class + # and ideally into a method of a yet to be created UI class property = { + # variable name is name of activity without prefix "variableName": activity.get_basename().replace("_schema", ""), "isAbout": activity.get_URI(), "prefLabel": activity.get_pref_label(), diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index 65bdb77..a5157d8 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -32,6 +32,9 @@ def test_default(): clean_up(item) +# TODO +# many items types (audio ones for examples) are not yet tested because the code cannot yet generate them. + """ text items """ From 580db6371e2418bfdfde3ffa77b814d518bda745 Mon Sep 17 00:00:00 2001 From: Sanu Ann Date: Mon, 7 Jun 2021 14:44:26 -0400 Subject: [PATCH 34/69] wip classes --- reproschema/models/activity.py | 82 +++++++++++++++++++- reproschema/models/activity_new.py | 74 ++++++++++++++++++ reproschema/models/base.py | 11 ++- reproschema/models/base_new.py | 90 ++++++++++++++++++++++ reproschema/models/protocol.py | 3 +- reproschema/models/protocol_new.py | 98 ++++++++++++++++++++++++ reproschema/models/tests/test_schema.py | 26 +++---- reproschema/models/tests/wtest_schema.py | 20 +++++ 8 files changed, 383 insertions(+), 21 deletions(-) create mode 100644 reproschema/models/activity_new.py create mode 100644 reproschema/models/base_new.py create mode 100644 reproschema/models/protocol_new.py create mode 100644 reproschema/models/tests/wtest_schema.py diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index 25d27d2..8199e35 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -1,7 +1,9 @@ -from .base import SchemaBase +# from reproschema.models.base_new import SchemaBase +import base_new +import attr - -class Activity(SchemaBase): +@attr.s(frozen=True) +class Activity(base_new.SchemaBase): """ class to deal with reproschema activities """ @@ -65,3 +67,77 @@ def sort(self): ui_order = ["shuffle", "order", "addProperties"] self.sort_ui(ui_order) + + +aa = Activity(prefLabel="ann", description='trial') +print('------------') +print(12, aa.__dict__.keys()) + + +# # from .base import SchemaBase +# import base +# +# class Activity(base.SchemaBase): +# """ +# class to deal with reproschema activities +# """ +# +# schema_type = "reproschema:Activity" +# +# def __init__(self, version=None): +# super().__init__(version) +# self.schema["ui"] = {"shuffle": [], "order": [], "addProperties": []} +# +# def set_ui_shuffle(self, shuffle=False): +# self.schema["ui"]["shuffle"] = shuffle +# +# def set_URI(self, URI): +# self.URI = URI +# +# def get_URI(self): +# return self.URI +# +# # TODO +# # preamble +# # compute +# # citation +# # image +# +# def set_defaults(self, name): +# self._ReproschemaSchema__set_defaults(name) # this looks wrong +# self.set_ui_shuffle(False) +# +# def update_activity(self, item_info): +# +# # TODO +# # - remove the hard coding on visibility and valueRequired +# +# # update the content of the activity schema with new item +# +# item_info["URI"] = "items/" + item_info["name"] +# +# append_to_activity = { +# "variableName": item_info["name"], +# "isAbout": item_info["URI"], +# "isVis": item_info["visibility"], +# "valueRequired": False, +# } +# +# self.schema["ui"]["order"].append(item_info["URI"]) +# self.schema["ui"]["addProperties"].append(append_to_activity) +# +# def sort(self): +# schema_order = [ +# "@context", +# "@type", +# "@id", +# "prefLabel", +# "description", +# "schemaVersion", +# "version", +# "ui", +# ] +# self.sort_schema(schema_order) +# +# ui_order = ["shuffle", "order", "addProperties"] +# self.sort_ui(ui_order) diff --git a/reproschema/models/activity_new.py b/reproschema/models/activity_new.py new file mode 100644 index 0000000..7afb356 --- /dev/null +++ b/reproschema/models/activity_new.py @@ -0,0 +1,74 @@ +# from reproschema.models.base_new import SchemaBase +import base_new +import attr + +@attr.s(frozen=True) +class Activity(base_new.SchemaBase): + """ + class to deal with reproschema activities + """ + + schema_type = "reproschema:Activity" + + def __init__(self, version=None): + super().__init__(version) + self.schema["ui"] = {"shuffle": [], "order": [], "addProperties": []} + + def set_ui_shuffle(self, shuffle=False): + self.schema["ui"]["shuffle"] = shuffle + + def set_URI(self, URI): + self.URI = URI + + def get_URI(self): + return self.URI + + # TODO + # preamble + # compute + # citation + # image + + def set_defaults(self, name): + self._ReproschemaSchema__set_defaults(name) # this looks wrong + self.set_ui_shuffle(False) + + def update_activity(self, item_info): + + # TODO + # - remove the hard coding on visibility and valueRequired + + # update the content of the activity schema with new item + + item_info["URI"] = "items/" + item_info["name"] + + append_to_activity = { + "variableName": item_info["name"], + "isAbout": item_info["URI"], + "isVis": item_info["visibility"], + "valueRequired": False, + } + + self.schema["ui"]["order"].append(item_info["URI"]) + self.schema["ui"]["addProperties"].append(append_to_activity) + + def sort(self): + schema_order = [ + "@context", + "@type", + "@id", + "prefLabel", + "description", + "schemaVersion", + "version", + "ui", + ] + self.sort_schema(schema_order) + + ui_order = ["shuffle", "order", "addProperties"] + self.sort_ui(ui_order) + + +aa = Activity(prefLabel="ann", description='trial') +print('------------') +print(12, aa.__dict__.keys()) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index a2d59db..c44c4e4 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -3,9 +3,6 @@ class SchemaBase: - """ - class to deal with reproschema schemas - """ schema_type = None @@ -56,7 +53,8 @@ def sort_ui(self, ui_order): reordered_dict = {k: self.schema["ui"][k] for k in ui_order} self.schema["ui"] = reordered_dict - def write(self, output_dir): + def write(self, output_dir, file_name): + self.set_filename(file_name) with open(os.path.join(output_dir, self.schema_file), "w") as ff: json.dump(self.schema, ff, sort_keys=False, indent=4) @@ -77,3 +75,8 @@ def from_file(cls, filepath): if "@type" not in data: raise ValueError("Missing @type key") return cls.from_data(data) + + +# aa = SchemaBase(version="0.0.1") +# aa.x = 'abc' +# print(aa.__dict__) \ No newline at end of file diff --git a/reproschema/models/base_new.py b/reproschema/models/base_new.py new file mode 100644 index 0000000..3467f6c --- /dev/null +++ b/reproschema/models/base_new.py @@ -0,0 +1,90 @@ +import json +import os +import attr + +# SchemaBase = attr.make_class("SchemaBase", +# {"prefLabel": attr.ib(kw_only=True, validator=attr.validators.instance_of(str)), +# "description": attr.ib(default=None, +# validator=attr.validators.optional(attr.validators.instance_of(str))), +# "schemaVersion": attr.ib(default="1.0.0-rc4", validator=attr.validators.instance_of(str)), +# "version": attr.ib(default="0.0.1", validator=attr.validators.instance_of(str)), +# "schema_type": attr.ib(default=None, kw_only=True) +# }) + +@attr.s(frozen=True) +class SchemaBase: + prefLabel = attr.ib(kw_only=True, validator=attr.validators.instance_of(str)) + description = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(str))) + schemaVersion = attr.ib(default="1.0.0-rc4", validator=attr.validators.instance_of(str)) + version = attr.ib(default="0.0.1", validator=attr.validators.instance_of(str)) + # schema_type = attr.ib(default=None, kw_only=True) + # schema = attr.ib(default={ + # "@context": "https://raw.githubusercontent.com/ReproNim/reproschema//contexts/generic", + # "@type": 'None', + # }) + # order = attr.ib(validator=attr.validators.deep_iterable( + # member_validator = attr.validators.instance_of(str), + # iterable_validator = attr.validators.instance_of(list) + # )) + + # def set_filename(self, name): + # self.schema_file = name + "_schema" + # self.schema["@id"] = name + "_schema" + + def get_name(self): + return self.schema_file.replace("_schema", "") + + def get_filename(self): + return self.schema_file + + def __set_defaults(self, name): + self.set_filename(name) + self.set_directory(name) + self.set_pref_label(name.replace("_", " ")) + self.set_description(name.replace("_", " ")) # description isn't mandatory, no? + + def sort_schema(self, schema_order): + + reordered_dict = {k: self.schema[k] for k in schema_order} + self.schema = reordered_dict + + def sort_ui(self, ui_order): + + reordered_dict = {k: self.schema["ui"][k] for k in ui_order} + self.schema["ui"] = reordered_dict + + # def write(self, output_dir, filename): + # # self.schema_file = filename + "_schema" + # self.schema["@id"] = filename + # props = aa.__dict__.copy() + # del props['schema'] + # props.update(self.schema) + # with open(os.path.join(output_dir, filename), "w") as ff: + # json.dump(props, ff, sort_keys=True, indent=4) + + @classmethod + # def from_data(cls, data): + # if cls.schema_type is None: + # raise ValueError("SchemaBase cannot be used to instantiate class") + # if cls.schema_type != data["@type"]: + # raise ValueError(f"Mismatch in type {data['@type']} != {cls.schema_type}") + # klass = cls() + # klass.schema = data + # return klass + + @classmethod + def from_file(cls, filepath): + with open(filepath) as fp: + data = json.load(fp) + if "@type" not in data: + raise ValueError("Missing @type key") + return cls.from_data(data) + + +# aa = SchemaBase(prefLabel="ann", description='trial') +# print('------------') +# print(12, aa.__dict__.keys()) +# props = aa.__dict__.copy() +# # del props['schema'] +# # print(13, props) +# print(aa.write('./', 'tt_schema')) diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index c630914..bb3be6b 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -15,6 +15,7 @@ def __init__(self, version=None): "shuffle": [], "order": [], "addProperties": [], + "overrideProperties": [], } def set_landing_page(self, landing_page_url, lang="en"): @@ -74,5 +75,5 @@ def sort(self): ] self.sort_schema(schema_order) - ui_order = ["allow", "shuffle", "order", "addProperties"] + ui_order = ["allow", "shuffle", "order", "addProperties", "overrideProperties"] self.sort_ui(ui_order) diff --git a/reproschema/models/protocol_new.py b/reproschema/models/protocol_new.py new file mode 100644 index 0000000..799f06a --- /dev/null +++ b/reproschema/models/protocol_new.py @@ -0,0 +1,98 @@ +import attr +# from .base import SchemaBase + + +@attr.s() +class Protocol(SchemaBase): + """ + class to deal with reproschema protocols + """ + + schema_type = "reproschema:Protocol" + # schema_type = attr.ib(default=None, kw_only=True) + + # ui = attr.ib(default={ + # "allow": attr.ib(default=attr.Factory(list)), + # "shuffle": attr.ib(default=False), + # "order": attr.ib(default=attr.Factory(list)), + # "addProperties": attr.ib(default=attr.Factory(list)), + # "overrideProperties": attr.ib(default=attr.Factory(list)), + # }) + # def __init__(self, version=None): + # super().__init__(version) + # self.schema["ui"] = { + # "allow": [], + # "shuffle": [], + # "order": [], + # "addProperties": [], + # "overrideProperties": [], + # } + + def set_landing_page(self, landing_page_url, lang="en"): + self.schema["landingPage"] = {"@id": landing_page_url, "@language": lang} + + # TODO + # def add_landing_page(self, landing_page_url, lang="en"): + # preamble + # compute + + def set_image(self, image_url): + self.schema["image"] = image_url + + def set_ui_allow(self): + self.schema["ui"]["allow"] = [ + "reproschema:AutoAdvance", + "reproschema:AllowExport", + ] + + # def set_ui_shuffle(self, shuffle=False): + # self.schema["ui"]["shuffle"] = shuffle + + def set_defaults(self, name): + self._ReproschemaSchema__set_defaults(name) # this looks wrong + self.set_landing_page("../../README-en.md") + self.set_ui_allow() + self.set_ui_shuffle(False) + + def append_activity(self, activity): + + # TODO + # - remove the hard coding on visibility and valueRequired + + # update the content of the protocol with this new activity + append_to_protocol = { + "variableName": activity.get_name(), + "isAbout": activity.get_URI(), + "prefLabel": {"en": activity.schema["prefLabel"]}, + "isVis": True, + "valueRequired": False, + # schedule, randomMaxDelay, limit + } + + self.schema["ui"]["order"].append(activity.URI) + self.schema["ui"]["addProperties"].append(append_to_protocol) + + def sort(self): + schema_order = [ + "@context", + "@type", + "@id", + "prefLabel", + "description", + "schemaVersion", + "version", + "landingPage", + "ui", + ] + self.sort_schema(schema_order) + + ui_order = ["allow", "shuffle", "order", "addProperties", "overrideProperties"] + self.sort_ui(ui_order) + +# pp = Protocol(prefLabel="ann", description='trial') +# print('------------') +# print(12, pp.__dict__.keys()) +# props = pp.__dict__.copy() +# del props['schema'] +# print(13, props) +# print(pp.write('./', 'tt_schema')) diff --git a/reproschema/models/tests/test_schema.py b/reproschema/models/tests/test_schema.py index a68e808..a3de3c5 100644 --- a/reproschema/models/tests/test_schema.py +++ b/reproschema/models/tests/test_schema.py @@ -1,20 +1,20 @@ -from .. import Protocol, Activity, Item - +# from .. import Protocol, Activity, Item +from .. import activity, protocol, item def test_constructors(): - Protocol() - Activity() - Item() - version = "1.0.0-rc2" - proto = Protocol(version=version) + protocol.Protocol() + activity.Activity() + + version = "1.0.0-rc4" + proto = protocol.Protocol(version=version) assert proto.schema["schemaVersion"] == version - act = Activity(version) + act = activity.Activity(version) assert act.schema["schemaVersion"] == version - item = Item(version) - assert item.schema["schemaVersion"] == version + # item = item.Item(version) + # assert item.schema["schemaVersion"] == version def test_constructors_from_data(): - Protocol.from_data({"@type": "reproschema:Protocol"}) - Activity.from_data({"@type": "reproschema:Activity"}) - Item.from_data({"@type": "reproschema:Field"}) + protocol.Protocol.from_data({"@type": "reproschema:Protocol"}) + activity.Activity.from_data({"@type": "reproschema:Activity"}) + # item.Item.from_data({"@type": "reproschema:Field"}) diff --git a/reproschema/models/tests/wtest_schema.py b/reproschema/models/tests/wtest_schema.py new file mode 100644 index 0000000..a68e808 --- /dev/null +++ b/reproschema/models/tests/wtest_schema.py @@ -0,0 +1,20 @@ +from .. import Protocol, Activity, Item + + +def test_constructors(): + Protocol() + Activity() + Item() + version = "1.0.0-rc2" + proto = Protocol(version=version) + assert proto.schema["schemaVersion"] == version + act = Activity(version) + assert act.schema["schemaVersion"] == version + item = Item(version) + assert item.schema["schemaVersion"] == version + + +def test_constructors_from_data(): + Protocol.from_data({"@type": "reproschema:Protocol"}) + Activity.from_data({"@type": "reproschema:Activity"}) + Item.from_data({"@type": "reproschema:Field"}) From 953f1c9ca286f3885d65f0ca42af99d66ff913ce Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 7 Jun 2021 20:47:17 +0200 Subject: [PATCH 35/69] add more todo to item class --- reproschema/models/item.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index b719c83..6165773 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -335,6 +335,14 @@ def use_preset(self, URI): self.options["choices"] = URI def add_choice(self, choice, value, lang=DEFAULT_LANG): + """ + Add a response option + """ + # TODO currently a certain response option can only be appended, + # not removed, no edited + # Also the current implementation with languages makes it hard for example + # to query what is the value or question associated with a certain language + # The nested structure should probably be flattened. self.options["choices"].append({"name": {lang: choice}, "value": value}) def sort(self): From e3e3e980d085f9a7ee367e8ca45766c191df1ef1 Mon Sep 17 00:00:00 2001 From: Sanu Ann Date: Tue, 8 Jun 2021 11:34:20 -0400 Subject: [PATCH 36/69] refactor classes to add remi's changes --- reproschema/models/activity.py | 154 ++++--------- reproschema/models/base.py | 234 +++++++++++++++++--- reproschema/models/item.py | 389 +++++++++++++++++++++++++-------- reproschema/models/protocol.py | 93 ++++---- 4 files changed, 600 insertions(+), 270 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index 8199e35..c0f83e1 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -1,56 +1,50 @@ -# from reproschema.models.base_new import SchemaBase -import base_new -import attr +from .base import SchemaBase -@attr.s(frozen=True) -class Activity(base_new.SchemaBase): +DEFAULT_LANG = "en" + + +class Activity(SchemaBase): """ class to deal with reproschema activities """ schema_type = "reproschema:Activity" + visible = True + required = False + skippable = True + def __init__(self, version=None): super().__init__(version) - self.schema["ui"] = {"shuffle": [], "order": [], "addProperties": []} - - def set_ui_shuffle(self, shuffle=False): - self.schema["ui"]["shuffle"] = shuffle - - def set_URI(self, URI): - self.URI = URI - - def get_URI(self): - return self.URI - - # TODO - # preamble - # compute - # citation - # image - - def set_defaults(self, name): - self._ReproschemaSchema__set_defaults(name) # this looks wrong - self.set_ui_shuffle(False) - - def update_activity(self, item_info): - # TODO - # - remove the hard coding on visibility and valueRequired + def set_defaults(self, name="default"): + self._SchemaBase__set_defaults(name) + self.set_preamble() + self.set_ui_default() - # update the content of the activity schema with new item + def set_compute(self, variable, expression): + self.schema["compute"] = [ + {"variableName": variable, "jsExpression": expression} + ] - item_info["URI"] = "items/" + item_info["name"] + def append_item(self, item): + # See comment and TODO of the append_activity Protocol class - append_to_activity = { - "variableName": item_info["name"], - "isAbout": item_info["URI"], - "isVis": item_info["visibility"], - "valueRequired": False, + property = { + "variableName": item.get_basename(), + "isAbout": item.get_URI(), + "isVis": item.visible, + "requiredValue": item.required, } + if item.skippable: + property["allow"] = ["reproschema:Skipped"] + + self.schema["ui"]["order"].append(item.get_URI()) + self.schema["ui"]["addProperties"].append(property) - self.schema["ui"]["order"].append(item_info["URI"]) - self.schema["ui"]["addProperties"].append(append_to_activity) + """ + writing, reading, sorting, unsetting + """ def sort(self): schema_order = [ @@ -61,83 +55,15 @@ def sort(self): "description", "schemaVersion", "version", + "preamble", + "citation", + "image", + "compute", "ui", ] self.sort_schema(schema_order) + self.sort_ui() - ui_order = ["shuffle", "order", "addProperties"] - self.sort_ui(ui_order) - - -aa = Activity(prefLabel="ann", description='trial') -print('------------') -print(12, aa.__dict__.keys()) - - -# # from .base import SchemaBase -# import base -# -# class Activity(base.SchemaBase): -# """ -# class to deal with reproschema activities -# """ -# -# schema_type = "reproschema:Activity" -# -# def __init__(self, version=None): -# super().__init__(version) -# self.schema["ui"] = {"shuffle": [], "order": [], "addProperties": []} -# -# def set_ui_shuffle(self, shuffle=False): -# self.schema["ui"]["shuffle"] = shuffle -# -# def set_URI(self, URI): -# self.URI = URI -# -# def get_URI(self): -# return self.URI -# -# # TODO -# # preamble -# # compute -# # citation -# # image -# -# def set_defaults(self, name): -# self._ReproschemaSchema__set_defaults(name) # this looks wrong -# self.set_ui_shuffle(False) -# -# def update_activity(self, item_info): -# -# # TODO -# # - remove the hard coding on visibility and valueRequired -# -# # update the content of the activity schema with new item -# -# item_info["URI"] = "items/" + item_info["name"] -# -# append_to_activity = { -# "variableName": item_info["name"], -# "isAbout": item_info["URI"], -# "isVis": item_info["visibility"], -# "valueRequired": False, -# } -# -# self.schema["ui"]["order"].append(item_info["URI"]) -# self.schema["ui"]["addProperties"].append(append_to_activity) -# -# def sort(self): -# schema_order = [ -# "@context", -# "@type", -# "@id", -# "prefLabel", -# "description", -# "schemaVersion", -# "version", -# "ui", -# ] -# self.sort_schema(schema_order) -# -# ui_order = ["shuffle", "order", "addProperties"] -# self.sort_ui(ui_order) + def write(self, output_dir): + self.sort() + self._SchemaBase__write(output_dir) \ No newline at end of file diff --git a/reproschema/models/base.py b/reproschema/models/base.py index c44c4e4..df21015 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -1,26 +1,145 @@ import json import os +from pathlib import Path +from collections import OrderedDict + +""" +For any key that can be 'translated' we set english as the default language +in case the user does not provide it. +""" +DEFAULT_LANG = "en" + +""" +""" +DEFAULT_VERSION = "1.0.0-rc4" + + +def default_context(version): + """ + For now we assume that the github repo will be where schema will be read from. + """ + URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" + VERSION = version or DEFAULT_VERSION + return URL + VERSION + "/contexts/generic" class SchemaBase: + """ + base class to deal with reproschema schemas + + The content of the schema is stored in the dictionnary ``self.schema`` + + self.schema["@context"] + self.schema["@type"] + self.schema["schemaVersion"] + self.schema["version"] + self.schema["preamble"] + self.schema["citation"] + self.schema["image"] + ... + + When the output file is created, its content is simply dumped into a json + by the ``write`` method. + + """ + + # TODO might be more convenient to have some of the properties not centrlized in a single dictionnary + # + # Could be more practical to only create part or all of the dictionnary when write is called + # schema_type = None def __init__(self, version): - URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" - VERSION = version or "1.0.0-rc2" + # TODO the version handling could probably be refactored + VERSION = version or DEFAULT_VERSION self.schema = { - "@context": URL + VERSION + "/contexts/generic", "@type": self.schema_type, "schemaVersion": VERSION, "version": "0.0.1", } - def set_filename(self, name): - self.schema_file = name + "_schema" - self.schema["@id"] = name + "_schema" + URL = self.get_default_context(version) + self.set_context(URL) + + # This probably needs some cleaning but is at the moment necessary to pass + # the context to the ResponseOption class + def get_default_context(self, version): + return default_context(version) + + def __set_defaults(self, name): + self.set_filename(name) + self.set_directory(name) + "We use the ``name`` of this class instance for schema keys minus some underscore" + self.set_pref_label(name.replace("_", " ")) + self.set_description(name.replace("_", " ")) + + """ + setters + """ + + def set_directory(self, output_directory): + """ + Where the file will be written by the ``write`` method + """ + self.dir = output_directory + + def set_URI(self, URI): + """ + In case we need to keep where the output file is located, + we can set it with this method. + This can be useful if we have just read or created an item + and want to add it to an activity. + """ + self.URI = URI + + """ + schema related setters + + use them to set several of the common keys of the schema + """ + + def set_context(self, context): + """ + In case we want to overide the default context + """ + self.schema["@context"] = context + + def set_preamble(self, preamble="", lang=DEFAULT_LANG): + self.schema["preamble"] = {lang: preamble} + + def set_citation(self, citation): + self.schema["citation"] = citation + + def set_image(self, image): + self.schema["image"] = image + + def set_filename(self, name, ext=".jsonld"): + """ + By default all files are given: + - the ``.jsold`` extension + - have ``_schema`` suffix appended to them + + For item files their name won't have the schema prefix. + """ + # TODO figure out if the latter is a desirable behavior + self.schema_file = name + "_schema" + ext + self.schema["@id"] = name + "_schema" + ext + + def set_pref_label(self, pref_label, lang=DEFAULT_LANG): + self.schema["prefLabel"] = {lang: pref_label} + + def set_description(self, description): + self.schema["description"] = description + + """ + getters + + to access the content of some of the keys of the schema + or some of the instance properties + """ def get_name(self): return self.schema_file.replace("_schema", "") @@ -28,33 +147,90 @@ def get_name(self): def get_filename(self): return self.schema_file - def set_pref_label(self, pref_label): - self.schema["prefLabel"] = pref_label + def get_basename(self): + return Path(self.schema_file).stem - def set_description(self, description): - self.schema["description"] = description + def get_pref_label(self): + return self.schema["prefLabel"] - def set_directory(self, output_directory): - self.dir = output_directory + def get_URI(self): + return self.URI - def __set_defaults(self, name): - self.set_filename(name) - self.set_directory(name) - self.set_pref_label(name.replace("_", " ")) - self.set_description(name.replace("_", " ")) + """ + UI: setters specific to the user interface keys of the schema - def sort_schema(self, schema_order): + The ui has its own dictionnary. - reordered_dict = {k: self.schema[k] for k in schema_order} - self.schema = reordered_dict + Mostly used by the Protocol and Activity class. + + """ + + def set_ui_default(self): + self.schema["ui"] = { + "shuffle": [], + "order": [], + "addProperties": [], + "allow": [], + } + self.set_ui_shuffle() + self.set_ui_allow() + + def set_ui_shuffle(self, shuffle=False): + self.schema["ui"]["shuffle"] = shuffle - def sort_ui(self, ui_order): + def set_ui_allow(self, auto_advance=True, allow_export=True, disable_back=False): + # TODO + # Could be more convenient to have one method for each property + # + # Also currently the way this is handled makes it hard to update a single value: + # all 3 have to be reset everytime!!! + # + # Could be useful to have a UI class with dictionnary content that is only + # generated when the file is written + allow = [] + if auto_advance: + allow.append("reproschema:AutoAdvance") + if allow_export: + allow.append("reproschema:AllowExport") + if disable_back: + allow.append("reproschema:DisableBack") + self.schema["ui"]["allow"] = allow + + """ + writing, reading, sorting, unsetting + + Editing and appending things to the dictionnary tends to give json output + that is not standardized: the @context can end up at the bottom for one file + and stay at the top for another. + So there are a couple of sorting methods to rearrange the keys of + the different dictionaries and those are called right before writing the jsonld file. + + Those methods enforces a certain order or keys in the output and + also remove any empty or unknown keys. + """ + + # TODO allow for unknown keys to be added because otherwise adding new fields in the json always + # forces you to go and change those `schema_order` and `ui_order` otherwise they will not be added. + # + # This will require modifying the helper function `reorder_dict_skip_missing` + # + + def sort_schema(self, schema_order): + """ + The ``schema_order`` is specific to each "level" of the reproschema + (protocol, activity, item) so that each can be reordered in its own way + """ + reordered_dict = reorder_dict_skip_missing(self.schema, schema_order) + self.schema = reordered_dict - reordered_dict = {k: self.schema["ui"][k] for k in ui_order} + def sort_ui(self, ui_order=["shuffle", "order", "addProperties", "allow"]): + reordered_dict = reorder_dict_skip_missing(self.schema["ui"], ui_order) self.schema["ui"] = reordered_dict - def write(self, output_dir, file_name): - self.set_filename(file_name) + def __write(self, output_dir): + """ + Reused by the write method of the children classes + """ with open(os.path.join(output_dir, self.schema_file), "w") as ff: json.dump(self.schema, ff, sort_keys=False, indent=4) @@ -77,6 +253,10 @@ def from_file(cls, filepath): return cls.from_data(data) -# aa = SchemaBase(version="0.0.1") -# aa.x = 'abc' -# print(aa.__dict__) \ No newline at end of file +def reorder_dict_skip_missing(old_dict, key_list): + """ + reorders dictionary according to ``key_list`` + removing any key with no associated value + or that is not in the key list + """ + return OrderedDict((k, old_dict[k]) for k in key_list if k in old_dict) \ No newline at end of file diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 595d67d..6ccbd63 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -1,146 +1,257 @@ from .base import SchemaBase +DEFAULT_LANG = "en" + + class Item(SchemaBase): """ - class to deal with reproschema activities + class to deal with reproschema items """ schema_type = "reproschema:Field" + visible = True + required = False + skippable = True def __init__(self, version=None): super().__init__(version) self.schema["ui"] = {"inputType": []} self.schema["question"] = {} + """ + The responseOptions dictionnary is kept empty until the file has to be written + then it gets its content wit the method `set_response_options` + from the `options` dictionnary of an instance of the ResponseOptions class that is kept in + ``self.response_options`` + """ self.schema["responseOptions"] = {} - # default input type is "char" - self.set_input_type_as_char() + self.response_options = ResponseOption() + + # default input type is "text" + self.set_input_type_as_text() + + def set_defaults(self, name="default"): + self._SchemaBase__set_defaults(name) + self.set_filename(name) + self.set_input_type_as_text() + + def set_filename(self, name, ext=".jsonld"): + """ + Note there is no _schema suffix for items names + """ + name = name.replace(" ", "_") + self.schema_file = name + ext + self.schema["@id"] = name + ext + + def set_question(self, question, lang=DEFAULT_LANG): + # TODO add test to check adding several questions to an item + self.schema["question"][lang] = question - def set_URI(self, URI): - self.URI = URI + """ + CREATE DIFFERENT ITEMS + """ + # TODO: items not yet covered + # audioCheck: AudioCheck/AudioCheck.vue + # audioRecord: WebAudioRecord/Audio.vue + # audioPassageRecord: WebAudioRecord/Audio.vue + # audioImageRecord: WebAudioRecord/Audio.vue + # audioRecordNumberTask: WebAudioRecord/Audio.vue + # audioAutoRecord: AudioCheckRecord/AudioCheckRecord.vue + # documentUpload: DocumentUpload/DocumentUpload.vue + # save: SaveData/SaveData.vue + # static: Static/Static.vue + # StaticReadOnly: Static/Static.vue - # TODO - # image - # readonlyValue + def set_basic_response_type(self, response_type): + """ + Handles the dispatching to other methods for specific item creations + Does not cover all items types (maybe it should as this would help + from an API point of view to pass everything through this function) + The default is "text" input type + """ + self.set_input_type_as_text() - def set_defaults(self, name): - self._ReproschemaSchema__set_defaults(name) # this looks wrong - self.schema_file = name - self.schema["@id"] = name - self.set_input_type_as_char() + if response_type == "int": + self.set_input_type_as_int() - def set_question(self, question, lang="en"): - self.schema["question"][lang] = question + elif response_type == "float": + self.set_input_type_as_float() - def set_input_type(self, input_type): - self.schema["ui"]["inputType"] = input_type + elif response_type == "date": + self.set_input_type_as_date() - def set_response_options(self, response_options): - self.schema["responseOptions"] = response_options + elif response_type == "time range": + self.set_input_type_as_time_range() - """ + elif response_type == "language": + self.set_input_type_as_language() + """ input types with different response choices + For many items it is necessary to call + + self.response_options.unset + + To remove unecessary or unwanted keys from the response_options + dictionary. + Many of those are put there by the constructor of that set + the default input type as ``self.set_input_type_as_text()`` + so it might be better to maybe have a more minimalistic constructor. + """ - def set_input_type_as_radio(self, response_options): - self.set_input_type("radio") - self.set_response_options(response_options) + def set_input_type_as_int(self): + self.set_input_type("number") + self.response_options.set_type("integer") + self.response_options.unset(["maxLength"]) - def set_input_type_as_select(self, response_options): - self.set_input_type("select") - self.set_response_options(response_options) + def set_input_type_as_float(self): + self.set_input_type("float") + self.response_options.set_type("float") + self.response_options.unset(["maxLength"]) - def set_input_type_as_slider(self): - self.set_input_type_as_char() # until the slide item of the ui is fixed - # self.set_input_type("slider") - # self.set_response_options({"valueType": "xsd:string"}) + def set_input_type_as_date(self): + self.set_input_type("date") + self.response_options.unset(["maxLength"]) + self.response_options.set_type("date") + + def set_input_type_as_time_range(self): + self.set_input_type("timeRange") + self.response_options.unset(["maxLength"]) + self.response_options.set_type("datetime") + + def set_input_type_as_year(self): + self.set_input_type("year") + self.response_options.unset(["maxLength"]) + self.response_options.set_type("date") + + """ + input types with preset response choices + """ def set_input_type_as_language(self): - URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" + URL = "https://raw.githubusercontent.com/ReproNim/reproschema-library/" self.set_input_type("selectLanguage") - response_options = { - "valueType": "xsd:string", - "multipleChoice": True, - "choices": URL + "master/resources/languages.json", - } - self.set_response_options(response_options) + self.response_options.set_type("string") + self.response_options.set_multiple_choice(True) + self.response_options.use_preset(URL + "master/resources/languages.json") + self.response_options.unset(["maxLength"]) - """ + def set_input_type_as_country(self): - input types with no response choice + URL = "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json" - """ + self.set_input_type("selectCountry") - def set_input_type_as_char(self): - self.set_input_type("text") - self.set_response_options({"valueType": "xsd:string"}) + self.response_options.set_type("string") + self.response_options.use_preset(URL) + self.response_options.set_length(50) - def set_input_type_as_int(self): - self.set_input_type("number") - self.set_response_options({"valueType": "xsd:integer"}) + def set_input_type_as_state(self): - def set_input_type_as_float(self): - self.set_input_type("float") - self.set_response_options({"valueType": "xsd:float"}) + URL = "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json" - def set_input_type_as_time_range(self): - self.set_input_type("timeRange") - self.set_response_options({"valueType": "datetime"}) + self.set_input_type("selectState") - def set_input_type_as_date(self): - self.set_input_type("date") - self.set_response_options({"valueType": "xsd:date"}) + self.response_options.set_type("string") + self.response_options.use_preset(URL) + self.response_options.unset(["maxLength"]) """ + input types requiring user typed input + """ - input types with no response choice but with some parameters + def set_input_type_as_text(self, length=300): + self.set_input_type("text") + self.response_options.set_type("string") + self.response_options.set_length(length) + self.response_options.unset( + ["maxValue", "minValue", "multipleChoice", "choices"] + ) + + def set_input_type_as_multitext(self, length=300): + self.set_input_type("multitext") + self.response_options.set_type("string") + self.response_options.set_length(length) + + def set_input_type_as_email(self): + self.set_input_type("email") + self.response_options.unset(["maxLength"]) + + def set_input_type_as_id(self): + """ + for participant id items + """ + self.set_input_type("pid") + self.response_options.unset(["maxLength"]) """ + input types with 'different response choices' - def set_input_type_as_multitext(self, max_length=300): - self.set_input_type("text") - self.set_response_options({"valueType": "xsd:string", "maxLength": max_length}) + Those methods require an instance of ResponseOptions as input and + it will replace the one initialized in the construction. - # TODO - # email: EmailInput/EmailInput.vue - # audioCheck: AudioCheck/AudioCheck.vue - # audioRecord: WebAudioRecord/Audio.vue - # audioPassageRecord: WebAudioRecord/Audio.vue - # audioImageRecord: WebAudioRecord/Audio.vue - # audioRecordNumberTask: WebAudioRecord/Audio.vue - # audioAutoRecord: AudioCheckRecord/AudioCheckRecord.vue - # year: YearInput/YearInput.vue - # selectCountry: SelectInput/SelectInput.vue - # selectState: SelectInput/SelectInput.vue - # documentUpload: DocumentUpload/DocumentUpload.vue - # save: SaveData/SaveData.vue - # static: Static/Static.vue - # StaticReadOnly: Static/Static.vue + Most likely a bad idea and a confusing API from the user perpective: + probably better to set the input type and then let the user construct + the response choices via calls to the methods of - def set_basic_response_type(self, response_type): + self.response_options + """ - # default (also valid for "char" input type) - self.set_input_type_as_char() + def set_input_type_as_radio(self, response_options): + self.set_input_type("radio") + response_options.set_type("integer") + self.response_options = response_options - if response_type == "int": - self.set_input_type_as_int() + def set_input_type_as_select(self, response_options): + self.set_input_type("select") + response_options.set_type("integer") + self.response_options = response_options - elif response_type == "float": - self.set_input_type_as_float() + def set_input_type_as_slider(self, response_options): + self.set_input_type("slider") + response_options.set_type("integer") + self.response_options = response_options - elif response_type == "date": - self.set_input_type_as_date() + """ + UI + """ + # are input_type and read_only specific properties to items + # or should they be brought up into the base class? + # or be made part of an UI class? - elif response_type == "time range": - self.set_input_type_as_time_range() + def set_input_type(self, input_type): + self.schema["ui"]["inputType"] = input_type - elif response_type == "language": - self.set_input_type_as_language() + def set_read_only_value(self, value): + self.schema["ui"]["readonlyValue"] = value + + """ + writing, reading, sorting, unsetting + """ + + def set_response_options(self): + """ + Passes the content of the response options to the schema of the item. + To be done before writing the item + """ + self.schema["responseOptions"] = self.response_options.options + + def unset(self, keys): + """ + Mostly used to remove some empty keys from the schema. Rarely used. + """ + for i in keys: + self.schema.pop(i, None) + + def write(self, output_dir): + self.sort() + self.set_response_options() + self._SchemaBase__write(output_dir) def sort(self): schema_order = [ @@ -156,3 +267,107 @@ def sort(self): "responseOptions", ] self.sort_schema(schema_order) + + +class ResponseOption(SchemaBase): + """ + class to deal with reproschema response options + """ + + # TODO + # the dictionnary that keeps track of the content of the response options should + # be called "schema" and not "options" so as to be able to make proper use of the + # methods of the parent class and avoid copying content between + # + # self.options and self.schema + + schema_type = "reproschema:ResponseOption" + + def __init__(self): + self.options = { + "valueType": "", + "minValue": 0, + "maxValue": 0, + "choices": [], + "multipleChoice": False, + } + + def set_defaults(self, name="valueConstraints", version=None): + super().__init__(version) + self.options["@context"] = self.schema["@context"] + self.options["@type"] = self.schema_type + self.set_filename(name) + + def set_filename(self, name, ext=".jsonld"): + name = name.replace(" ", "_") + self.schema_file = name + ext + self.options["@id"] = name + ext + + def unset(self, keys): + if type(keys) == str: + keys = [keys] + for i in keys: + self.options.pop(i, None) + + def set_type(self, type): + self.options["valueType"] = "xsd:" + type + + # TODO a nice thing to do would be to read the min and max value + # from the rest of the content of self.options + # could avoid having the user to input those + def set_min(self, value): + self.options["minValue"] = value + + def set_max(self, value): + self.options["maxValue"] = value + + def set_length(self, value): + self.options["maxLength"] = value + + def set_multiple_choice(self, value): + self.options["multipleChoice"] = value + + def use_preset(self, URI): + """ + In case the list response options are read from another file + like for languages, country, state... + """ + self.options["choices"] = URI + + def add_choice(self, choice, value, lang=DEFAULT_LANG): + self.options["choices"].append({"name": {lang: choice}, "value": value}) + + def sort(self): + options_order = [ + "@context", + "@type", + "@id", + "valueType", + "minValue", + "maxValue", + "multipleChoice", + "choices", + ] + reordered_dict = reorder_dict_skip_missing(self.options, options_order) + self.options = reordered_dict + + def write(self, output_dir): + self.sort() + self.schema = self.options + self._SchemaBase__write(output_dir) + + +# TODO +# DUPLICATE from the base class to be used for ResponseOptions sorting of the options +# needs refactoring that will be made easier if the name the ResponseOptions dictionary is +# schema and note options +from collections import OrderedDict + + +def reorder_dict_skip_missing(old_dict, key_list): + """ + reorders dictionary according to ``key_list`` + removing any key with no associated value + or that is not in the key list + """ + return OrderedDict((k, old_dict[k]) for k in key_list if k in old_dict) \ No newline at end of file diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index bb3be6b..1d837cc 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -1,5 +1,7 @@ from .base import SchemaBase +DEFAULT_LANG = "en" + class Protocol(SchemaBase): """ @@ -9,57 +11,58 @@ class to deal with reproschema protocols schema_type = "reproschema:Protocol" def __init__(self, version=None): + """ + Rely on the parent class for construction of the instance + """ super().__init__(version) - self.schema["ui"] = { - "allow": [], - "shuffle": [], - "order": [], - "addProperties": [], - "overrideProperties": [], - } - - def set_landing_page(self, landing_page_url, lang="en"): - self.schema["landingPage"] = {"@id": landing_page_url, "@language": lang} - # TODO - # def add_landing_page(self, landing_page_url, lang="en"): - # preamble - # compute + def set_defaults(self, name="default"): + self._SchemaBase__set_defaults(name) + self.set_landing_page("README-en.md") + # does it make sense to give a preamble by default to protocols since + # they already have a landing page? + self.set_preamble() + self.set_ui_default() - def set_image(self, image_url): - self.schema["image"] = image_url + def set_landing_page(self, landing_page_uri, lang=DEFAULT_LANG): + self.schema["landingPage"] = {"@id": landing_page_uri, "inLanguage": lang} - def set_ui_allow(self): - self.schema["ui"]["allow"] = [ - "reproschema:AutoAdvance", - "reproschema:AllowExport", - ] + def append_activity(self, activity): + """ + We get from an activity instance the info we need to update the protocol scheme. - def set_ui_shuffle(self, shuffle=False): - self.schema["ui"]["shuffle"] = shuffle + This appends the activity after all the other ones. - def set_defaults(self, name): - self._ReproschemaSchema__set_defaults(name) # this looks wrong - self.set_landing_page("../../README-en.md") - self.set_ui_allow() - self.set_ui_shuffle(False) - - def append_activity(self, activity): + So this means the order of the activities will be dependent + on the order in which they are "read". + This implementation assumes that the activities are read + from a list and added one after the other. + """ # TODO - # - remove the hard coding on visibility and valueRequired - - # update the content of the protocol with this new activity - append_to_protocol = { - "variableName": activity.get_name(), + # - find a way to reorder, remove or add an activity + # at any point in the protocol + # - this method is nearly identical to the append_item method of Activity + # and should probably be refactored into a single method of the parent class + # and ideally into a method of a yet to be created UI class + + property = { + # variable name is name of activity without prefix + "variableName": activity.get_basename().replace("_schema", ""), "isAbout": activity.get_URI(), - "prefLabel": {"en": activity.schema["prefLabel"]}, - "isVis": True, - "valueRequired": False, + "prefLabel": activity.get_pref_label(), + "isVis": activity.visible, + "requiredValue": activity.required, } + if activity.skippable: + property["allow"] = ["reproschema:Skipped"] + + self.schema["ui"]["order"].append(activity.get_URI()) + self.schema["ui"]["addProperties"].append(property) - self.schema["ui"]["order"].append(activity.URI) - self.schema["ui"]["addProperties"].append(append_to_protocol) + """ + writing, reading, sorting, unsetting + """ def sort(self): schema_order = [ @@ -71,9 +74,15 @@ def sort(self): "schemaVersion", "version", "landingPage", + "preamble", + "citation", + "image", + "compute", "ui", ] self.sort_schema(schema_order) + self.sort_ui() - ui_order = ["allow", "shuffle", "order", "addProperties", "overrideProperties"] - self.sort_ui(ui_order) + def write(self, output_dir): + self.sort() + self._SchemaBase__write(output_dir) \ No newline at end of file From ff6c950d1c7195f5b6787e965b48e2f09d6db722 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Thu, 2 Dec 2021 23:00:43 +0100 Subject: [PATCH 37/69] add text area and refactor --- reproschema/models/item.py | 91 ++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 38 deletions(-) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 6165773..2f6b466 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -71,6 +71,9 @@ def set_basic_response_type(self, response_type): """ self.set_input_type_as_text() + if response_type == "textarea": + self.set_input_type_as_text_area() + if response_type == "int": self.set_input_type_as_int() @@ -102,29 +105,29 @@ def set_basic_response_type(self, response_type): """ def set_input_type_as_int(self): - self.set_input_type("number") - self.response_options.set_type("integer") - self.response_options.unset(["maxLength"]) + self._set_numeric_input_type("number", "integer") def set_input_type_as_float(self): - self.set_input_type("float") - self.response_options.set_type("float") + self._set_numeric_input_type("float", "float") + + def _set_numeric_input_type(self, arg0, arg1): + self.set_input_type(arg0) + self.response_options.set_type(arg1) self.response_options.unset(["maxLength"]) def set_input_type_as_date(self): - self.set_input_type("date") - self.response_options.unset(["maxLength"]) - self.response_options.set_type("date") + self._set_time_input_type("date", "date") def set_input_type_as_time_range(self): - self.set_input_type("timeRange") - self.response_options.unset(["maxLength"]) - self.response_options.set_type("datetime") + self._set_time_input_type("timeRange", "datetime") def set_input_type_as_year(self): - self.set_input_type("year") + self._set_time_input_type("year", "date") + + def _set_time_input_type(self, arg0, arg1): + self.set_input_type(arg0) self.response_options.unset(["maxLength"]) - self.response_options.set_type("date") + self.response_options.set_type(arg1) """ input types with preset response choices @@ -132,50 +135,63 @@ def set_input_type_as_year(self): def set_input_type_as_language(self): - URL = "https://raw.githubusercontent.com/ReproNim/reproschema-library/" - - self.set_input_type("selectLanguage") + URL = self._set_input_type_using_preset( + "https://raw.githubusercontent.com/ReproNim/reproschema-library/", + "selectLanguage", + ) - self.response_options.set_type("string") self.response_options.set_multiple_choice(True) self.response_options.use_preset(URL + "master/resources/languages.json") self.response_options.unset(["maxLength"]) def set_input_type_as_country(self): - URL = "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json" - - self.set_input_type("selectCountry") + URL = self._set_input_type_using_preset( + "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json", + "selectCountry", + ) - self.response_options.set_type("string") self.response_options.use_preset(URL) self.response_options.set_length(50) def set_input_type_as_state(self): - URL = "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json" - - self.set_input_type("selectState") + URL = self._set_input_type_using_preset( + "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json", + "selectState", + ) - self.response_options.set_type("string") self.response_options.use_preset(URL) self.response_options.unset(["maxLength"]) + def _set_input_type_using_preset(self, arg0, arg1): + result = arg0 + self.set_input_type(arg1) + self.response_options.set_type("string") + return result + """ input types requiring user typed input """ def set_input_type_as_text(self, length=300): - self.set_input_type("text") - self.response_options.set_type("string") - self.response_options.set_length(length) + self._set_text_input_type("text", length) + self.response_options.unset( + ["maxValue", "minValue", "multipleChoice", "choices"] + ) + + def set_input_type_as_text_area(self, length=300): + self._set_text_input_type("textarea", length) self.response_options.unset( ["maxValue", "minValue", "multipleChoice", "choices"] ) def set_input_type_as_multitext(self, length=300): - self.set_input_type("multitext") - self.response_options.set_type("string") + self._set_text_input_type("multitext", length) + + def _set_text_input_type(self, arg0, length): + self.set_input_type(arg0) + self.response_options.set_type('string') self.response_options.set_length(length) def set_input_type_as_email(self): @@ -203,18 +219,17 @@ def set_input_type_as_id(self): """ def set_input_type_as_radio(self, response_options): - self.set_input_type("radio") - response_options.set_type("integer") - self.response_options = response_options + self._set_multiplce_choice_item("radio", response_options) def set_input_type_as_select(self, response_options): - self.set_input_type("select") - response_options.set_type("integer") - self.response_options = response_options + self._set_multiplce_choice_item("select", response_options) def set_input_type_as_slider(self, response_options): - self.set_input_type("slider") - response_options.set_type("integer") + self._set_multiplce_choice_item("slider", response_options) + + def _set_multiplce_choice_item(self, arg0, response_options): + self.set_input_type(arg0) + response_options.set_type('integer') self.response_options = response_options """ From 9391fc19c3093e3092cee23f82c1b2445e75b01f Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 30 Jul 2022 21:02:47 +0200 Subject: [PATCH 38/69] add tests for new schema --- .pre-commit-config.yaml | 8 +++- reproschema/models/activity_new.py | 22 ++++----- reproschema/models/base_new.py | 53 +++++++++++++-------- reproschema/models/protocol_new.py | 9 ++-- reproschema/models/tests/test_new_schema.py | 19 ++++++++ reproschema/models/tests/test_schema.py | 26 +++++----- reproschema/models/tests/wtest_schema.py | 20 -------- 7 files changed, 88 insertions(+), 69 deletions(-) create mode 100644 reproschema/models/tests/test_new_schema.py delete mode 100644 reproschema/models/tests/wtest_schema.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c429bfa..d1174c3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,13 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files + - repo: https://github.com/psf/black - rev: 19.3b0 + rev: "22.6.0" hooks: - id: black + +- repo: https://github.com/asottile/reorder_python_imports + rev: "v3.8.2" + hooks: + - id: reorder-python-imports diff --git a/reproschema/models/activity_new.py b/reproschema/models/activity_new.py index 7afb356..c168a8b 100644 --- a/reproschema/models/activity_new.py +++ b/reproschema/models/activity_new.py @@ -1,18 +1,20 @@ -# from reproschema.models.base_new import SchemaBase -import base_new +# from reproschema.models import base_new +from . import base_new import attr -@attr.s(frozen=True) +from attrs import define, field + +@define class Activity(base_new.SchemaBase): """ class to deal with reproschema activities """ + def __attrs_post_init__(self): + self.schema_type ="reproschema:Activity" - schema_type = "reproschema:Activity" - - def __init__(self, version=None): - super().__init__(version) - self.schema["ui"] = {"shuffle": [], "order": [], "addProperties": []} + # def __init__(self, prefLabel="", version=None): + # super().__init__(prefLabel, version) + # self.schema["ui"] = {"shuffle": [], "order": [], "addProperties": []} def set_ui_shuffle(self, shuffle=False): self.schema["ui"]["shuffle"] = shuffle @@ -68,7 +70,3 @@ def sort(self): ui_order = ["shuffle", "order", "addProperties"] self.sort_ui(ui_order) - -aa = Activity(prefLabel="ann", description='trial') -print('------------') -print(12, aa.__dict__.keys()) diff --git a/reproschema/models/base_new.py b/reproschema/models/base_new.py index 3467f6c..1089fce 100644 --- a/reproschema/models/base_new.py +++ b/reproschema/models/base_new.py @@ -1,6 +1,9 @@ import json -import os -import attr + +from typing import Optional + +from attrs import define, field +from attrs.validators import instance_of, optional # SchemaBase = attr.make_class("SchemaBase", # {"prefLabel": attr.ib(kw_only=True, validator=attr.validators.instance_of(str)), @@ -11,17 +14,27 @@ # "schema_type": attr.ib(default=None, kw_only=True) # }) -@attr.s(frozen=True) + +@define class SchemaBase: - prefLabel = attr.ib(kw_only=True, validator=attr.validators.instance_of(str)) - description = attr.ib(default=None, validator=attr.validators.optional(attr.validators.instance_of(str))) - schemaVersion = attr.ib(default="1.0.0-rc4", validator=attr.validators.instance_of(str)) - version = attr.ib(default="0.0.1", validator=attr.validators.instance_of(str)) - # schema_type = attr.ib(default=None, kw_only=True) - # schema = attr.ib(default={ - # "@context": "https://raw.githubusercontent.com/ReproNim/reproschema//contexts/generic", - # "@type": 'None', - # }) + + prefLabel: Optional[str] = field( + default=None, kw_only=True, validator=optional(instance_of(str)) + ) + description: Optional[str] = field( + default=None, kw_only=True, validator=optional(instance_of(str)) + ) + schemaVersion: Optional[str] = field( + default="1.0.0-rc4", kw_only=True, validator=instance_of(str) + ) + version: Optional[str] = field( + default="0.0.1", kw_only=True, validator=instance_of(str) + ) + schema_type = field(default=None, kw_only=True) + schema = field(default={ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema//contexts/generic", + "@type": 'None', + }) # order = attr.ib(validator=attr.validators.deep_iterable( # member_validator = attr.validators.instance_of(str), # iterable_validator = attr.validators.instance_of(list) @@ -63,14 +76,14 @@ def sort_ui(self, ui_order): # json.dump(props, ff, sort_keys=True, indent=4) @classmethod - # def from_data(cls, data): - # if cls.schema_type is None: - # raise ValueError("SchemaBase cannot be used to instantiate class") - # if cls.schema_type != data["@type"]: - # raise ValueError(f"Mismatch in type {data['@type']} != {cls.schema_type}") - # klass = cls() - # klass.schema = data - # return klass + def from_data(cls, data, schema_type=None): + klass = cls(schema_type=schema_type) + if klass.schema_type is None: + raise ValueError("SchemaBase cannot be used to instantiate class") + if klass.schema_type != data["@type"]: + raise ValueError(f"Mismatch in type {data['@type']} != {klass.schema_type}") + klass.schema = data + return klass @classmethod def from_file(cls, filepath): diff --git a/reproschema/models/protocol_new.py b/reproschema/models/protocol_new.py index 799f06a..ef9f9cc 100644 --- a/reproschema/models/protocol_new.py +++ b/reproschema/models/protocol_new.py @@ -1,14 +1,17 @@ import attr -# from .base import SchemaBase +from .base_new import SchemaBase -@attr.s() +from attrs import define, field + +@define class Protocol(SchemaBase): """ class to deal with reproschema protocols """ + def __attrs_post_init__(self): + self.schema_type ="reproschema:Protocol" - schema_type = "reproschema:Protocol" # schema_type = attr.ib(default=None, kw_only=True) # ui = attr.ib(default={ diff --git a/reproschema/models/tests/test_new_schema.py b/reproschema/models/tests/test_new_schema.py new file mode 100644 index 0000000..4d54145 --- /dev/null +++ b/reproschema/models/tests/test_new_schema.py @@ -0,0 +1,19 @@ +from .. import activity_new, protocol_new + +def test_constructors(): + protocol_new.Protocol() + activity_new.Activity() + + version = "1.0.0-rc4" + proto = protocol_new.Protocol(version=version) + assert proto.schemaVersion == version + assert proto.schema_type == "reproschema:Protocol" + act = activity_new.Activity(version=version) + assert act.schemaVersion == version + assert act.schema_type == "reproschema:Activity" + + +def test_constructors_from_data(): + protocol_new.Protocol.from_data({"@type": "reproschema:Protocol"}, "reproschema:Protocol") + activity_new.Activity.from_data({"@type": "reproschema:Activity"}, "reproschema:Activity") + diff --git a/reproschema/models/tests/test_schema.py b/reproschema/models/tests/test_schema.py index a3de3c5..a68e808 100644 --- a/reproschema/models/tests/test_schema.py +++ b/reproschema/models/tests/test_schema.py @@ -1,20 +1,20 @@ -# from .. import Protocol, Activity, Item -from .. import activity, protocol, item +from .. import Protocol, Activity, Item -def test_constructors(): - protocol.Protocol() - activity.Activity() - version = "1.0.0-rc4" - proto = protocol.Protocol(version=version) +def test_constructors(): + Protocol() + Activity() + Item() + version = "1.0.0-rc2" + proto = Protocol(version=version) assert proto.schema["schemaVersion"] == version - act = activity.Activity(version) + act = Activity(version) assert act.schema["schemaVersion"] == version - # item = item.Item(version) - # assert item.schema["schemaVersion"] == version + item = Item(version) + assert item.schema["schemaVersion"] == version def test_constructors_from_data(): - protocol.Protocol.from_data({"@type": "reproschema:Protocol"}) - activity.Activity.from_data({"@type": "reproschema:Activity"}) - # item.Item.from_data({"@type": "reproschema:Field"}) + Protocol.from_data({"@type": "reproschema:Protocol"}) + Activity.from_data({"@type": "reproschema:Activity"}) + Item.from_data({"@type": "reproschema:Field"}) diff --git a/reproschema/models/tests/wtest_schema.py b/reproschema/models/tests/wtest_schema.py deleted file mode 100644 index a68e808..0000000 --- a/reproschema/models/tests/wtest_schema.py +++ /dev/null @@ -1,20 +0,0 @@ -from .. import Protocol, Activity, Item - - -def test_constructors(): - Protocol() - Activity() - Item() - version = "1.0.0-rc2" - proto = Protocol(version=version) - assert proto.schema["schemaVersion"] == version - act = Activity(version) - assert act.schema["schemaVersion"] == version - item = Item(version) - assert item.schema["schemaVersion"] == version - - -def test_constructors_from_data(): - Protocol.from_data({"@type": "reproschema:Protocol"}) - Activity.from_data({"@type": "reproschema:Activity"}) - Item.from_data({"@type": "reproschema:Field"}) From f1fdf659a439848a96184786608068a8e6d0e591 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 30 Jul 2022 21:59:14 +0200 Subject: [PATCH 39/69] tmp commit --- Pipfile | 11 +++ reproschema/cli.py | 1 - reproschema/models/base.py | 4 +- reproschema/models/base_new.py | 91 +++++++++++++++---- reproschema/models/protocol_new.py | 22 ++--- .../models/tests/protocols/default_schema | 21 +++++ reproschema/models/tests/test_new_protocol.py | 89 ++++++++++++++++++ setup.cfg | 1 + 8 files changed, 208 insertions(+), 32 deletions(-) create mode 100644 Pipfile create mode 100644 reproschema/models/tests/protocols/default_schema create mode 100644 reproschema/models/tests/test_new_protocol.py diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..71e4f7c --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +url = "https://pypi.org/simple" +verify_ssl = true +name = "pypi" + +[packages] + +[dev-packages] + +[requires] +python_version = "3.9" diff --git a/reproschema/cli.py b/reproschema/cli.py index adbf509..a8ff7ed 100644 --- a/reproschema/cli.py +++ b/reproschema/cli.py @@ -13,7 +13,6 @@ def print_version(ctx, param, value): click.echo(__version__) ctx.exit() - # group to provide commands @click.group() @click.option( diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 4cbb2bb..c050ea5 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -231,7 +231,9 @@ def __write(self, output_dir): """ Reused by the write method of the children classes """ - with open(os.path.join(output_dir, self.schema_file), "w") as ff: + if isinstance(output_dir, str): + output_dir = Path(output_dir) + with open(output_dir.joinpath(self.schema_file), "w") as ff: json.dump(self.schema, ff, sort_keys=False, indent=4) @classmethod diff --git a/reproschema/models/base_new.py b/reproschema/models/base_new.py index 1089fce..7ab060a 100644 --- a/reproschema/models/base_new.py +++ b/reproschema/models/base_new.py @@ -18,6 +18,8 @@ @define class SchemaBase: + DEFAULT_LANG = "en" + prefLabel: Optional[str] = field( default=None, kw_only=True, validator=optional(instance_of(str)) ) @@ -31,18 +33,37 @@ class SchemaBase: default="0.0.1", kw_only=True, validator=instance_of(str) ) schema_type = field(default=None, kw_only=True) - schema = field(default={ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema//contexts/generic", - "@type": 'None', - }) + schema = field( + default={ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema//contexts/generic", + "@type": "None", + } + ) + schema_file = field(default=None) + directory = field(default=None) + + def set_directory(self, output_directory): + """ + Where the file will be written by the ``write`` method + """ + self.directory = output_directory + # order = attr.ib(validator=attr.validators.deep_iterable( # member_validator = attr.validators.instance_of(str), # iterable_validator = attr.validators.instance_of(list) # )) - # def set_filename(self, name): - # self.schema_file = name + "_schema" - # self.schema["@id"] = name + "_schema" + def set_filename(self, name, ext=".jsonld"): + """ + By default all files are given: + - the ``.jsold`` extension + - have ``_schema`` suffix appended to them + + For item files their name won't have the schema prefix. + """ + # TODO figure out if the latter is a desirable behavior + self.schema_file = name + "_schema" + ext + self.schema["@id"] = name + "_schema" + ext def get_name(self): return self.schema_file.replace("_schema", "") @@ -53,8 +74,42 @@ def get_filename(self): def __set_defaults(self, name): self.set_filename(name) self.set_directory(name) - self.set_pref_label(name.replace("_", " ")) - self.set_description(name.replace("_", " ")) # description isn't mandatory, no? + self.prefLabel = name.replace("_", " ") + self.description = name.replace("_", " ") + + def set_ui_default(self): + self.schema["ui"] = { + "shuffle": [], + "order": [], + "addProperties": [], + "allow": [], + } + self.set_ui_shuffle() + self.set_ui_allow() + + def set_ui_shuffle(self, shuffle=False): + self.schema["ui"]["shuffle"] = shuffle + + def set_ui_allow(self, auto_advance=True, allow_export=True, disable_back=False): + # TODO + # Could be more convenient to have one method for each property + # + # Also currently the way this is handled makes it hard to update a single value: + # all 3 have to be reset everytime!!! + # + # Could be useful to have a UI class with dictionnary content that is only + # generated when the file is written + allow = [] + if auto_advance: + allow.append("reproschema:AutoAdvance") + if allow_export: + allow.append("reproschema:AllowExport") + if disable_back: + allow.append("reproschema:DisableBack") + self.schema["ui"]["allow"] = allow + + def set_preamble(self, preamble="", lang=DEFAULT_LANG): + self.schema["preamble"] = {lang: preamble} def sort_schema(self, schema_order): @@ -66,6 +121,15 @@ def sort_ui(self, ui_order): reordered_dict = {k: self.schema["ui"][k] for k in ui_order} self.schema["ui"] = reordered_dict + def __write(self, output_dir): + """ + Reused by the write method of the children classes + """ + if isinstance(output_dir, str): + output_dir = Path(output_dir) + with open(output_dir.joinpath(self.schema_file), "w") as ff: + json.dump(self.schema, ff, sort_keys=False, indent=4) + # def write(self, output_dir, filename): # # self.schema_file = filename + "_schema" # self.schema["@id"] = filename @@ -92,12 +156,3 @@ def from_file(cls, filepath): if "@type" not in data: raise ValueError("Missing @type key") return cls.from_data(data) - - -# aa = SchemaBase(prefLabel="ann", description='trial') -# print('------------') -# print(12, aa.__dict__.keys()) -# props = aa.__dict__.copy() -# # del props['schema'] -# # print(13, props) -# print(aa.write('./', 'tt_schema')) diff --git a/reproschema/models/protocol_new.py b/reproschema/models/protocol_new.py index ef9f9cc..1b53392 100644 --- a/reproschema/models/protocol_new.py +++ b/reproschema/models/protocol_new.py @@ -51,11 +51,13 @@ def set_ui_allow(self): # def set_ui_shuffle(self, shuffle=False): # self.schema["ui"]["shuffle"] = shuffle - def set_defaults(self, name): - self._ReproschemaSchema__set_defaults(name) # this looks wrong - self.set_landing_page("../../README-en.md") - self.set_ui_allow() - self.set_ui_shuffle(False) + def set_defaults(self, name="default"): + self._SchemaBase__set_defaults(name) + self.set_landing_page("README-en.md") + # does it make sense to give a preamble by default to protocols since + # they already have a landing page? + self.set_preamble() + self.set_ui_default() def append_activity(self, activity): @@ -92,10 +94,6 @@ def sort(self): ui_order = ["allow", "shuffle", "order", "addProperties", "overrideProperties"] self.sort_ui(ui_order) -# pp = Protocol(prefLabel="ann", description='trial') -# print('------------') -# print(12, pp.__dict__.keys()) -# props = pp.__dict__.copy() -# del props['schema'] -# print(13, props) -# print(pp.write('./', 'tt_schema')) + def write(self, output_dir): + # self.sort() + self._SchemaBase__write(output_dir) diff --git a/reproschema/models/tests/protocols/default_schema b/reproschema/models/tests/protocols/default_schema new file mode 100644 index 0000000..58899fd --- /dev/null +++ b/reproschema/models/tests/protocols/default_schema @@ -0,0 +1,21 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema//contexts/generic", + "@type": "None", + "@id": "default_schema", + "landingPage": { + "@id": "README-en.md", + "@language": "en" + }, + "preamble": { + "en": "" + }, + "ui": { + "shuffle": false, + "order": [], + "addProperties": [], + "allow": [ + "reproschema:AutoAdvance", + "reproschema:AllowExport" + ] + } +} \ No newline at end of file diff --git a/reproschema/models/tests/test_new_protocol.py b/reproschema/models/tests/test_new_protocol.py new file mode 100644 index 0000000..947276b --- /dev/null +++ b/reproschema/models/tests/test_new_protocol.py @@ -0,0 +1,89 @@ +import json, os +from pathlib import Path + +from ..protocol_new import Protocol +from ..activity import Activity + +my_path = Path(__file__).resolve().parent + + +# TODO +# refactor across the different test modules +protocol_dir = my_path.joinpath("protocols") +protocol_dir.mkdir(parents=True, exist_ok=True) + +""" +Only for the few cases when we want to check against some of the files in +reproschema/tests/data +""" +reproschema_test_data = my_path.joinpath("..", "..", "tests", "data") + + +def test_default(): + + """ + FYI: The default protocol does not conform to the schema + so `reproschema validate` will complain if you run it in this + """ + + protocol = Protocol() + protocol.set_defaults() + + protocol.write(protocol_dir) + protocol_content, expected = load_jsons(protocol) + assert protocol_content == expected + + clean_up(protocol) + + +def test_protocol(): + + protocol = Protocol() + protocol.set_defaults("protocol1") + protocol.prefLabel = "Protocol1" + protocol.description ="example Protocol" + protocol.set_landing_page("http://example.com/sample-readme.md") + + auto_advance = True + allow_export = True + disable_back = True + protocol.set_ui_allow(auto_advance, allow_export, disable_back) + + activity_1 = Activity() + activity_1.set_defaults("activity1") + activity_1.set_pref_label("Screening") + activity_1.set_URI(os.path.join("..", "activities", activity_1.get_filename())) + protocol.append_activity(activity_1) + + protocol.write(protocol_dir) + protocol_content, expected = load_jsons(protocol) + assert protocol_content == expected + + clean_up(protocol) + + +""" +HELPER FUNCTIONS +""" + + +def load_jsons(obj): + + output_file = protocol_dir.joinpath(obj.get_filename()) + content = read_json(output_file) + + data_file = my_path.joinpath("data", "protocols", obj.get_filename()) + expected = read_json(data_file) + + return content, expected + + +def read_json(file): + + with open(file, "r") as ff: + return json.load(ff) + + +def clean_up(obj): + + protocol_dir.joinpath(obj.get_filename()).unlink() diff --git a/setup.cfg b/setup.cfg index ef22d32..f3d7283 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,6 +31,7 @@ install_requires = PyLD requests requests_cache + attrs >=22.0.0 test_requires = pytest >= 4.4.0 From c60ba274e442e611428ed67ee5a8e6c46df58f4a Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 30 Jul 2022 22:00:12 +0200 Subject: [PATCH 40/69] remove attr new and restart refactoring --- reproschema/models/activity_new.py | 72 -------- reproschema/models/base_new.py | 158 ------------------ reproschema/models/protocol_new.py | 99 ----------- reproschema/models/tests/test_new_protocol.py | 89 ---------- reproschema/models/tests/test_new_schema.py | 19 --- 5 files changed, 437 deletions(-) delete mode 100644 reproschema/models/activity_new.py delete mode 100644 reproschema/models/base_new.py delete mode 100644 reproschema/models/protocol_new.py delete mode 100644 reproschema/models/tests/test_new_protocol.py delete mode 100644 reproschema/models/tests/test_new_schema.py diff --git a/reproschema/models/activity_new.py b/reproschema/models/activity_new.py deleted file mode 100644 index c168a8b..0000000 --- a/reproschema/models/activity_new.py +++ /dev/null @@ -1,72 +0,0 @@ -# from reproschema.models import base_new -from . import base_new -import attr - -from attrs import define, field - -@define -class Activity(base_new.SchemaBase): - """ - class to deal with reproschema activities - """ - def __attrs_post_init__(self): - self.schema_type ="reproschema:Activity" - - # def __init__(self, prefLabel="", version=None): - # super().__init__(prefLabel, version) - # self.schema["ui"] = {"shuffle": [], "order": [], "addProperties": []} - - def set_ui_shuffle(self, shuffle=False): - self.schema["ui"]["shuffle"] = shuffle - - def set_URI(self, URI): - self.URI = URI - - def get_URI(self): - return self.URI - - # TODO - # preamble - # compute - # citation - # image - - def set_defaults(self, name): - self._ReproschemaSchema__set_defaults(name) # this looks wrong - self.set_ui_shuffle(False) - - def update_activity(self, item_info): - - # TODO - # - remove the hard coding on visibility and valueRequired - - # update the content of the activity schema with new item - - item_info["URI"] = "items/" + item_info["name"] - - append_to_activity = { - "variableName": item_info["name"], - "isAbout": item_info["URI"], - "isVis": item_info["visibility"], - "valueRequired": False, - } - - self.schema["ui"]["order"].append(item_info["URI"]) - self.schema["ui"]["addProperties"].append(append_to_activity) - - def sort(self): - schema_order = [ - "@context", - "@type", - "@id", - "prefLabel", - "description", - "schemaVersion", - "version", - "ui", - ] - self.sort_schema(schema_order) - - ui_order = ["shuffle", "order", "addProperties"] - self.sort_ui(ui_order) - diff --git a/reproschema/models/base_new.py b/reproschema/models/base_new.py deleted file mode 100644 index 7ab060a..0000000 --- a/reproschema/models/base_new.py +++ /dev/null @@ -1,158 +0,0 @@ -import json - -from typing import Optional - -from attrs import define, field -from attrs.validators import instance_of, optional - -# SchemaBase = attr.make_class("SchemaBase", -# {"prefLabel": attr.ib(kw_only=True, validator=attr.validators.instance_of(str)), -# "description": attr.ib(default=None, -# validator=attr.validators.optional(attr.validators.instance_of(str))), -# "schemaVersion": attr.ib(default="1.0.0-rc4", validator=attr.validators.instance_of(str)), -# "version": attr.ib(default="0.0.1", validator=attr.validators.instance_of(str)), -# "schema_type": attr.ib(default=None, kw_only=True) -# }) - - -@define -class SchemaBase: - - DEFAULT_LANG = "en" - - prefLabel: Optional[str] = field( - default=None, kw_only=True, validator=optional(instance_of(str)) - ) - description: Optional[str] = field( - default=None, kw_only=True, validator=optional(instance_of(str)) - ) - schemaVersion: Optional[str] = field( - default="1.0.0-rc4", kw_only=True, validator=instance_of(str) - ) - version: Optional[str] = field( - default="0.0.1", kw_only=True, validator=instance_of(str) - ) - schema_type = field(default=None, kw_only=True) - schema = field( - default={ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema//contexts/generic", - "@type": "None", - } - ) - schema_file = field(default=None) - directory = field(default=None) - - def set_directory(self, output_directory): - """ - Where the file will be written by the ``write`` method - """ - self.directory = output_directory - - # order = attr.ib(validator=attr.validators.deep_iterable( - # member_validator = attr.validators.instance_of(str), - # iterable_validator = attr.validators.instance_of(list) - # )) - - def set_filename(self, name, ext=".jsonld"): - """ - By default all files are given: - - the ``.jsold`` extension - - have ``_schema`` suffix appended to them - - For item files their name won't have the schema prefix. - """ - # TODO figure out if the latter is a desirable behavior - self.schema_file = name + "_schema" + ext - self.schema["@id"] = name + "_schema" + ext - - def get_name(self): - return self.schema_file.replace("_schema", "") - - def get_filename(self): - return self.schema_file - - def __set_defaults(self, name): - self.set_filename(name) - self.set_directory(name) - self.prefLabel = name.replace("_", " ") - self.description = name.replace("_", " ") - - def set_ui_default(self): - self.schema["ui"] = { - "shuffle": [], - "order": [], - "addProperties": [], - "allow": [], - } - self.set_ui_shuffle() - self.set_ui_allow() - - def set_ui_shuffle(self, shuffle=False): - self.schema["ui"]["shuffle"] = shuffle - - def set_ui_allow(self, auto_advance=True, allow_export=True, disable_back=False): - # TODO - # Could be more convenient to have one method for each property - # - # Also currently the way this is handled makes it hard to update a single value: - # all 3 have to be reset everytime!!! - # - # Could be useful to have a UI class with dictionnary content that is only - # generated when the file is written - allow = [] - if auto_advance: - allow.append("reproschema:AutoAdvance") - if allow_export: - allow.append("reproschema:AllowExport") - if disable_back: - allow.append("reproschema:DisableBack") - self.schema["ui"]["allow"] = allow - - def set_preamble(self, preamble="", lang=DEFAULT_LANG): - self.schema["preamble"] = {lang: preamble} - - def sort_schema(self, schema_order): - - reordered_dict = {k: self.schema[k] for k in schema_order} - self.schema = reordered_dict - - def sort_ui(self, ui_order): - - reordered_dict = {k: self.schema["ui"][k] for k in ui_order} - self.schema["ui"] = reordered_dict - - def __write(self, output_dir): - """ - Reused by the write method of the children classes - """ - if isinstance(output_dir, str): - output_dir = Path(output_dir) - with open(output_dir.joinpath(self.schema_file), "w") as ff: - json.dump(self.schema, ff, sort_keys=False, indent=4) - - # def write(self, output_dir, filename): - # # self.schema_file = filename + "_schema" - # self.schema["@id"] = filename - # props = aa.__dict__.copy() - # del props['schema'] - # props.update(self.schema) - # with open(os.path.join(output_dir, filename), "w") as ff: - # json.dump(props, ff, sort_keys=True, indent=4) - - @classmethod - def from_data(cls, data, schema_type=None): - klass = cls(schema_type=schema_type) - if klass.schema_type is None: - raise ValueError("SchemaBase cannot be used to instantiate class") - if klass.schema_type != data["@type"]: - raise ValueError(f"Mismatch in type {data['@type']} != {klass.schema_type}") - klass.schema = data - return klass - - @classmethod - def from_file(cls, filepath): - with open(filepath) as fp: - data = json.load(fp) - if "@type" not in data: - raise ValueError("Missing @type key") - return cls.from_data(data) diff --git a/reproschema/models/protocol_new.py b/reproschema/models/protocol_new.py deleted file mode 100644 index 1b53392..0000000 --- a/reproschema/models/protocol_new.py +++ /dev/null @@ -1,99 +0,0 @@ -import attr -from .base_new import SchemaBase - - -from attrs import define, field - -@define -class Protocol(SchemaBase): - """ - class to deal with reproschema protocols - """ - def __attrs_post_init__(self): - self.schema_type ="reproschema:Protocol" - - # schema_type = attr.ib(default=None, kw_only=True) - - # ui = attr.ib(default={ - # "allow": attr.ib(default=attr.Factory(list)), - # "shuffle": attr.ib(default=False), - # "order": attr.ib(default=attr.Factory(list)), - # "addProperties": attr.ib(default=attr.Factory(list)), - # "overrideProperties": attr.ib(default=attr.Factory(list)), - # }) - # def __init__(self, version=None): - # super().__init__(version) - # self.schema["ui"] = { - # "allow": [], - # "shuffle": [], - # "order": [], - # "addProperties": [], - # "overrideProperties": [], - # } - - def set_landing_page(self, landing_page_url, lang="en"): - self.schema["landingPage"] = {"@id": landing_page_url, "@language": lang} - - # TODO - # def add_landing_page(self, landing_page_url, lang="en"): - # preamble - # compute - - def set_image(self, image_url): - self.schema["image"] = image_url - - def set_ui_allow(self): - self.schema["ui"]["allow"] = [ - "reproschema:AutoAdvance", - "reproschema:AllowExport", - ] - - # def set_ui_shuffle(self, shuffle=False): - # self.schema["ui"]["shuffle"] = shuffle - - def set_defaults(self, name="default"): - self._SchemaBase__set_defaults(name) - self.set_landing_page("README-en.md") - # does it make sense to give a preamble by default to protocols since - # they already have a landing page? - self.set_preamble() - self.set_ui_default() - - def append_activity(self, activity): - - # TODO - # - remove the hard coding on visibility and valueRequired - - # update the content of the protocol with this new activity - append_to_protocol = { - "variableName": activity.get_name(), - "isAbout": activity.get_URI(), - "prefLabel": {"en": activity.schema["prefLabel"]}, - "isVis": True, - "valueRequired": False, - # schedule, randomMaxDelay, limit - } - - self.schema["ui"]["order"].append(activity.URI) - self.schema["ui"]["addProperties"].append(append_to_protocol) - - def sort(self): - schema_order = [ - "@context", - "@type", - "@id", - "prefLabel", - "description", - "schemaVersion", - "version", - "landingPage", - "ui", - ] - self.sort_schema(schema_order) - - ui_order = ["allow", "shuffle", "order", "addProperties", "overrideProperties"] - self.sort_ui(ui_order) - - def write(self, output_dir): - # self.sort() - self._SchemaBase__write(output_dir) diff --git a/reproschema/models/tests/test_new_protocol.py b/reproschema/models/tests/test_new_protocol.py deleted file mode 100644 index 947276b..0000000 --- a/reproschema/models/tests/test_new_protocol.py +++ /dev/null @@ -1,89 +0,0 @@ -import json, os -from pathlib import Path - -from ..protocol_new import Protocol -from ..activity import Activity - -my_path = Path(__file__).resolve().parent - - -# TODO -# refactor across the different test modules -protocol_dir = my_path.joinpath("protocols") -protocol_dir.mkdir(parents=True, exist_ok=True) - -""" -Only for the few cases when we want to check against some of the files in -reproschema/tests/data -""" -reproschema_test_data = my_path.joinpath("..", "..", "tests", "data") - - -def test_default(): - - """ - FYI: The default protocol does not conform to the schema - so `reproschema validate` will complain if you run it in this - """ - - protocol = Protocol() - protocol.set_defaults() - - protocol.write(protocol_dir) - protocol_content, expected = load_jsons(protocol) - assert protocol_content == expected - - clean_up(protocol) - - -def test_protocol(): - - protocol = Protocol() - protocol.set_defaults("protocol1") - protocol.prefLabel = "Protocol1" - protocol.description ="example Protocol" - protocol.set_landing_page("http://example.com/sample-readme.md") - - auto_advance = True - allow_export = True - disable_back = True - protocol.set_ui_allow(auto_advance, allow_export, disable_back) - - activity_1 = Activity() - activity_1.set_defaults("activity1") - activity_1.set_pref_label("Screening") - activity_1.set_URI(os.path.join("..", "activities", activity_1.get_filename())) - protocol.append_activity(activity_1) - - protocol.write(protocol_dir) - protocol_content, expected = load_jsons(protocol) - assert protocol_content == expected - - clean_up(protocol) - - -""" -HELPER FUNCTIONS -""" - - -def load_jsons(obj): - - output_file = protocol_dir.joinpath(obj.get_filename()) - content = read_json(output_file) - - data_file = my_path.joinpath("data", "protocols", obj.get_filename()) - expected = read_json(data_file) - - return content, expected - - -def read_json(file): - - with open(file, "r") as ff: - return json.load(ff) - - -def clean_up(obj): - - protocol_dir.joinpath(obj.get_filename()).unlink() diff --git a/reproschema/models/tests/test_new_schema.py b/reproschema/models/tests/test_new_schema.py deleted file mode 100644 index 4d54145..0000000 --- a/reproschema/models/tests/test_new_schema.py +++ /dev/null @@ -1,19 +0,0 @@ -from .. import activity_new, protocol_new - -def test_constructors(): - protocol_new.Protocol() - activity_new.Activity() - - version = "1.0.0-rc4" - proto = protocol_new.Protocol(version=version) - assert proto.schemaVersion == version - assert proto.schema_type == "reproschema:Protocol" - act = activity_new.Activity(version=version) - assert act.schemaVersion == version - assert act.schema_type == "reproschema:Activity" - - -def test_constructors_from_data(): - protocol_new.Protocol.from_data({"@type": "reproschema:Protocol"}, "reproschema:Protocol") - activity_new.Activity.from_data({"@type": "reproschema:Activity"}, "reproschema:Activity") - From ea681852b02f3c6a1c46819835592e7de7bf94f9 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 30 Jul 2022 22:38:36 +0200 Subject: [PATCH 41/69] start using attr --- pyproject.toml | 4 +++ reproschema/models/activity.py | 11 +++---- reproschema/models/base.py | 40 +++++++++++------------ reproschema/models/item.py | 11 ++----- reproschema/models/protocol.py | 3 +- reproschema/models/tests/test_protocol.py | 2 -- reproschema/models/tests/test_schema.py | 4 +-- 7 files changed, 34 insertions(+), 41 deletions(-) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..a5eacca --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,4 @@ +[tool.pytest.ini_options] +minversion = "4.4.0" +addopts = "-ra -vv" + diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index f7cc7cb..ef2427f 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -2,20 +2,17 @@ DEFAULT_LANG = "en" - class Activity(SchemaBase): """ class to deal with reproschema activities """ - schema_type = "reproschema:Activity" - visible = True required = False skippable = True def __init__(self, version=None): - super().__init__(version) + super().__init__(version=version, schema_type="reproschema:Activity") def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) @@ -30,17 +27,17 @@ def set_compute(self, variable, expression): def append_item(self, item): # See comment and TODO of the append_activity Protocol class - property = { + item_property = { "variableName": item.get_basename(), "isAbout": item.get_URI(), "isVis": item.visible, "requiredValue": item.required, } if item.skippable: - property["allow"] = ["reproschema:Skipped"] + item_property["allow"] = ["reproschema:Skipped"] self.schema["ui"]["order"].append(item.get_URI()) - self.schema["ui"]["addProperties"].append(property) + self.schema["ui"]["addProperties"].append(item_property) """ writing, reading, sorting, unsetting diff --git a/reproschema/models/base.py b/reproschema/models/base.py index c050ea5..817feea 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -3,6 +3,10 @@ from pathlib import Path from collections import OrderedDict +from attrs import define, field + +import attrs + """ For any key that can be 'translated' we set english as the default language in case the user does not provide it. @@ -23,6 +27,7 @@ def default_context(version): return URL + VERSION + "/contexts/generic" +@define class SchemaBase: """ base class to deal with reproschema schemas @@ -43,25 +48,20 @@ class SchemaBase: """ - # TODO might be more convenient to have some of the properties not centrlized in a single dictionnary - # - # Could be more practical to only create part or all of the dictionnary when write is called - # - - schema_type = None - - def __init__(self, version): - - # TODO the version handling could probably be refactored - VERSION = version or DEFAULT_VERSION + version = field( + kw_only=True, + default=DEFAULT_VERSION, + converter=attrs.converters.default_if_none(default=DEFAULT_VERSION), + ) + schema_type = field(kw_only=True, default=None) + def __attrs_post_init__(self): self.schema = { "@type": self.schema_type, - "schemaVersion": VERSION, + "schemaVersion": self.version, "version": "0.0.1", } - - URL = self.get_default_context(version) + URL = self.get_default_context(self.version) self.set_context(URL) # This probably needs some cleaning but is at the moment necessary to pass @@ -125,8 +125,8 @@ def set_filename(self, name, ext=".jsonld"): For item files their name won't have the schema prefix. """ # TODO figure out if the latter is a desirable behavior - self.schema_file = name + "_schema" + ext - self.schema["@id"] = name + "_schema" + ext + self.schema_file = f"{name}_schema{ext}" + self.schema["@id"] = f"{name}_schema{ext}" def set_pref_label(self, pref_label, lang=DEFAULT_LANG): self.schema["prefLabel"] = {lang: pref_label} @@ -238,11 +238,11 @@ def __write(self, output_dir): @classmethod def from_data(cls, data): - if cls.schema_type is None: - raise ValueError("SchemaBase cannot be used to instantiate class") - if cls.schema_type != data["@type"]: - raise ValueError(f"Mismatch in type {data['@type']} != {cls.schema_type}") klass = cls() + if klass.schema_type is None: + raise ValueError("SchemaBase cannot be used to instantiate class") + if klass.schema_type != data["@type"]: + raise ValueError(f"Mismatch in type {data['@type']} != {klass.schema_type}") klass.schema = data return klass diff --git a/reproschema/models/item.py b/reproschema/models/item.py index b719c83..5513530 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -1,21 +1,18 @@ from .base import SchemaBase - DEFAULT_LANG = "en" - class Item(SchemaBase): """ class to deal with reproschema items """ - schema_type = "reproschema:Field" visible = True required = False skippable = True def __init__(self, version=None): - super().__init__(version) + super().__init__(version=version, schema_type="reproschema:Field") self.schema["ui"] = {"inputType": []} self.schema["question"] = {} """ @@ -281,8 +278,6 @@ class to deal with reproschema response options # # self.options and self.schema - schema_type = "reproschema:ResponseOption" - def __init__(self): self.options = { "valueType": "", @@ -293,7 +288,7 @@ def __init__(self): } def set_defaults(self, name="valueConstraints", version=None): - super().__init__(version) + super().__init__(version=version, schema_type="reproschema:ResponseOption") self.options["@context"] = self.schema["@context"] self.options["@type"] = self.schema_type self.set_filename(name) @@ -310,7 +305,7 @@ def unset(self, keys): self.options.pop(i, None) def set_type(self, type): - self.options["valueType"] = "xsd:" + type + self.options["valueType"] = f"xsd:{type}" # TODO a nice thing to do would be to read the min and max value # from the rest of the content of self.options diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index dded1cd..2418ae9 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -8,13 +8,12 @@ class Protocol(SchemaBase): class to deal with reproschema protocols """ - schema_type = "reproschema:Protocol" def __init__(self, version=None): """ Rely on the parent class for construction of the instance """ - super().__init__(version) + super().__init__(version=version, schema_type="reproschema:Protocol") def set_defaults(self, name="default"): self._SchemaBase__set_defaults(name) diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py index ddc391f..cee846b 100644 --- a/reproschema/models/tests/test_protocol.py +++ b/reproschema/models/tests/test_protocol.py @@ -5,8 +5,6 @@ my_path = os.path.dirname(os.path.abspath(__file__)) -# Left here in case Remi and python path or import can't be friends once again. -# sys.path.insert(0, my_path + "/../") # TODO # refactor across the different test modules diff --git a/reproschema/models/tests/test_schema.py b/reproschema/models/tests/test_schema.py index a68e808..bf5578f 100644 --- a/reproschema/models/tests/test_schema.py +++ b/reproschema/models/tests/test_schema.py @@ -8,9 +8,9 @@ def test_constructors(): version = "1.0.0-rc2" proto = Protocol(version=version) assert proto.schema["schemaVersion"] == version - act = Activity(version) + act = Activity(version=version) assert act.schema["schemaVersion"] == version - item = Item(version) + item = Item(version=version) assert item.schema["schemaVersion"] == version From 03fe296d5681bf76f2d7811f2dda014cc4ea68f4 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 31 Jul 2022 22:55:22 +0200 Subject: [PATCH 42/69] refactor refactor run pre commit refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor context setting add validation refactor set_description refactor refactor naming items refactor naming items refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor --- .gitignore | 2 + .vscode/settings.json | 6 + reproschema/models/__init__.py | 2 +- reproschema/models/activity.py | 82 ++-- reproschema/models/base.py | 303 +++++++------- reproschema/models/item.py | 385 +++++++----------- reproschema/models/protocol.py | 90 ++-- reproschema/models/response_options.py | 95 +++++ reproschema/models/tests/__init__.py | 0 .../data/activities/default_schema.jsonld | 2 +- .../models/tests/data/items/default.jsonld | 2 +- .../models/tests/data/items/text.jsonld | 2 +- .../data/protocols/default_schema.jsonld | 2 +- .../data/protocols/protocol1_schema.jsonld | 2 +- reproschema/models/tests/items/country | 22 + reproschema/models/tests/items/date | 20 + reproschema/models/tests/items/default | 19 + reproschema/models/tests/items/email | 20 + reproschema/models/tests/items/float | 20 + reproschema/models/tests/items/integer | 20 + reproschema/models/tests/items/language | 22 + reproschema/models/tests/items/multitext | 21 + reproschema/models/tests/items/participant_id | 20 + reproschema/models/tests/items/radio | 37 ++ reproschema/models/tests/items/select | 43 ++ reproschema/models/tests/items/slider | 55 +++ reproschema/models/tests/items/state | 21 + reproschema/models/tests/items/text | 21 + reproschema/models/tests/items/time_range | 20 + reproschema/models/tests/items/year | 20 + reproschema/models/tests/protocols/protocol1 | 77 ++++ reproschema/models/tests/test_activity.py | 85 +--- reproschema/models/tests/test_item.py | 239 ++++------- reproschema/models/tests/test_protocol.py | 69 +--- .../models/tests/test_response_options.py | 48 +-- reproschema/models/tests/test_schema.py | 10 +- reproschema/models/tests/test_ui.py | 5 + reproschema/models/tests/utils.py | 32 ++ reproschema/models/ui.py | 88 ++++ reproschema/models/utils.py | 5 +- setup.cfg | 1 + 41 files changed, 1249 insertions(+), 786 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 reproschema/models/response_options.py delete mode 100644 reproschema/models/tests/__init__.py create mode 100644 reproschema/models/tests/items/country create mode 100644 reproschema/models/tests/items/date create mode 100644 reproschema/models/tests/items/default create mode 100644 reproschema/models/tests/items/email create mode 100644 reproschema/models/tests/items/float create mode 100644 reproschema/models/tests/items/integer create mode 100644 reproschema/models/tests/items/language create mode 100644 reproschema/models/tests/items/multitext create mode 100644 reproschema/models/tests/items/participant_id create mode 100644 reproschema/models/tests/items/radio create mode 100644 reproschema/models/tests/items/select create mode 100644 reproschema/models/tests/items/slider create mode 100644 reproschema/models/tests/items/state create mode 100644 reproschema/models/tests/items/text create mode 100644 reproschema/models/tests/items/time_range create mode 100644 reproschema/models/tests/items/year create mode 100644 reproschema/models/tests/protocols/protocol1 create mode 100644 reproschema/models/tests/test_ui.py create mode 100644 reproschema/models/tests/utils.py create mode 100644 reproschema/models/ui.py diff --git a/.gitignore b/.gitignore index 0658872..8e838a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +**/tmp + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4bc4068 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "cSpell.words": [ + "reproschema" + ], + "autoDocstring.docstringFormat": "numpy" +} diff --git a/reproschema/models/__init__.py b/reproschema/models/__init__.py index 1c1a154..9a94610 100644 --- a/reproschema/models/__init__.py +++ b/reproschema/models/__init__.py @@ -1,3 +1,3 @@ -from .protocol import Protocol from .activity import Activity from .item import Item +from .protocol import Protocol diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index ef2427f..86aeebd 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -1,22 +1,56 @@ from .base import SchemaBase +from .item import Item DEFAULT_LANG = "en" +schema_order = [ + "@context", + "@type", + "@id", + "prefLabel", + "description", + "schemaVersion", + "version", + "preamble", + "citation", + "image", + "compute", + "ui", +] + + class Activity(SchemaBase): """ class to deal with reproschema activities """ - visible = True - required = False - skippable = True + def __init__( + self, + name="activity", + schemaVersion=None, + suffix="_schema", + ext=".jsonld", + visible: bool = True, + required: bool = False, + skippable: bool = True, + ): + super().__init__( + at_id=name, + schemaVersion=schemaVersion, + type="reproschema:Activity", + schema_order=schema_order, + suffix=suffix, + ext=ext, + ) + self.visible = visible + self.required = required + self.skippable = skippable - def __init__(self, version=None): - super().__init__(version=version, schema_type="reproschema:Activity") - - def set_defaults(self, name="default"): - self._SchemaBase__set_defaults(name) - self.set_preamble() + def set_defaults(self, name=None): + if not name: + name = self.at_id + super().set_defaults(name) + self.set_preamble(name) self.set_ui_default() def set_compute(self, variable, expression): @@ -24,43 +58,19 @@ def set_compute(self, variable, expression): {"variableName": variable, "jsExpression": expression} ] - def append_item(self, item): - # See comment and TODO of the append_activity Protocol class - + def append_item(self, item: Item): item_property = { "variableName": item.get_basename(), - "isAbout": item.get_URI(), + "isAbout": item.URI, "isVis": item.visible, "requiredValue": item.required, } - if item.skippable: - item_property["allow"] = ["reproschema:Skipped"] - - self.schema["ui"]["order"].append(item.get_URI()) - self.schema["ui"]["addProperties"].append(item_property) + self.append_to_ui(item, item_property) """ writing, reading, sorting, unsetting """ - def sort(self): - schema_order = [ - "@context", - "@type", - "@id", - "prefLabel", - "description", - "schemaVersion", - "version", - "preamble", - "citation", - "image", - "compute", - "ui", - ] - self.sort_schema(schema_order) - self.sort_ui() - def write(self, output_dir): self.sort() self._SchemaBase__write(output_dir) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 817feea..668f97a 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -1,38 +1,46 @@ import json -import os -from pathlib import Path from collections import OrderedDict - -from attrs import define, field +from pathlib import Path +from typing import Dict +from typing import List +from typing import Optional +from typing import Union import attrs +from attrs import define +from attrs import field +from attrs.validators import instance_of -""" -For any key that can be 'translated' we set english as the default language -in case the user does not provide it. -""" -DEFAULT_LANG = "en" - -""" -""" -DEFAULT_VERSION = "1.0.0-rc4" +from .ui import UI -def default_context(version): - """ - For now we assume that the github repo will be where schema will be read from. - """ - URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" - VERSION = version or DEFAULT_VERSION - return URL + VERSION + "/contexts/generic" +DEFAULT_LANG = "en" +DEFAULT_VERSION = "1.0.0-rc4" @define class SchemaBase: + """_summary_ + + Returns + ------- + _type_ + _description_ + + Raises + ------ + ValueError + _description_ + ValueError + _description_ + ValueError + _description_ + """ + """ base class to deal with reproschema schemas - The content of the schema is stored in the dictionnary ``self.schema`` + The content of the schema is stored in the dictionary ``self.schema`` self.schema["@context"] self.schema["@type"] @@ -48,91 +56,103 @@ class SchemaBase: """ - version = field( + type: str = field(kw_only=True, default="", validator=[instance_of(str)]) + schemaVersion: str = field( kw_only=True, default=DEFAULT_VERSION, - converter=attrs.converters.default_if_none(default=DEFAULT_VERSION), + converter=attrs.converters.default_if_none(default=DEFAULT_VERSION), # type: ignore ) - schema_type = field(kw_only=True, default=None) - - def __attrs_post_init__(self): + version: str = field(kw_only=True, default="0.0.1", validator=[instance_of(str)]) + at_id: str = field(kw_only=True, default="", validator=[instance_of(str)]) + at_context: str = field(kw_only=True, default="", validator=[instance_of(str)]) + prefLabel: dict = field(kw_only=True, default={}, validator=[instance_of(dict)]) + description: str = field(kw_only=True, default="", validator=[instance_of(str)]) + + URI = field(default=None) + lang = field(default=DEFAULT_LANG) + schema_order: list = field( + kw_only=True, + default=None, + converter=attrs.converters.default_if_none(default=[]), # type: ignore + ) + suffix = field(kw_only=True, default="_schema") + ext = field(kw_only=True, default=".jsonld") + + def __attrs_post_init__(self) -> None: + self.at_id = self.at_id.replace(" ", "_") + if self.description == "": + self.description = self.at_id.replace("_", " ") + if self.at_context == "": + self.at_context: str = self.default_context() + if self.prefLabel == {}: + self.prefLabel = {self.lang: self.at_id} self.schema = { - "@type": self.schema_type, - "schemaVersion": self.version, - "version": "0.0.1", + "@type": self.type, + "schemaVersion": self.schemaVersion, + "version": self.version, + "prefLabel": {}, + "description": self.description, } - URL = self.get_default_context(self.version) - self.set_context(URL) + self.ui = UI(self.type) + self.update() + + def update(self) -> None: + self.schema["@id"] = self.at_id + self.schema["@type"] = self.type + self.schema["schemaVersion"] = self.schemaVersion + self.schema["version"] = self.version + self.schema["@context"] = self.at_context + self.schema["prefLabel"] = self.prefLabel + self.schema["description"] = self.description + self.ui.update() + self.schema["ui"] = self.ui.schema # This probably needs some cleaning but is at the moment necessary to pass # the context to the ResponseOption class - def get_default_context(self, version): - return default_context(version) + def default_context(self) -> str: + """ + For now we assume that the github repo will be where schema will be read from. + """ + URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" + VERSION = self.schemaVersion or DEFAULT_VERSION + return URL + VERSION + "/contexts/generic" - def __set_defaults(self, name): + def set_defaults(self, name=None) -> None: + if not name: + name = self.at_id self.set_filename(name) - self.set_directory(name) "We use the ``name`` of this class instance for schema keys minus some underscore" self.set_pref_label(name.replace("_", " ")) self.set_description(name.replace("_", " ")) - """ - setters - """ - - def set_directory(self, output_directory): - """ - Where the file will be written by the ``write`` method - """ - self.dir = output_directory - - def set_URI(self, URI): - """ - In case we need to keep where the output file is located, - we can set it with this method. - This can be useful if we have just read or created an item - and want to add it to an activity. - """ - self.URI = URI - - """ - schema related setters - - use them to set several of the common keys of the schema - """ - - def set_context(self, context): - """ - In case we want to overide the default context - """ - self.schema["@context"] = context - - def set_preamble(self, preamble="", lang=DEFAULT_LANG): + def set_preamble( + self, preamble: Optional[str] = None, lang: str = DEFAULT_LANG + ) -> None: + if not preamble: + preamble = "" self.schema["preamble"] = {lang: preamble} - def set_citation(self, citation): + def set_citation(self, citation: str) -> None: self.schema["citation"] = citation - def set_image(self, image): + def set_image(self, image) -> None: self.schema["image"] = image - def set_filename(self, name, ext=".jsonld"): - """ - By default all files are given: - - the ``.jsold`` extension - - have ``_schema`` suffix appended to them - - For item files their name won't have the schema prefix. - """ - # TODO figure out if the latter is a desirable behavior - self.schema_file = f"{name}_schema{ext}" - self.schema["@id"] = f"{name}_schema{ext}" + def set_filename(self, name: str) -> None: + name = name.replace(" ", "_") + self.at_id = f"{name}{self.suffix}{self.ext}" + self.update() - def set_pref_label(self, pref_label, lang=DEFAULT_LANG): - self.schema["prefLabel"] = {lang: pref_label} + def set_pref_label(self, pref_label: str = None, lang: str = DEFAULT_LANG) -> None: + if not pref_label: + return + self.prefLabel[lang] = pref_label + self.schema["prefLabel"] = self.prefLabel - def set_description(self, description): - self.schema["description"] = description + def set_description(self, description: str) -> None: + self.description = description + self.schema["description"] = self.description + # self.update() """ getters @@ -141,60 +161,52 @@ def set_description(self, description): or some of the instance properties """ - def get_name(self): - return self.schema_file.replace("_schema", "") - - def get_filename(self): - return self.schema_file - - def get_basename(self): - return Path(self.schema_file).stem - - def get_pref_label(self): - return self.schema["prefLabel"] - - def get_URI(self): - return self.URI + def get_basename(self) -> str: + return Path(self.at_id).stem """ UI: setters specific to the user interface keys of the schema - The ui has its own dictionnary. + The ui has its own dictionary. Mostly used by the Protocol and Activity class. """ - def set_ui_default(self): + def set_ui_default(self) -> None: self.schema["ui"] = { "shuffle": [], "order": [], "addProperties": [], "allow": [], } - self.set_ui_shuffle() self.set_ui_allow() - - def set_ui_shuffle(self, shuffle=False): - self.schema["ui"]["shuffle"] = shuffle - - def set_ui_allow(self, auto_advance=True, allow_export=True, disable_back=False): - # TODO - # Could be more convenient to have one method for each property - # - # Also currently the way this is handled makes it hard to update a single value: - # all 3 have to be reset everytime!!! - # - # Could be useful to have a UI class with dictionnary content that is only - # generated when the file is written - allow = [] - if auto_advance: - allow.append("reproschema:AutoAdvance") - if allow_export: - allow.append("reproschema:AllowExport") - if disable_back: - allow.append("reproschema:DisableBack") - self.schema["ui"]["allow"] = allow + self.set_ui_shuffle() + self.ui.update() + + def set_ui_shuffle(self, shuffle: bool = False) -> None: + self.ui.shuffle = shuffle + self.schema["ui"]["shuffle"] = self.ui.shuffle + + def set_ui_allow( + self, + auto_advance: bool = True, + allow_export: bool = True, + disable_back: bool = False, + ) -> None: + self.ui.AutoAdvance(auto_advance) + self.ui.DisableBack(disable_back) + self.ui.AllowExport(allow_export) + self.schema["ui"]["allow"] = self.ui.allow + + def append_to_ui(self, obj, properties: dict) -> None: + if obj.skippable: + properties["allow"] = ["reproschema:Skipped"] + + self.ui.order.append(obj.URI) + self.ui.add_properties.append(properties) + self.schema["ui"]["order"].append(obj.URI) + self.schema["ui"]["addProperties"].append(properties) """ writing, reading, sorting, unsetting @@ -215,50 +227,51 @@ def set_ui_allow(self, auto_advance=True, allow_export=True, disable_back=False) # This will require modifying the helper function `reorder_dict_skip_missing` # - def sort_schema(self, schema_order): - """ - The ``schema_order`` is specific to each "level" of the reproschema - (protocol, activity, item) so that each can be reordered in its own way - """ - reordered_dict = reorder_dict_skip_missing(self.schema, schema_order) + def sort(self): + self.sort_schema() + self.sort_ui() + + def sort_schema(self) -> None: + reordered_dict = self.reorder_dict_skip_missing(self.schema, self.schema_order) self.schema = reordered_dict - def sort_ui(self, ui_order=["shuffle", "order", "addProperties", "allow"]): - reordered_dict = reorder_dict_skip_missing(self.schema["ui"], ui_order) + def sort_ui(self) -> None: + ui_order = self.ui.schema_order + reordered_dict = self.reorder_dict_skip_missing(self.schema["ui"], ui_order) self.schema["ui"] = reordered_dict - def __write(self, output_dir): + def __write(self, output_dir: Union[str, Path]) -> None: """ Reused by the write method of the children classes """ if isinstance(output_dir, str): output_dir = Path(output_dir) - with open(output_dir.joinpath(self.schema_file), "w") as ff: + with open(output_dir.joinpath(self.at_id), "w") as ff: json.dump(self.schema, ff, sort_keys=False, indent=4) @classmethod - def from_data(cls, data): + def from_data(cls, data: dict): klass = cls() - if klass.schema_type is None: + if klass.type is None: raise ValueError("SchemaBase cannot be used to instantiate class") - if klass.schema_type != data["@type"]: - raise ValueError(f"Mismatch in type {data['@type']} != {klass.schema_type}") + if klass.type != data["@type"]: + raise ValueError(f"Mismatch in type {data['@type']} != {klass.type}") klass.schema = data return klass @classmethod - def from_file(cls, filepath): + def from_file(cls, filepath: Union[str, Path]): with open(filepath) as fp: data = json.load(fp) if "@type" not in data: raise ValueError("Missing @type key") return cls.from_data(data) - -def reorder_dict_skip_missing(old_dict, key_list): - """ - reorders dictionary according to ``key_list`` - removing any key with no associated value - or that is not in the key list - """ - return OrderedDict((k, old_dict[k]) for k in key_list if k in old_dict) + @staticmethod + def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> Dict: + """ + reorders dictionary according to ``key_list`` + removing any key with no associated value + or that is not in the key list + """ + return OrderedDict((k, old_dict[k]) for k in key_list if k in old_dict) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 5513530..9de7d35 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -1,44 +1,66 @@ from .base import SchemaBase +from .response_options import ResponseOption DEFAULT_LANG = "en" +schema_order = [ + "@context", + "@type", + "@id", + "prefLabel", + "description", + "schemaVersion", + "version", + "ui", + "question", + "responseOptions", +] + + class Item(SchemaBase): """ class to deal with reproschema items """ - visible = True - required = False - skippable = True + def __init__( + self, + name: str = "item", + input_type: str = "text", + question: str = "", + schemaVersion=None, + suffix="", + ext=".jsonld", + visible: bool = True, + required: bool = False, + skippable: bool = True, + ): + super().__init__( + at_id=name, + schemaVersion=schemaVersion, + type="reproschema:Field", + schema_order=schema_order, + suffix=suffix, + ext=ext, + ) + self.schema["ui"] = self.ui.schema + self.schema["question"] = {self.lang: question} + self.input_type = input_type + self.visible = visible + self.required = required + self.skippable = skippable + self.question = question - def __init__(self, version=None): - super().__init__(version=version, schema_type="reproschema:Field") - self.schema["ui"] = {"inputType": []} - self.schema["question"] = {} """ - The responseOptions dictionnary is kept empty until the file has to be written + The responseOptions dictionary is kept empty until the file has to be written then it gets its content wit the method `set_response_options` - from the `options` dictionnary of an instance of the ResponseOptions class that is kept in - ``self.response_options`` + from the `options` dictionary of an instance of the ResponseOptions class + that is kept in ``self.response_options`` """ - self.schema["responseOptions"] = {} - self.response_options = ResponseOption() - - # default input type is "text" - self.set_input_type_as_text() - - def set_defaults(self, name="default"): - self._SchemaBase__set_defaults(name) - self.set_filename(name) - self.set_input_type_as_text() + self.response_options: ResponseOption = ResponseOption() + self.set_response_options() - def set_filename(self, name, ext=".jsonld"): - """ - Note there is no _schema suffix for items names - """ - name = name.replace(" ", "_") - self.schema_file = name + ext - self.schema["@id"] = name + ext + super().set_defaults(self.at_id) + self.set_input_type() def set_question(self, question, lang=DEFAULT_LANG): # TODO add test to check adding several questions to an item @@ -59,69 +81,60 @@ def set_question(self, question, lang=DEFAULT_LANG): # static: Static/Static.vue # StaticReadOnly: Static/Static.vue - def set_basic_response_type(self, response_type): - """ - Handles the dispatching to other methods for specific item creations - Does not cover all items types (maybe it should as this would help - from an API point of view to pass everything through this function) - The default is "text" input type - """ - self.set_input_type_as_text() - - if response_type == "int": - self.set_input_type_as_int() - - elif response_type == "float": - self.set_input_type_as_float() - - elif response_type == "date": - self.set_input_type_as_date() - - elif response_type == "time range": - self.set_input_type_as_time_range() - - elif response_type == "language": + def set_input_type(self): + if not self.input_type: + return + if self.input_type == "text": + self.set_input_type_as_text() + if self.input_type == "multitext": + self.set_input_type_as_multitext() + elif self.input_type == "int": + self.set_input_type_numeric("number", "integer") + elif self.input_type == "float": + self.set_input_type_numeric("float", "float") + elif self.input_type == "year": + self.schema["ui"]["inputType"] = "year" + self.response_options.set_type("date") + self.response_options.unset( + ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] + ) + elif self.input_type == "date": + self.schema["ui"]["inputType"] = "date" + self.response_options.set_type("date") + self.response_options.unset( + ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] + ) + elif self.input_type == "time_range": + self.schema["ui"]["inputType"] = "timeRange" + self.response_options.set_type("datetime") + self.response_options.unset( + ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] + ) + elif self.input_type == "language": self.set_input_type_as_language() - - """ - input types with different response choices - - For many items it is necessary to call - - self.response_options.unset - - To remove unecessary or unwanted keys from the response_options - dictionary. - Many of those are put there by the constructor of that set - the default input type as ``self.set_input_type_as_text()`` - so it might be better to maybe have a more minimalistic constructor. - - """ - - def set_input_type_as_int(self): - self.set_input_type("number") - self.response_options.set_type("integer") - self.response_options.unset(["maxLength"]) - - def set_input_type_as_float(self): - self.set_input_type("float") - self.response_options.set_type("float") - self.response_options.unset(["maxLength"]) - - def set_input_type_as_date(self): - self.set_input_type("date") - self.response_options.unset(["maxLength"]) - self.response_options.set_type("date") - - def set_input_type_as_time_range(self): - self.set_input_type("timeRange") - self.response_options.unset(["maxLength"]) - self.response_options.set_type("datetime") - - def set_input_type_as_year(self): - self.set_input_type("year") - self.response_options.unset(["maxLength"]) - self.response_options.set_type("date") + elif self.input_type == "country": + self.set_input_type_as_country() + elif self.input_type == "state": + self.set_input_type_as_state() + elif self.input_type == "email": + self.schema["ui"]["inputType"] = "email" + self.response_options.set_type("string") + self.response_options.unset( + ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] + ) + elif self.input_type == "id": + self.schema["ui"]["inputType"] = "pid" + self.response_options.set_type("string") + self.response_options.unset( + ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] + ) + + def set_input_type_numeric(self, arg0, arg1): + self.schema["ui"]["inputType"] = arg0 + self.response_options.set_type(arg1) + self.response_options.unset( + ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] + ) """ input types with preset response choices @@ -129,63 +142,60 @@ def set_input_type_as_year(self): def set_input_type_as_language(self): - URL = "https://raw.githubusercontent.com/ReproNim/reproschema-library/" - - self.set_input_type("selectLanguage") + URL = self.set_input_from_preset( + "https://raw.githubusercontent.com/ReproNim/reproschema-library/", + "selectLanguage", + ) - self.response_options.set_type("string") self.response_options.set_multiple_choice(True) - self.response_options.use_preset(URL + "master/resources/languages.json") - self.response_options.unset(["maxLength"]) + self.response_options.use_preset(f"{URL}master/resources/languages.json") def set_input_type_as_country(self): - URL = "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json" - - self.set_input_type("selectCountry") + URL = self.set_input_from_preset( + "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json", + "selectCountry", + ) - self.response_options.set_type("string") self.response_options.use_preset(URL) self.response_options.set_length(50) def set_input_type_as_state(self): - URL = "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json" + URL = self.set_input_from_preset( + "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json", + "selectState", + ) - self.set_input_type("selectState") + self.response_options.use_preset(URL) + def set_input_from_preset(self, arg0, arg1): + result = arg0 + self.schema["ui"]["inputType"] = arg1 self.response_options.set_type("string") - self.response_options.use_preset(URL) - self.response_options.unset(["maxLength"]) + self.response_options.unset( + ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] + ) + return result """ input types requiring user typed input """ def set_input_type_as_text(self, length=300): - self.set_input_type("text") - self.response_options.set_type("string") - self.response_options.set_length(length) + self.set_input_type_generic_text("text", length) self.response_options.unset( ["maxValue", "minValue", "multipleChoice", "choices"] ) def set_input_type_as_multitext(self, length=300): - self.set_input_type("multitext") + self.set_input_type_generic_text("multitext", length) + + def set_input_type_generic_text(self, arg0, length): + self.schema["ui"]["inputType"] = arg0 self.response_options.set_type("string") self.response_options.set_length(length) - def set_input_type_as_email(self): - self.set_input_type("email") - self.response_options.unset(["maxLength"]) - - def set_input_type_as_id(self): - """ - for participant id items - """ - self.set_input_type("pid") - self.response_options.unset(["maxLength"]) - """ input types with 'different response choices' @@ -199,18 +209,17 @@ def set_input_type_as_id(self): self.response_options """ - def set_input_type_as_radio(self, response_options): - self.set_input_type("radio") - response_options.set_type("integer") - self.response_options = response_options + def set_input_type_as_radio(self, response_options: ResponseOption): + self.set_input_type_rasesli("radio", response_options) - def set_input_type_as_select(self, response_options): - self.set_input_type("select") - response_options.set_type("integer") - self.response_options = response_options + def set_input_type_as_select(self, response_options: ResponseOption): + self.set_input_type_rasesli("select", response_options) + + def set_input_type_as_slider(self, response_options: ResponseOption): + self.set_input_type_rasesli("slider", response_options) - def set_input_type_as_slider(self, response_options): - self.set_input_type("slider") + def set_input_type_rasesli(self, arg0, response_options: ResponseOption): + self.schema["ui"]["inputType"] = arg0 response_options.set_type("integer") self.response_options = response_options @@ -221,8 +230,8 @@ def set_input_type_as_slider(self, response_options): # or should they be brought up into the base class? # or be made part of an UI class? - def set_input_type(self, input_type): - self.schema["ui"]["inputType"] = input_type + # def set_input_type(self, input_type): + # self.schema["ui"]["inputType"] = input_type def set_read_only_value(self, value): self.schema["ui"]["readonlyValue"] = value @@ -251,118 +260,4 @@ def write(self, output_dir): self._SchemaBase__write(output_dir) def sort(self): - schema_order = [ - "@context", - "@type", - "@id", - "prefLabel", - "description", - "schemaVersion", - "version", - "ui", - "question", - "responseOptions", - ] - self.sort_schema(schema_order) - - -class ResponseOption(SchemaBase): - """ - class to deal with reproschema response options - """ - - # TODO - # the dictionnary that keeps track of the content of the response options should - # be called "schema" and not "options" so as to be able to make proper use of the - # methods of the parent class and avoid copying content between - # - # self.options and self.schema - - def __init__(self): - self.options = { - "valueType": "", - "minValue": 0, - "maxValue": 0, - "choices": [], - "multipleChoice": False, - } - - def set_defaults(self, name="valueConstraints", version=None): - super().__init__(version=version, schema_type="reproschema:ResponseOption") - self.options["@context"] = self.schema["@context"] - self.options["@type"] = self.schema_type - self.set_filename(name) - - def set_filename(self, name, ext=".jsonld"): - name = name.replace(" ", "_") - self.schema_file = name + ext - self.options["@id"] = name + ext - - def unset(self, keys): - if type(keys) == str: - keys = [keys] - for i in keys: - self.options.pop(i, None) - - def set_type(self, type): - self.options["valueType"] = f"xsd:{type}" - - # TODO a nice thing to do would be to read the min and max value - # from the rest of the content of self.options - # could avoid having the user to input those - def set_min(self, value): - self.options["minValue"] = value - - def set_max(self, value): - self.options["maxValue"] = value - - def set_length(self, value): - self.options["maxLength"] = value - - def set_multiple_choice(self, value): - self.options["multipleChoice"] = value - - def use_preset(self, URI): - """ - In case the list response options are read from another file - like for languages, country, state... - """ - self.options["choices"] = URI - - def add_choice(self, choice, value, lang=DEFAULT_LANG): - self.options["choices"].append({"name": {lang: choice}, "value": value}) - - def sort(self): - options_order = [ - "@context", - "@type", - "@id", - "valueType", - "minValue", - "maxValue", - "multipleChoice", - "choices", - ] - reordered_dict = reorder_dict_skip_missing(self.options, options_order) - self.options = reordered_dict - - def write(self, output_dir): - self.sort() - self.schema = self.options - self._SchemaBase__write(output_dir) - - -# TODO -# DUPLICATE from the base class to be used for ResponseOptions sorting of the options -# needs refactoring that will be made easier if the name the ResponseOptions dictionary is -# schema and note options -from collections import OrderedDict - - -def reorder_dict_skip_missing(old_dict, key_list): - """ - reorders dictionary according to ``key_list`` - removing any key with no associated value - or that is not in the key list - """ - return OrderedDict((k, old_dict[k]) for k in key_list if k in old_dict) + self.sort_schema() diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index 2418ae9..681650d 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -1,32 +1,66 @@ +from .activity import Activity from .base import SchemaBase + DEFAULT_LANG = "en" +schema_order = [ + "@context", + "@type", + "@id", + "prefLabel", + "description", + "schemaVersion", + "version", + "landingPage", + "preamble", + "citation", + "image", + "compute", + "ui", +] + class Protocol(SchemaBase): """ class to deal with reproschema protocols """ - - def __init__(self, version=None): + def __init__( + self, + name="protocol", + schemaVersion=None, + prefLabel="protocol", + suffix="_schema", + ext=".jsonld", + lang=DEFAULT_LANG, + ): """ Rely on the parent class for construction of the instance """ - super().__init__(version=version, schema_type="reproschema:Protocol") - - def set_defaults(self, name="default"): - self._SchemaBase__set_defaults(name) + super().__init__( + at_id=name, + schemaVersion=schemaVersion, + type="reproschema:Protocol", + prefLabel={lang: prefLabel}, + schema_order=schema_order, + suffix=suffix, + ext=ext, + lang=lang, + ) + + def set_defaults(self, name=None): + if not name: + name = self.at_id + super().set_defaults(name) self.set_landing_page("README-en.md") - # does it make sense to give a preamble by default to protocols since - # they already have a landing page? - self.set_preamble() + self.set_preamble(name) self.set_ui_default() - def set_landing_page(self, landing_page_uri, lang=DEFAULT_LANG): + def set_landing_page(self, landing_page_uri: str, lang=DEFAULT_LANG): self.schema["landingPage"] = {"@id": landing_page_uri, "inLanguage": lang} - def append_activity(self, activity): + def append_activity(self, activity: Activity): """ We get from an activity instance the info we need to update the protocol scheme. @@ -41,47 +75,21 @@ def append_activity(self, activity): # TODO # - find a way to reorder, remove or add an activity # at any point in the protocol - # - this method is nearly identical to the append_item method of Activity - # and should probably be refactored into a single method of the parent class - # and ideally into a method of a yet to be created UI class - property = { + activity_property = { # variable name is name of activity without prefix "variableName": activity.get_basename().replace("_schema", ""), - "isAbout": activity.get_URI(), - "prefLabel": activity.get_pref_label(), + "isAbout": activity.URI, + "prefLabel": activity.prefLabel, "isVis": activity.visible, "requiredValue": activity.required, } - if activity.skippable: - property["allow"] = ["reproschema:Skipped"] - - self.schema["ui"]["order"].append(activity.get_URI()) - self.schema["ui"]["addProperties"].append(property) + self.append_to_ui(activity, activity_property) """ writing, reading, sorting, unsetting """ - def sort(self): - schema_order = [ - "@context", - "@type", - "@id", - "prefLabel", - "description", - "schemaVersion", - "version", - "landingPage", - "preamble", - "citation", - "image", - "compute", - "ui", - ] - self.sort_schema(schema_order) - self.sort_ui() - def write(self, output_dir): self.sort() self._SchemaBase__write(output_dir) diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py new file mode 100644 index 0000000..17f53b8 --- /dev/null +++ b/reproschema/models/response_options.py @@ -0,0 +1,95 @@ +from .base import SchemaBase + +DEFAULT_LANG = "en" + +options_order = [ + "@context", + "@type", + "@id", + "valueType", + "minValue", + "maxValue", + "multipleChoice", + "choices", +] + + +class ResponseOption(SchemaBase): + """ + class to deal with reproschema response options + """ + + # the dictionary that keeps track of the content of the response options should + # be called "schema" and not "options" so as to be able to make proper use of the + # methods of the parent class and avoid copying content between + # + # self.options and self.schema + + def __init__(self, name="valueConstraints", schemaVersion=None): + super().__init__( + at_id=name, + schema_order=options_order, + schemaVersion=schemaVersion, + type="reproschema:ResponseOption", + ext=".jsonld", + suffix="", + ) + self.options = { + "valueType": "", + "minValue": 0, + "maxValue": 0, + "choices": [], + "multipleChoice": False, + } + + def set_defaults(self): + self.options["@context"] = self.schema["@context"] + self.options["@type"] = self.type + self.set_filename(self.at_id) + + def set_filename(self, name: str): + super().set_filename(name) + self.options["@id"] = self.at_id + + def unset(self, keys): + if type(keys) == str: + keys = [keys] + for i in keys: + self.options.pop(i, None) + + def set_type(self, type): + self.options["valueType"] = f"xsd:{type}" + + # TODO a nice thing to do would be to read the min and max value + # from the rest of the content of self.options + # could avoid having the user to input those + def set_min(self, value): + self.options["minValue"] = value + + def set_max(self, value): + self.options["maxValue"] = value + + def set_length(self, value): + self.options["maxLength"] = value + + def set_multiple_choice(self, value): + self.options["multipleChoice"] = value + + def use_preset(self, URI): + """ + In case the list response options are read from another file + like for languages, country, state... + """ + self.options["choices"] = URI + + def add_choice(self, choice, value, lang=DEFAULT_LANG): + self.options["choices"].append({"name": {lang: choice}, "value": value}) + + def sort(self): + reordered_dict = self.reorder_dict_skip_missing(self.options, self.schema_order) + self.options = reordered_dict + + def write(self, output_dir): + self.sort() + self.schema = self.options + self._SchemaBase__write(output_dir) diff --git a/reproschema/models/tests/__init__.py b/reproschema/models/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/reproschema/models/tests/data/activities/default_schema.jsonld b/reproschema/models/tests/data/activities/default_schema.jsonld index 9efd947..2647274 100644 --- a/reproschema/models/tests/data/activities/default_schema.jsonld +++ b/reproschema/models/tests/data/activities/default_schema.jsonld @@ -9,7 +9,7 @@ "schemaVersion": "1.0.0-rc4", "version": "0.0.1", "preamble": { - "en": "" + "en": "default" }, "ui": { "shuffle": false, diff --git a/reproschema/models/tests/data/items/default.jsonld b/reproschema/models/tests/data/items/default.jsonld index f4bbbfe..944f630 100644 --- a/reproschema/models/tests/data/items/default.jsonld +++ b/reproschema/models/tests/data/items/default.jsonld @@ -11,7 +11,7 @@ "ui": { "inputType": "text" }, - "question": {}, + "question": {"en": ""}, "responseOptions": { "valueType": "xsd:string", "maxLength": 300 diff --git a/reproschema/models/tests/data/items/text.jsonld b/reproschema/models/tests/data/items/text.jsonld index d27c641..7e75772 100644 --- a/reproschema/models/tests/data/items/text.jsonld +++ b/reproschema/models/tests/data/items/text.jsonld @@ -16,6 +16,6 @@ }, "responseOptions": { "valueType": "xsd:string", - "maxLength": 100 + "maxLength": 300 } } diff --git a/reproschema/models/tests/data/protocols/default_schema.jsonld b/reproschema/models/tests/data/protocols/default_schema.jsonld index 7612d91..ff4df75 100644 --- a/reproschema/models/tests/data/protocols/default_schema.jsonld +++ b/reproschema/models/tests/data/protocols/default_schema.jsonld @@ -13,7 +13,7 @@ "inLanguage": "en" }, "preamble": { - "en": "" + "en": "default" }, "ui": { "allow": [ diff --git a/reproschema/models/tests/data/protocols/protocol1_schema.jsonld b/reproschema/models/tests/data/protocols/protocol1_schema.jsonld index 38426ce..303ba2e 100644 --- a/reproschema/models/tests/data/protocols/protocol1_schema.jsonld +++ b/reproschema/models/tests/data/protocols/protocol1_schema.jsonld @@ -13,7 +13,7 @@ "inLanguage": "en" }, "preamble": { - "en": "" + "en": "protocol1" }, "ui": { "addProperties": [ diff --git a/reproschema/models/tests/items/country b/reproschema/models/tests/items/country new file mode 100644 index 0000000..ea63ac2 --- /dev/null +++ b/reproschema/models/tests/items/country @@ -0,0 +1,22 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "country", + "prefLabel": { + "en": "country" + }, + "description": "country", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "selectCountry" + }, + "question": { + "en": "select a country" + }, + "responseOptions": { + "valueType": "xsd:string", + "maxLength": 50, + "choices": "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json" + } +} diff --git a/reproschema/models/tests/items/date b/reproschema/models/tests/items/date new file mode 100644 index 0000000..3bb61ba --- /dev/null +++ b/reproschema/models/tests/items/date @@ -0,0 +1,20 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "date", + "prefLabel": { + "en": "date" + }, + "description": "date", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "date" + }, + "question": { + "en": "input a date" + }, + "responseOptions": { + "valueType": "xsd:date" + } +} diff --git a/reproschema/models/tests/items/default b/reproschema/models/tests/items/default new file mode 100644 index 0000000..65a5e2f --- /dev/null +++ b/reproschema/models/tests/items/default @@ -0,0 +1,19 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "default", + "prefLabel": { + "en": "default" + }, + "description": "default", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "text" + }, + "question": {}, + "responseOptions": { + "valueType": "xsd:string", + "maxLength": 300 + } +} diff --git a/reproschema/models/tests/items/email b/reproschema/models/tests/items/email new file mode 100644 index 0000000..fd711a8 --- /dev/null +++ b/reproschema/models/tests/items/email @@ -0,0 +1,20 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "email", + "prefLabel": { + "en": "email" + }, + "description": "email", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "email" + }, + "question": { + "en": "input email address" + }, + "responseOptions": { + "valueType": "xsd:string" + } +} diff --git a/reproschema/models/tests/items/float b/reproschema/models/tests/items/float new file mode 100644 index 0000000..b73614d --- /dev/null +++ b/reproschema/models/tests/items/float @@ -0,0 +1,20 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "float", + "prefLabel": { + "en": "float" + }, + "description": "This is a float item.", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "float" + }, + "question": { + "en": "This is an item where the user can input a float." + }, + "responseOptions": { + "valueType": "xsd:float" + } +} diff --git a/reproschema/models/tests/items/integer b/reproschema/models/tests/items/integer new file mode 100644 index 0000000..60fa880 --- /dev/null +++ b/reproschema/models/tests/items/integer @@ -0,0 +1,20 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "integer", + "prefLabel": { + "en": "integer" + }, + "description": "This is a integer item.", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "number" + }, + "question": { + "en": "This is an item where the user can input a integer." + }, + "responseOptions": { + "valueType": "xsd:integer" + } +} diff --git a/reproschema/models/tests/items/language b/reproschema/models/tests/items/language new file mode 100644 index 0000000..059e4ea --- /dev/null +++ b/reproschema/models/tests/items/language @@ -0,0 +1,22 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "language", + "prefLabel": { + "en": "language" + }, + "description": "language", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "selectLanguage" + }, + "question": { + "en": "This is an item where the user can select several language." + }, + "responseOptions": { + "valueType": "xsd:string", + "multipleChoice": true, + "choices": "https://raw.githubusercontent.com/ReproNim/reproschema-library/master/resources/languages.json" + } +} diff --git a/reproschema/models/tests/items/multitext b/reproschema/models/tests/items/multitext new file mode 100644 index 0000000..0753a0b --- /dev/null +++ b/reproschema/models/tests/items/multitext @@ -0,0 +1,21 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "multitext", + "prefLabel": { + "en": "multitext" + }, + "description": "multitext", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "multitext" + }, + "question": { + "en": "This is an item where the user can input several text field." + }, + "responseOptions": { + "valueType": "xsd:string", + "maxLength": 50 + } +} diff --git a/reproschema/models/tests/items/participant_id b/reproschema/models/tests/items/participant_id new file mode 100644 index 0000000..a32bac9 --- /dev/null +++ b/reproschema/models/tests/items/participant_id @@ -0,0 +1,20 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "participant_id", + "prefLabel": { + "en": "participant id" + }, + "description": "participant id", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "pid" + }, + "question": { + "en": "input the participant id number" + }, + "responseOptions": { + "valueType": "xsd:string" + } +} diff --git a/reproschema/models/tests/items/radio b/reproschema/models/tests/items/radio new file mode 100644 index 0000000..7a21602 --- /dev/null +++ b/reproschema/models/tests/items/radio @@ -0,0 +1,37 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "radio", + "prefLabel": { + "en": "radio" + }, + "description": "radio", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "radio" + }, + "question": { + "en": "question for radio item" + }, + "responseOptions": { + "valueType": "xsd:integer", + "minValue": 0, + "maxValue": 1, + "choices": [ + { + "name": { + "en": "Not at all" + }, + "value": 0 + }, + { + "name": { + "en": "Several days" + }, + "value": 1 + } + ], + "multipleChoice": false + } +} diff --git a/reproschema/models/tests/items/select b/reproschema/models/tests/items/select new file mode 100644 index 0000000..3464c17 --- /dev/null +++ b/reproschema/models/tests/items/select @@ -0,0 +1,43 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "select", + "prefLabel": { + "en": "select" + }, + "description": "select", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "select" + }, + "question": { + "en": "question for select item" + }, + "responseOptions": { + "valueType": "xsd:integer", + "minValue": 0, + "maxValue": 2, + "choices": [ + { + "name": { + "en": "Response option 1" + }, + "value": 0 + }, + { + "name": { + "en": "Response option 2" + }, + "value": 1 + }, + { + "name": { + "en": "Response option 3" + }, + "value": 2 + } + ], + "multipleChoice": false + } +} diff --git a/reproschema/models/tests/items/slider b/reproschema/models/tests/items/slider new file mode 100644 index 0000000..7cdf98d --- /dev/null +++ b/reproschema/models/tests/items/slider @@ -0,0 +1,55 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "slider", + "prefLabel": { + "en": "slider" + }, + "description": "slider", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "slider" + }, + "question": { + "en": "question for slider item" + }, + "responseOptions": { + "valueType": "xsd:integer", + "minValue": 0, + "maxValue": 4, + "choices": [ + { + "name": { + "en": "not at all" + }, + "value": 0 + }, + { + "name": { + "en": "a bit" + }, + "value": 1 + }, + { + "name": { + "en": "so so" + }, + "value": 2 + }, + { + "name": { + "en": "a lot" + }, + "value": 3 + }, + { + "name": { + "en": "very much" + }, + "value": 4 + } + ], + "multipleChoice": false + } +} diff --git a/reproschema/models/tests/items/state b/reproschema/models/tests/items/state new file mode 100644 index 0000000..7e326a9 --- /dev/null +++ b/reproschema/models/tests/items/state @@ -0,0 +1,21 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "state", + "prefLabel": { + "en": "state" + }, + "description": "state", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "selectState" + }, + "question": { + "en": "select a USA state" + }, + "responseOptions": { + "valueType": "xsd:string", + "choices": "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json" + } +} diff --git a/reproschema/models/tests/items/text b/reproschema/models/tests/items/text new file mode 100644 index 0000000..0a1ec36 --- /dev/null +++ b/reproschema/models/tests/items/text @@ -0,0 +1,21 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "text", + "prefLabel": { + "en": "text" + }, + "description": "text", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "text" + }, + "question": { + "en": "question for text item" + }, + "responseOptions": { + "valueType": "xsd:string", + "maxLength": 100 + } +} diff --git a/reproschema/models/tests/items/time_range b/reproschema/models/tests/items/time_range new file mode 100644 index 0000000..cb29f76 --- /dev/null +++ b/reproschema/models/tests/items/time_range @@ -0,0 +1,20 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "time_range", + "prefLabel": { + "en": "time range" + }, + "description": "time range", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "timeRange" + }, + "question": { + "en": "input a time range" + }, + "responseOptions": { + "valueType": "xsd:datetime" + } +} diff --git a/reproschema/models/tests/items/year b/reproschema/models/tests/items/year new file mode 100644 index 0000000..4593400 --- /dev/null +++ b/reproschema/models/tests/items/year @@ -0,0 +1,20 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Field", + "@id": "year", + "prefLabel": { + "en": "year" + }, + "description": "year", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "ui": { + "inputType": "year" + }, + "question": { + "en": "input a year" + }, + "responseOptions": { + "valueType": "xsd:date" + } +} diff --git a/reproschema/models/tests/protocols/protocol1 b/reproschema/models/tests/protocols/protocol1 new file mode 100644 index 0000000..81ddcc5 --- /dev/null +++ b/reproschema/models/tests/protocols/protocol1 @@ -0,0 +1,77 @@ +{ + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Protocol", + "@id": "protocol1", + "prefLabel": { + "en": "Protocol1" + }, + "description": "example Protocol", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "landingPage": { + "@id": "http://example.com/sample-readme.md", + "inLanguage": "en" + }, + "ui": { + "order": [ + null, + "items/item1.jsonld", + "../other_dir/item_two.jsonld", + "items/activity1_total_score", + "../activities/activity1_schema.jsonld", + "../activities/activity1_schema.jsonld" + ], + "addProperties": [ + { + "variableName": "item1", + "isAbout": "items/item1.jsonld", + "isVis": true, + "requiredValue": true + }, + { + "variableName": "item_two", + "isAbout": "../other_dir/item_two.jsonld", + "isVis": true, + "requiredValue": true, + "allow": [ + "reproschema:Skipped" + ] + }, + { + "variableName": "activity1_total_score", + "isAbout": "items/activity1_total_score", + "isVis": false, + "requiredValue": true + }, + { + "variableName": "activity1", + "isAbout": "../activities/activity1_schema.jsonld", + "prefLabel": { + "en": "Screening" + }, + "isVis": true, + "requiredValue": false, + "allow": [ + "reproschema:Skipped" + ] + }, + { + "variableName": "activity1", + "isAbout": "../activities/activity1_schema.jsonld", + "prefLabel": { + "en": "Screening" + }, + "isVis": true, + "requiredValue": false, + "allow": [ + "reproschema:Skipped" + ] + } + ], + "allow": [ + "reproschema:AutoAdvance", + "reproschema:AllowExport", + "reproschema:DisableBack" + ] + } +} diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index b4c72d0..9dd8397 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -1,24 +1,13 @@ -import os, sys, json +import os -from ..activity import Activity -from ..item import Item +from utils import clean_up +from utils import load_jsons +from utils import output_dir -my_path = os.path.dirname(os.path.abspath(__file__)) +from reproschema.models.activity import Activity +from reproschema.models.item import Item -# Left here in case Remi and python path or import can't be friends once again. -# sys.path.insert(0, my_path + "/../") - -# TODO -# refactor across the different test modules -activity_dir = os.path.join(my_path, "activities") -if not os.path.exists(activity_dir): - os.makedirs(os.path.join(activity_dir)) - -""" -Only for the few cases when we want to check against some of the files in -reproschema/tests/data -""" -reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") +activity_dir = output_dir("activities") def test_default(): @@ -28,20 +17,21 @@ def test_default(): so `reproschema validate` will complain if you run it in this """ - activity = Activity() + activity = Activity(name="default") activity.set_defaults() activity.write(activity_dir) - activity_content, expected = load_jsons(activity) + + activity_content, expected = load_jsons(activity_dir, activity) assert activity_content == expected - clean_up(activity) + clean_up(activity_dir, activity) def test_activity(): - activity = Activity() - activity.set_defaults("activity1") + activity = Activity(name="activity1") + activity.set_defaults() activity.set_description("Activity example 1") activity.set_pref_label("Example 1") activity.set_preamble( @@ -53,11 +43,10 @@ def test_activity(): ) activity.set_compute("activity1_total_score", "item1 + item2") - item_1 = Item() - item_1.set_defaults("item1") + item_1 = Item(name="item1") # TODO # probably want to have items/item_name be a default - item_1.set_URI(os.path.join("items", item_1.get_filename())) + item_1.URI = os.path.join("items", item_1.at_id) # TODO # We probably want a method to change those values rather that modifying # the instance directly @@ -68,58 +57,28 @@ def test_activity(): """ activity.append_item(item_1) - item_2 = Item() - item_2.set_defaults("item2") + item_2 = Item(name="item2") item_2.set_filename("item_two") """ In this case the URI is relative to where the activity file will be saved """ - item_2.set_URI(os.path.join("..", "other_dir", item_2.get_filename())) + item_2.URI = os.path.join("..", "other_dir", item_2.at_id) item_2.required = True activity.append_item(item_2) - item_3 = Item() - item_3.set_defaults("activity1_total_score") """ - By default all files are save with a json.ld extension but this can be changed + By default all files are save with a jsonld extension but this can be changed """ - file_ext = "" - item_3.set_filename("activity1_total_score", file_ext) - item_3.set_URI(os.path.join("items", item_3.get_filename())) + item_3 = Item(name="activity1_total_score", ext="") + item_3.URI = os.path.join("items", item_3.at_id) item_3.skippable = False item_3.required = True item_3.visible = False activity.append_item(item_3) activity.write(activity_dir) - activity_content, expected = load_jsons(activity) + activity_content, expected = load_jsons(activity_dir, activity) assert activity_content == expected - clean_up(activity) - - -""" -HELPER FUNCTIONS -""" - - -def load_jsons(obj): - - output_file = os.path.join(activity_dir, obj.get_filename()) - content = read_json(output_file) - - data_file = os.path.join(my_path, "data", "activities", obj.get_filename()) - expected = read_json(data_file) - - return content, expected - - -def read_json(file): - - with open(file, "r") as ff: - return json.load(ff) - - -def clean_up(obj): - os.remove(os.path.join(activity_dir, obj.get_filename())) + clean_up(activity_dir, activity) diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index a5157d8..2c6a163 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -1,35 +1,26 @@ -import os, sys, json +import os +from pathlib import Path -from ..item import Item, ResponseOption +from utils import clean_up +from utils import load_jsons +from utils import output_dir +from utils import read_json -my_path = os.path.dirname(os.path.abspath(__file__)) +from reproschema.models.item import Item +from reproschema.models.item import ResponseOption -# Left here in case Remi and python path or import can't be friends once again. -# sys.path.insert(0, my_path + "/../") - -# TODO -# refactor across the different test modules -item_dir = os.path.join(my_path, "items") -if not os.path.exists(item_dir): - os.makedirs(os.path.join(item_dir)) - -""" -Only for the few cases when we want to check against some of the files in -reproschema/tests/data -""" -reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") +item_dir = output_dir("items") def test_default(): - item = Item() - item.set_defaults() + item = Item(name="default") item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) # TODO @@ -42,36 +33,29 @@ def test_default(): def test_text(): - text_length = 100 - - item = Item("1.0.0-rc4") - item.set_defaults("text") - item.set_input_type_as_text(text_length) - - item.set_question("question for text item") + item = Item(name="text", question="question for text item", input_type="text") item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) def test_multitext(): text_length = 50 - item = Item("1.0.0-rc4") - item.set_defaults("multitext") + item = Item(name="multitext") item.set_input_type_as_multitext(text_length) item.set_question("This is an item where the user can input several text field.") item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) """ @@ -81,77 +65,63 @@ def test_multitext(): def test_email(): - item = Item("1.0.0-rc4") - item.set_defaults("email") - item.set_input_type_as_email() - - item.set_question("input email address") + item = Item(name="email", question="input email address", input_type="email") item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) def test_participant_id(): - item = Item("1.0.0-rc4") - item.set_defaults("participant id") - item.set_input_type_as_id() - - item.set_question("input the participant id number") + item = Item( + name="participant id", + question="input the participant id number", + input_type="id", + ) item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) def test_date(): - item = Item("1.0.0-rc4") - item.set_defaults("date") - item.set_input_type_as_date() - - item.set_question("input a date") + item = Item(name="date", question="input a date", input_type="date") item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) def test_time_range(): - item = Item("1.0.0-rc4") - item.set_defaults("time range") - item.set_input_type_as_time_range() - - item.set_question("input a time range") + item = Item( + name="time range", question="input a time range", input_type="time_range" + ) item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) def test_year(): - item = Item("1.0.0-rc4") - item.set_defaults("year") - item.set_input_type_as_year() - - item.set_question("input a year") + item = Item(name="year", question="input a year", input_type="year") item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) """ @@ -161,44 +131,39 @@ def test_year(): def test_language(): - item = Item() - item.set_defaults("language") - item.set_input_type_as_language() - item.set_question("This is an item where the user can select several language.") + item = Item( + name="language", + question="This is an item where the user can select several language.", + input_type="language", + ) item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) def test_country(): - item = Item() - item.set_defaults("country") - item.set_input_type_as_country() - item.set_question("select a country") + item = Item(name="country", question="select a country", input_type="country") item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) def test_state(): - item = Item() - item.set_defaults("state") - item.set_input_type_as_state() - item.set_question("select a USA state") + item = Item(name="state", question="select a USA state", input_type="state") item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) """ @@ -208,32 +173,34 @@ def test_state(): def test_float(): - item = Item("1.0.0-rc4") - item.set_defaults("float") + item = Item( + name="float", + question="This is an item where the user can input a float.", + input_type="float", + ) item.set_description("This is a float item.") - item.set_input_type_as_float() - item.set_question("This is an item where the user can input a float.") item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) def test_integer(): - item = Item() - item.set_defaults("integer") + item = Item( + name="integer", + question="This is an item where the user can input a integer.", + input_type="int", + ) item.set_description("This is a integer item.") - item.set_input_type_as_int() - item.set_question("This is an item where the user can input a integer.") item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) """ @@ -246,10 +213,7 @@ def test_integer(): def test_radio(): - item = Item("1.0.0-rc4") - item.set_defaults("radio") - - item.set_question("question for radio item", "en") + item = Item(name="radio", question="question for radio item") response_options = ResponseOption() response_options.add_choice("Not at all", 0, "en") @@ -262,10 +226,10 @@ def test_radio(): item.set_input_type_as_radio(response_options) item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) item.set_filename("radio multiple") item.set_description("radio multiple") @@ -275,17 +239,15 @@ def test_radio(): item.set_input_type_as_radio(response_options) item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) def test_select(): - item = Item() - item.set_defaults("select") - item.set_question("question for select item") + item = Item(name="select", question="question for select item") response_options = ResponseOption() response_options.add_choice("Response option 1", 0) @@ -296,10 +258,10 @@ def test_select(): item.set_input_type_as_select(response_options) item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) item.set_filename("select multiple") item.set_description("select multiple") @@ -309,17 +271,15 @@ def test_select(): item.set_input_type_as_select(response_options) item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) def test_slider(): - item = Item() - item.set_defaults("slider") - item.set_question("question for slider item", "en") + item = Item(name="slider", question="question for slider item") response_options = ResponseOption() response_options.add_choice("not at all", 0) @@ -332,10 +292,10 @@ def test_slider(): item.set_input_type_as_slider(response_options) item.write(item_dir) - item_content, expected = load_jsons(item) + item_content, expected = load_jsons(item_dir, item) assert item_content == expected - clean_up(item) + clean_up(item_dir, item) """ @@ -344,25 +304,26 @@ def test_slider(): Tries to recreate the item from reproschema/tests/data/activities/items/activity1_total_score """ +my_path = Path(__file__).resolve().parent +reproschema_test_data = my_path.joinpath(my_path, "..", "..", "tests", "data") def test_read_only(): - item = Item() - item.set_defaults("activity1_total_score") - item.set_context("../../../contexts/generic") - item.set_filename("activity1_total_score", "") + item = Item(name="activity1_total_score", ext="", input_type="int") + item.at_context = "../../../contexts/generic" + item.update() + item.set_filename("activity1_total_score") item.schema["prefLabel"] = "activity1_total_score" item.set_description("Score item for Activity 1") item.set_read_only_value(True) - item.set_input_type_as_int() item.response_options.set_max(3) item.response_options.set_min(0) item.unset(["question"]) item.write(item_dir) - output_file = os.path.join(item_dir, item.get_filename()) + output_file = os.path.join(item_dir, item.at_id) item_content = read_json(output_file) # test against one of the pre existing files @@ -372,30 +333,4 @@ def test_read_only(): expected = read_json(data_file) assert item_content == expected - clean_up(item) - - -""" -HELPER FUNCTIONS -""" - - -def load_jsons(item): - - output_file = os.path.join(item_dir, item.get_filename()) - item_content = read_json(output_file) - - data_file = os.path.join(my_path, "data", "items", item.get_filename()) - expected = read_json(data_file) - - return item_content, expected - - -def read_json(file): - - with open(file, "r") as ff: - return json.load(ff) - - -def clean_up(obj): - os.remove(os.path.join(item_dir, obj.get_filename())) + clean_up(item_dir, item) diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py index cee846b..6dd8799 100644 --- a/reproschema/models/tests/test_protocol.py +++ b/reproschema/models/tests/test_protocol.py @@ -1,22 +1,14 @@ -import os, sys, json +import os +from pathlib import Path -from ..protocol import Protocol -from ..activity import Activity +from reproschema.models.activity import Activity +from reproschema.models.protocol import Protocol -my_path = os.path.dirname(os.path.abspath(__file__)) +my_path = Path(__file__).resolve().parent +from utils import load_jsons, clean_up, output_dir -# TODO -# refactor across the different test modules -protocol_dir = os.path.join(my_path, "protocols") -if not os.path.exists(protocol_dir): - os.makedirs(os.path.join(protocol_dir)) - -""" -Only for the few cases when we want to check against some of the files in -reproschema/tests/data -""" -reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") +protocol_dir = output_dir("protocols") def test_default(): @@ -26,21 +18,21 @@ def test_default(): so `reproschema validate` will complain if you run it in this """ - protocol = Protocol() + protocol = Protocol(name="default") protocol.set_defaults() - protocol.write(protocol_dir) - protocol_content, expected = load_jsons(protocol) + + protocol_content, expected = load_jsons(protocol_dir, protocol) assert protocol_content == expected - clean_up(protocol) + clean_up(protocol_dir, protocol) def test_protocol(): - protocol = Protocol() - protocol.set_defaults("protocol1") - protocol.set_pref_label("Protocol1") + protocol = Protocol(name="protocol1") + protocol.set_defaults() + protocol.set_pref_label(pref_label="Protocol1", lang="en") protocol.set_description("example Protocol") protocol.set_landing_page("http://example.com/sample-readme.md") @@ -52,37 +44,12 @@ def test_protocol(): activity_1 = Activity() activity_1.set_defaults("activity1") activity_1.set_pref_label("Screening") - activity_1.set_URI(os.path.join("..", "activities", activity_1.get_filename())) + activity_1.URI = os.path.join("..", "activities", activity_1.at_id) protocol.append_activity(activity_1) protocol.write(protocol_dir) - protocol_content, expected = load_jsons(protocol) - assert protocol_content == expected - - clean_up(protocol) - - -""" -HELPER FUNCTIONS -""" - - -def load_jsons(obj): - - output_file = os.path.join(protocol_dir, obj.get_filename()) - content = read_json(output_file) - - data_file = os.path.join(my_path, "data", "protocols", obj.get_filename()) - expected = read_json(data_file) - - return content, expected - - -def read_json(file): - - with open(file, "r") as ff: - return json.load(ff) + protocol_content, expected = load_jsons(protocol_dir, protocol) + assert protocol_content == expected -def clean_up(obj): - os.remove(os.path.join(protocol_dir, obj.get_filename())) + clean_up(protocol_dir, protocol) diff --git a/reproschema/models/tests/test_response_options.py b/reproschema/models/tests/test_response_options.py index 5cf13cc..d3a0c10 100644 --- a/reproschema/models/tests/test_response_options.py +++ b/reproschema/models/tests/test_response_options.py @@ -1,23 +1,9 @@ -import os, sys, json +from utils import load_jsons +from utils import output_dir -from ..item import ResponseOption +from reproschema.models.item import ResponseOption -my_path = os.path.dirname(os.path.abspath(__file__)) - -# Left here in case Remi and python path or import can't be friends once again. -# sys.path.insert(0, my_path + "/../") - -# TODO -# refactor across the different test modules -response_options_dir = os.path.join(my_path, "response_options") -if not os.path.exists(response_options_dir): - os.makedirs(os.path.join(response_options_dir)) - -""" -Only for the few cases when we want to check against some of the files in -reproschema/tests/data -""" -reproschema_test_data = os.path.join(my_path, "..", "..", "tests", "data") +response_options_dir = output_dir("response_options") def test_default(): @@ -26,7 +12,7 @@ def test_default(): response_options.set_defaults() response_options.write(response_options_dir) - content, expected = load_jsons(response_options) + content, expected = load_jsons(response_options_dir, response_options) assert content == expected @@ -44,27 +30,5 @@ def test_example(): response_options.set_max(6) response_options.write(response_options_dir) - content, expected = load_jsons(response_options) + content, expected = load_jsons(response_options_dir, response_options) assert content == expected - - -""" -HELPER FUNCTIONS -""" - - -def load_jsons(obj): - - output_file = os.path.join(response_options_dir, obj.get_filename()) - content = read_json(output_file) - - data_file = os.path.join(my_path, "data", "response_options", obj.get_filename()) - expected = read_json(data_file) - - return content, expected - - -def read_json(file): - - with open(file, "r") as ff: - return json.load(ff) diff --git a/reproschema/models/tests/test_schema.py b/reproschema/models/tests/test_schema.py index bf5578f..36059b6 100644 --- a/reproschema/models/tests/test_schema.py +++ b/reproschema/models/tests/test_schema.py @@ -1,4 +1,6 @@ -from .. import Protocol, Activity, Item +from reproschema.models import Activity +from reproschema.models import Item +from reproschema.models import Protocol def test_constructors(): @@ -6,11 +8,11 @@ def test_constructors(): Activity() Item() version = "1.0.0-rc2" - proto = Protocol(version=version) + proto = Protocol(schemaVersion=version) assert proto.schema["schemaVersion"] == version - act = Activity(version=version) + act = Activity(schemaVersion=version) assert act.schema["schemaVersion"] == version - item = Item(version=version) + item = Item(schemaVersion=version) assert item.schema["schemaVersion"] == version diff --git a/reproschema/models/tests/test_ui.py b/reproschema/models/tests/test_ui.py new file mode 100644 index 0000000..13cd21b --- /dev/null +++ b/reproschema/models/tests/test_ui.py @@ -0,0 +1,5 @@ +from reproschema.models.ui import UI + + +a = UI(type="reproschema:Field") +print(a) diff --git a/reproschema/models/tests/utils.py b/reproschema/models/tests/utils.py new file mode 100644 index 0000000..550aec5 --- /dev/null +++ b/reproschema/models/tests/utils.py @@ -0,0 +1,32 @@ +import json +from pathlib import Path + +my_path = Path(__file__).resolve().parent + + +def load_jsons(dir, obj): + + output_file = Path(dir).joinpath(obj.at_id) + content = read_json(output_file) + + data_file = my_path.joinpath("data", Path(dir).name, obj.at_id) + expected = read_json(data_file) + + return content, expected + + +def read_json(file): + + with open(file, "r") as ff: + return json.load(ff) + + +def clean_up(dir, obj): + Path(dir).joinpath(obj.at_id).unlink() + + +def output_dir(dir): + value = my_path.joinpath(dir) + if not value.is_dir(): + value.mkdir(parents=True, exist_ok=True) + return value diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py new file mode 100644 index 0000000..5be5f17 --- /dev/null +++ b/reproschema/models/ui.py @@ -0,0 +1,88 @@ +from collections import OrderedDict +from typing import Dict +from typing import List + +from attrs import converters +from attrs import define +from attrs import field + + +@define +class UI: + type = field(default=[None]) + add_properties: list = field(default=[]) + order: List[str] = field(default=[None]) + allow: list = field(default=[]) + shuffle: bool = field(default=False) + auto_advance: bool = field(default=True) + disable_back: bool = field(default=False) + allow_export: bool = field(default=True) + + schema: dict = field( + default={ + "shuffle": [], + "order": [], + "addProperties": [], + "allow": [], + } + ) + + schema_order = field( + default=["shuffle", "order", "addProperties", "allow"], + converter=converters.default_if_none(default=[]), # type: ignore + ) + + def __attrs_post_init__(self) -> None: + if self.type == "reproschema:Field": + self.schema_order = ["inputType", "readonlyValue"] + self.schema = {"inputType": [], "readonlyValue": []} + + def AutoAdvance(self, value: bool = None) -> None: + if not value: + return + self.auto_advance = value + if self.auto_advance and "reproschema:AutoAdvance" not in self.allow: + self.allow.append("reproschema:AutoAdvance") + elif not self.auto_advance and "reproschema:AutoAdvance" in self.allow: + self.allow.remove("reproschema:AutoAdvance") + + def DisableBack(self, value: bool = None) -> None: + if not value: + return + self.disable_back = value + if self.disable_back and "reproschema:DisableBack" not in self.allow: + self.allow.append("reproschema:DisableBack") + elif not self.disable_back and "reproschema:DisableBack" in self.allow: + self.allow.remove("reproschema:DisableBack") + + def AllowExport(self, value: bool = None) -> None: + if not value: + return + self.allow_export = value + if self.allow_export and "reproschema:AllowExport" not in self.allow: + self.allow.append("reproschema:AllowExport") + elif not self.allow_export and "reproschema:AllowExport" in self.allow: + self.allow.remove("reproschema:AllowExport") + + def update(self) -> None: + self.schema["shuffle"] = self.shuffle + self.schema["order"] = self.order + self.schema["addProperties"] = self.add_properties + self.schema["allow"] = self.allow + self.schema = self.sort() + + def sort(self) -> Dict: + reordered_dict = self.reorder_dict_skip_missing(self.schema, self.schema_order) + self.schema = reordered_dict + return reordered_dict + + @staticmethod + def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> Dict: + """ + reorders dictionary according to ``key_list`` + removing any key with no associated value + or that is not in the key list + """ + return OrderedDict( + (k, old_dict[k]) for k in key_list if (k in old_dict and old_dict[k]) + ) diff --git a/reproschema/models/utils.py b/reproschema/models/utils.py index 745ec4a..5287870 100644 --- a/reproschema/models/utils.py +++ b/reproschema/models/utils.py @@ -1,5 +1,8 @@ import json -from . import Protocol, Activity, Item + +from . import Activity +from . import Item +from . import Protocol def load_schema(filepath): diff --git a/setup.cfg b/setup.cfg index f3d7283..5d1061d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -71,6 +71,7 @@ dev = %(test)s black pre-commit + mypy all = %(doc)s %(dev)s From 01e159ed3a286acc7ba7e00c2899cc76c0aa8597 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 31 Jul 2022 22:56:39 +0200 Subject: [PATCH 43/69] remove test outputs --- reproschema/models/tests/items/country | 22 ------ reproschema/models/tests/items/date | 20 ----- reproschema/models/tests/items/default | 19 ----- reproschema/models/tests/items/email | 20 ----- reproschema/models/tests/items/float | 20 ----- reproschema/models/tests/items/integer | 20 ----- reproschema/models/tests/items/language | 22 ------ reproschema/models/tests/items/multitext | 21 ----- reproschema/models/tests/items/participant_id | 20 ----- reproschema/models/tests/items/radio | 37 --------- reproschema/models/tests/items/select | 43 ----------- reproschema/models/tests/items/slider | 55 ------------- reproschema/models/tests/items/state | 21 ----- reproschema/models/tests/items/text | 21 ----- reproschema/models/tests/items/time_range | 20 ----- reproschema/models/tests/items/year | 20 ----- .../models/tests/protocols/default_schema | 21 ----- reproschema/models/tests/protocols/protocol1 | 77 ------------------- 18 files changed, 499 deletions(-) delete mode 100644 reproschema/models/tests/items/country delete mode 100644 reproschema/models/tests/items/date delete mode 100644 reproschema/models/tests/items/default delete mode 100644 reproschema/models/tests/items/email delete mode 100644 reproschema/models/tests/items/float delete mode 100644 reproschema/models/tests/items/integer delete mode 100644 reproschema/models/tests/items/language delete mode 100644 reproschema/models/tests/items/multitext delete mode 100644 reproschema/models/tests/items/participant_id delete mode 100644 reproschema/models/tests/items/radio delete mode 100644 reproschema/models/tests/items/select delete mode 100644 reproschema/models/tests/items/slider delete mode 100644 reproschema/models/tests/items/state delete mode 100644 reproschema/models/tests/items/text delete mode 100644 reproschema/models/tests/items/time_range delete mode 100644 reproschema/models/tests/items/year delete mode 100644 reproschema/models/tests/protocols/default_schema delete mode 100644 reproschema/models/tests/protocols/protocol1 diff --git a/reproschema/models/tests/items/country b/reproschema/models/tests/items/country deleted file mode 100644 index ea63ac2..0000000 --- a/reproschema/models/tests/items/country +++ /dev/null @@ -1,22 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "country", - "prefLabel": { - "en": "country" - }, - "description": "country", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "selectCountry" - }, - "question": { - "en": "select a country" - }, - "responseOptions": { - "valueType": "xsd:string", - "maxLength": 50, - "choices": "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json" - } -} diff --git a/reproschema/models/tests/items/date b/reproschema/models/tests/items/date deleted file mode 100644 index 3bb61ba..0000000 --- a/reproschema/models/tests/items/date +++ /dev/null @@ -1,20 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "date", - "prefLabel": { - "en": "date" - }, - "description": "date", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "date" - }, - "question": { - "en": "input a date" - }, - "responseOptions": { - "valueType": "xsd:date" - } -} diff --git a/reproschema/models/tests/items/default b/reproschema/models/tests/items/default deleted file mode 100644 index 65a5e2f..0000000 --- a/reproschema/models/tests/items/default +++ /dev/null @@ -1,19 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "default", - "prefLabel": { - "en": "default" - }, - "description": "default", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "text" - }, - "question": {}, - "responseOptions": { - "valueType": "xsd:string", - "maxLength": 300 - } -} diff --git a/reproschema/models/tests/items/email b/reproschema/models/tests/items/email deleted file mode 100644 index fd711a8..0000000 --- a/reproschema/models/tests/items/email +++ /dev/null @@ -1,20 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "email", - "prefLabel": { - "en": "email" - }, - "description": "email", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "email" - }, - "question": { - "en": "input email address" - }, - "responseOptions": { - "valueType": "xsd:string" - } -} diff --git a/reproschema/models/tests/items/float b/reproschema/models/tests/items/float deleted file mode 100644 index b73614d..0000000 --- a/reproschema/models/tests/items/float +++ /dev/null @@ -1,20 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "float", - "prefLabel": { - "en": "float" - }, - "description": "This is a float item.", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "float" - }, - "question": { - "en": "This is an item where the user can input a float." - }, - "responseOptions": { - "valueType": "xsd:float" - } -} diff --git a/reproschema/models/tests/items/integer b/reproschema/models/tests/items/integer deleted file mode 100644 index 60fa880..0000000 --- a/reproschema/models/tests/items/integer +++ /dev/null @@ -1,20 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "integer", - "prefLabel": { - "en": "integer" - }, - "description": "This is a integer item.", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "number" - }, - "question": { - "en": "This is an item where the user can input a integer." - }, - "responseOptions": { - "valueType": "xsd:integer" - } -} diff --git a/reproschema/models/tests/items/language b/reproschema/models/tests/items/language deleted file mode 100644 index 059e4ea..0000000 --- a/reproschema/models/tests/items/language +++ /dev/null @@ -1,22 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "language", - "prefLabel": { - "en": "language" - }, - "description": "language", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "selectLanguage" - }, - "question": { - "en": "This is an item where the user can select several language." - }, - "responseOptions": { - "valueType": "xsd:string", - "multipleChoice": true, - "choices": "https://raw.githubusercontent.com/ReproNim/reproschema-library/master/resources/languages.json" - } -} diff --git a/reproschema/models/tests/items/multitext b/reproschema/models/tests/items/multitext deleted file mode 100644 index 0753a0b..0000000 --- a/reproschema/models/tests/items/multitext +++ /dev/null @@ -1,21 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "multitext", - "prefLabel": { - "en": "multitext" - }, - "description": "multitext", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "multitext" - }, - "question": { - "en": "This is an item where the user can input several text field." - }, - "responseOptions": { - "valueType": "xsd:string", - "maxLength": 50 - } -} diff --git a/reproschema/models/tests/items/participant_id b/reproschema/models/tests/items/participant_id deleted file mode 100644 index a32bac9..0000000 --- a/reproschema/models/tests/items/participant_id +++ /dev/null @@ -1,20 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "participant_id", - "prefLabel": { - "en": "participant id" - }, - "description": "participant id", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "pid" - }, - "question": { - "en": "input the participant id number" - }, - "responseOptions": { - "valueType": "xsd:string" - } -} diff --git a/reproschema/models/tests/items/radio b/reproschema/models/tests/items/radio deleted file mode 100644 index 7a21602..0000000 --- a/reproschema/models/tests/items/radio +++ /dev/null @@ -1,37 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "radio", - "prefLabel": { - "en": "radio" - }, - "description": "radio", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "radio" - }, - "question": { - "en": "question for radio item" - }, - "responseOptions": { - "valueType": "xsd:integer", - "minValue": 0, - "maxValue": 1, - "choices": [ - { - "name": { - "en": "Not at all" - }, - "value": 0 - }, - { - "name": { - "en": "Several days" - }, - "value": 1 - } - ], - "multipleChoice": false - } -} diff --git a/reproschema/models/tests/items/select b/reproschema/models/tests/items/select deleted file mode 100644 index 3464c17..0000000 --- a/reproschema/models/tests/items/select +++ /dev/null @@ -1,43 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "select", - "prefLabel": { - "en": "select" - }, - "description": "select", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "select" - }, - "question": { - "en": "question for select item" - }, - "responseOptions": { - "valueType": "xsd:integer", - "minValue": 0, - "maxValue": 2, - "choices": [ - { - "name": { - "en": "Response option 1" - }, - "value": 0 - }, - { - "name": { - "en": "Response option 2" - }, - "value": 1 - }, - { - "name": { - "en": "Response option 3" - }, - "value": 2 - } - ], - "multipleChoice": false - } -} diff --git a/reproschema/models/tests/items/slider b/reproschema/models/tests/items/slider deleted file mode 100644 index 7cdf98d..0000000 --- a/reproschema/models/tests/items/slider +++ /dev/null @@ -1,55 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "slider", - "prefLabel": { - "en": "slider" - }, - "description": "slider", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "slider" - }, - "question": { - "en": "question for slider item" - }, - "responseOptions": { - "valueType": "xsd:integer", - "minValue": 0, - "maxValue": 4, - "choices": [ - { - "name": { - "en": "not at all" - }, - "value": 0 - }, - { - "name": { - "en": "a bit" - }, - "value": 1 - }, - { - "name": { - "en": "so so" - }, - "value": 2 - }, - { - "name": { - "en": "a lot" - }, - "value": 3 - }, - { - "name": { - "en": "very much" - }, - "value": 4 - } - ], - "multipleChoice": false - } -} diff --git a/reproschema/models/tests/items/state b/reproschema/models/tests/items/state deleted file mode 100644 index 7e326a9..0000000 --- a/reproschema/models/tests/items/state +++ /dev/null @@ -1,21 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "state", - "prefLabel": { - "en": "state" - }, - "description": "state", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "selectState" - }, - "question": { - "en": "select a USA state" - }, - "responseOptions": { - "valueType": "xsd:string", - "choices": "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json" - } -} diff --git a/reproschema/models/tests/items/text b/reproschema/models/tests/items/text deleted file mode 100644 index 0a1ec36..0000000 --- a/reproschema/models/tests/items/text +++ /dev/null @@ -1,21 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "text", - "prefLabel": { - "en": "text" - }, - "description": "text", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "text" - }, - "question": { - "en": "question for text item" - }, - "responseOptions": { - "valueType": "xsd:string", - "maxLength": 100 - } -} diff --git a/reproschema/models/tests/items/time_range b/reproschema/models/tests/items/time_range deleted file mode 100644 index cb29f76..0000000 --- a/reproschema/models/tests/items/time_range +++ /dev/null @@ -1,20 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "time_range", - "prefLabel": { - "en": "time range" - }, - "description": "time range", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "timeRange" - }, - "question": { - "en": "input a time range" - }, - "responseOptions": { - "valueType": "xsd:datetime" - } -} diff --git a/reproschema/models/tests/items/year b/reproschema/models/tests/items/year deleted file mode 100644 index 4593400..0000000 --- a/reproschema/models/tests/items/year +++ /dev/null @@ -1,20 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Field", - "@id": "year", - "prefLabel": { - "en": "year" - }, - "description": "year", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "ui": { - "inputType": "year" - }, - "question": { - "en": "input a year" - }, - "responseOptions": { - "valueType": "xsd:date" - } -} diff --git a/reproschema/models/tests/protocols/default_schema b/reproschema/models/tests/protocols/default_schema deleted file mode 100644 index 58899fd..0000000 --- a/reproschema/models/tests/protocols/default_schema +++ /dev/null @@ -1,21 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema//contexts/generic", - "@type": "None", - "@id": "default_schema", - "landingPage": { - "@id": "README-en.md", - "@language": "en" - }, - "preamble": { - "en": "" - }, - "ui": { - "shuffle": false, - "order": [], - "addProperties": [], - "allow": [ - "reproschema:AutoAdvance", - "reproschema:AllowExport" - ] - } -} \ No newline at end of file diff --git a/reproschema/models/tests/protocols/protocol1 b/reproschema/models/tests/protocols/protocol1 deleted file mode 100644 index 81ddcc5..0000000 --- a/reproschema/models/tests/protocols/protocol1 +++ /dev/null @@ -1,77 +0,0 @@ -{ - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Protocol", - "@id": "protocol1", - "prefLabel": { - "en": "Protocol1" - }, - "description": "example Protocol", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "landingPage": { - "@id": "http://example.com/sample-readme.md", - "inLanguage": "en" - }, - "ui": { - "order": [ - null, - "items/item1.jsonld", - "../other_dir/item_two.jsonld", - "items/activity1_total_score", - "../activities/activity1_schema.jsonld", - "../activities/activity1_schema.jsonld" - ], - "addProperties": [ - { - "variableName": "item1", - "isAbout": "items/item1.jsonld", - "isVis": true, - "requiredValue": true - }, - { - "variableName": "item_two", - "isAbout": "../other_dir/item_two.jsonld", - "isVis": true, - "requiredValue": true, - "allow": [ - "reproschema:Skipped" - ] - }, - { - "variableName": "activity1_total_score", - "isAbout": "items/activity1_total_score", - "isVis": false, - "requiredValue": true - }, - { - "variableName": "activity1", - "isAbout": "../activities/activity1_schema.jsonld", - "prefLabel": { - "en": "Screening" - }, - "isVis": true, - "requiredValue": false, - "allow": [ - "reproschema:Skipped" - ] - }, - { - "variableName": "activity1", - "isAbout": "../activities/activity1_schema.jsonld", - "prefLabel": { - "en": "Screening" - }, - "isVis": true, - "requiredValue": false, - "allow": [ - "reproschema:Skipped" - ] - } - ], - "allow": [ - "reproschema:AutoAdvance", - "reproschema:AllowExport", - "reproschema:DisableBack" - ] - } -} From 6594cb05c17c42be0383f3169fd96c81d2c60152 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 31 Jul 2022 23:22:00 +0200 Subject: [PATCH 44/69] parametrize tests --- reproschema/models/item.py | 3 + .../models/tests/data/items/float.jsonld | 4 +- .../models/tests/data/items/integer.jsonld | 4 +- .../models/tests/data/items/language.jsonld | 2 +- .../models/tests/data/items/multitext.jsonld | 4 +- reproschema/models/tests/test_item.py | 186 ++---------------- 6 files changed, 30 insertions(+), 173 deletions(-) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 9de7d35..73d7fa7 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -190,6 +190,9 @@ def set_input_type_as_text(self, length=300): def set_input_type_as_multitext(self, length=300): self.set_input_type_generic_text("multitext", length) + self.response_options.unset( + ["maxValue", "minValue", "multipleChoice", "choices"] + ) def set_input_type_generic_text(self, arg0, length): self.schema["ui"]["inputType"] = arg0 diff --git a/reproschema/models/tests/data/items/float.jsonld b/reproschema/models/tests/data/items/float.jsonld index e7200b5..9ef5d1e 100644 --- a/reproschema/models/tests/data/items/float.jsonld +++ b/reproschema/models/tests/data/items/float.jsonld @@ -5,11 +5,11 @@ "prefLabel": { "en": "float" }, - "description": "This is a float item.", + "description": "float", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", "question": { - "en": "This is an item where the user can input a float." + "en": "item to input a float" }, "ui": { "inputType": "float" diff --git a/reproschema/models/tests/data/items/integer.jsonld b/reproschema/models/tests/data/items/integer.jsonld index b184085..fe57ec9 100644 --- a/reproschema/models/tests/data/items/integer.jsonld +++ b/reproschema/models/tests/data/items/integer.jsonld @@ -5,11 +5,11 @@ "prefLabel": { "en": "integer" }, - "description": "This is a integer item.", + "description": "integer", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", "question": { - "en": "This is an item where the user can input a integer." + "en": "item to input a integer" }, "ui": { "inputType": "number" diff --git a/reproschema/models/tests/data/items/language.jsonld b/reproschema/models/tests/data/items/language.jsonld index 71a0dac..e4bac83 100644 --- a/reproschema/models/tests/data/items/language.jsonld +++ b/reproschema/models/tests/data/items/language.jsonld @@ -9,7 +9,7 @@ "schemaVersion": "1.0.0-rc4", "version": "0.0.1", "question": { - "en": "This is an item where the user can select several language." + "en": "item to select several language" }, "ui": { "inputType": "selectLanguage" diff --git a/reproschema/models/tests/data/items/multitext.jsonld b/reproschema/models/tests/data/items/multitext.jsonld index 04329b9..260426f 100644 --- a/reproschema/models/tests/data/items/multitext.jsonld +++ b/reproschema/models/tests/data/items/multitext.jsonld @@ -9,13 +9,13 @@ "schemaVersion": "1.0.0-rc4", "version": "0.0.1", "question": { - "en": "This is an item where the user can input several text field." + "en": "item with several text field" }, "ui": { "inputType": "multitext" }, "responseOptions": { "valueType": "xsd:string", - "maxLength": 50 + "maxLength": 300 } } diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index 2c6a163..faed44f 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -1,6 +1,7 @@ import os from pathlib import Path +import pytest from utils import clean_up from utils import load_jsons from utils import output_dir @@ -23,141 +24,26 @@ def test_default(): clean_up(item_dir, item) -# TODO -# many items types (audio ones for examples) are not yet tested because the code cannot yet generate them. +@pytest.mark.parametrize( + "name, question, input_type", + [ + ("text", "question for text item", "text"), + ("multitext", "item with several text field", "multitext"), + ("email", "input email address", "email"), + ("participant id", "input the participant id number", "id"), + ("date", "input a date", "date"), + ("time range", "input a time range", "time_range"), + ("year", "input a year", "year"), + ("language", "item to select several language", "language"), + ("country", "select a country", "country"), + ("state", "select a USA state", "state"), + ("float", "item to input a float", "float"), + ("integer", "item to input a integer", "int"), + ], +) +def test_items(name, question, input_type): -""" -text items -""" - - -def test_text(): - - item = Item(name="text", question="question for text item", input_type="text") - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - -def test_multitext(): - - text_length = 50 - - item = Item(name="multitext") - item.set_input_type_as_multitext(text_length) - - item.set_question("This is an item where the user can input several text field.") - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - -""" -items with a specific "type" -""" - - -def test_email(): - - item = Item(name="email", question="input email address", input_type="email") - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - -def test_participant_id(): - - item = Item( - name="participant id", - question="input the participant id number", - input_type="id", - ) - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - -def test_date(): - - item = Item(name="date", question="input a date", input_type="date") - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - -def test_time_range(): - - item = Item( - name="time range", question="input a time range", input_type="time_range" - ) - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - -def test_year(): - - item = Item(name="year", question="input a year", input_type="year") - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - -""" -Items that refer to a preset list of responses choices -""" - - -def test_language(): - - item = Item( - name="language", - question="This is an item where the user can select several language.", - input_type="language", - ) - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - -def test_country(): - - item = Item(name="country", question="select a country", input_type="country") - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - -def test_state(): - - item = Item(name="state", question="select a USA state", input_type="state") + item = Item(name=name, question=question, input_type=input_type) item.write(item_dir) item_content, expected = load_jsons(item_dir, item) @@ -171,38 +57,6 @@ def test_state(): """ -def test_float(): - - item = Item( - name="float", - question="This is an item where the user can input a float.", - input_type="float", - ) - item.set_description("This is a float item.") - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - -def test_integer(): - - item = Item( - name="integer", - question="This is an item where the user can input a integer.", - input_type="int", - ) - item.set_description("This is a integer item.") - - item.write(item_dir) - item_content, expected = load_jsons(item_dir, item) - assert item_content == expected - - clean_up(item_dir, item) - - """ SELECTION ITEMS: radio and select tested both with: From c711d83eea626e65f786dcd982fefb42c534ed78 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 1 Aug 2022 01:23:07 +0200 Subject: [PATCH 45/69] refactor refactor refactor --- reproschema/models/base.py | 6 +- reproschema/models/item.py | 115 +++++++++--------- reproschema/models/response_options.py | 51 ++++---- .../data/activities/default_schema.jsonld | 2 - .../data/protocols/default_schema.jsonld | 4 +- .../response_options/valueConstraints.jsonld | 7 +- reproschema/models/tests/test_item.py | 10 +- .../models/tests/test_response_options.py | 1 - 8 files changed, 99 insertions(+), 97 deletions(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 668f97a..108c2fe 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -274,4 +274,8 @@ def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> Dict: removing any key with no associated value or that is not in the key list """ - return OrderedDict((k, old_dict[k]) for k in key_list if k in old_dict) + return OrderedDict( + (k, old_dict[k]) + for k in key_list + if (k in old_dict and old_dict[k] not in ["", [], None]) + ) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 73d7fa7..45f184f 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -33,6 +33,7 @@ def __init__( visible: bool = True, required: bool = False, skippable: bool = True, + read_only: bool = False, ): super().__init__( at_id=name, @@ -49,6 +50,7 @@ def __init__( self.required = required self.skippable = skippable self.question = question + self.read_only = (read_only,) """ The responseOptions dictionary is kept empty until the file has to be written @@ -82,122 +84,127 @@ def set_question(self, question, lang=DEFAULT_LANG): # StaticReadOnly: Static/Static.vue def set_input_type(self): - if not self.input_type: + + SUPPORTED_TYPES = ( + "text", + "multitext", + "int", + "float", + "date", + "time", + "time_range", + "language", + "country", + "state", + "email", + "id", + ) + + if not self.input_type or self.input_type in ["select", "radio", "slider"]: return + + if self.input_type in SUPPORTED_TYPES: + self.response_options.unset( + ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] + ) + if self.input_type == "text": - self.set_input_type_as_text() - if self.input_type == "multitext": - self.set_input_type_as_multitext() + self.schema["ui"]["inputType"] = "text" + self.response_options.set_type("string") + self.response_options.set_length(300) + + elif self.input_type == "multitext": + self.schema["ui"]["inputType"] = "multitext" + self.response_options.set_length(300) + self.response_options.set_type("string") + elif self.input_type == "int": self.set_input_type_numeric("number", "integer") + elif self.input_type == "float": self.set_input_type_numeric("float", "float") + elif self.input_type == "year": self.schema["ui"]["inputType"] = "year" self.response_options.set_type("date") self.response_options.unset( ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] ) + elif self.input_type == "date": self.schema["ui"]["inputType"] = "date" self.response_options.set_type("date") - self.response_options.unset( - ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] - ) + elif self.input_type == "time_range": self.schema["ui"]["inputType"] = "timeRange" self.response_options.set_type("datetime") - self.response_options.unset( - ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] - ) + elif self.input_type == "language": self.set_input_type_as_language() + elif self.input_type == "country": self.set_input_type_as_country() + elif self.input_type == "state": self.set_input_type_as_state() + elif self.input_type == "email": self.schema["ui"]["inputType"] = "email" self.response_options.set_type("string") - self.response_options.unset( - ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] - ) + elif self.input_type == "id": self.schema["ui"]["inputType"] = "pid" self.response_options.set_type("string") - self.response_options.unset( - ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] + + else: + + raise ValueError( + f""" + Input_type {self.input_type} not supported. + Supported input_types are: {SUPPORTED_TYPES} + """ ) + # to remove empty options keys + # self.response_options.sort() + def set_input_type_numeric(self, arg0, arg1): self.schema["ui"]["inputType"] = arg0 self.response_options.set_type(arg1) - self.response_options.unset( - ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] - ) """ input types with preset response choices """ def set_input_type_as_language(self): - URL = self.set_input_from_preset( "https://raw.githubusercontent.com/ReproNim/reproschema-library/", "selectLanguage", ) - self.response_options.set_multiple_choice(True) self.response_options.use_preset(f"{URL}master/resources/languages.json") def set_input_type_as_country(self): - URL = self.set_input_from_preset( "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json", "selectCountry", ) - self.response_options.use_preset(URL) self.response_options.set_length(50) def set_input_type_as_state(self): - URL = self.set_input_from_preset( "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json", "selectState", ) - self.response_options.use_preset(URL) def set_input_from_preset(self, arg0, arg1): result = arg0 self.schema["ui"]["inputType"] = arg1 self.response_options.set_type("string") - self.response_options.unset( - ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] - ) - return result - - """ - input types requiring user typed input - """ - - def set_input_type_as_text(self, length=300): - self.set_input_type_generic_text("text", length) - self.response_options.unset( - ["maxValue", "minValue", "multipleChoice", "choices"] - ) - def set_input_type_as_multitext(self, length=300): - self.set_input_type_generic_text("multitext", length) - self.response_options.unset( - ["maxValue", "minValue", "multipleChoice", "choices"] - ) - - def set_input_type_generic_text(self, arg0, length): - self.schema["ui"]["inputType"] = arg0 - self.response_options.set_type("string") - self.response_options.set_length(length) + return result """ input types with 'different response choices' @@ -219,6 +226,7 @@ def set_input_type_as_select(self, response_options: ResponseOption): self.set_input_type_rasesli("select", response_options) def set_input_type_as_slider(self, response_options: ResponseOption): + response_options.set_multiple_choice(False) self.set_input_type_rasesli("slider", response_options) def set_input_type_rasesli(self, arg0, response_options: ResponseOption): @@ -229,15 +237,10 @@ def set_input_type_rasesli(self, arg0, response_options: ResponseOption): """ UI """ - # are input_type and read_only specific properties to items - # or should they be brought up into the base class? - # or be made part of an UI class? - - # def set_input_type(self, input_type): - # self.schema["ui"]["inputType"] = input_type - def set_read_only_value(self, value): - self.schema["ui"]["readonlyValue"] = value + def set_read_only(self, value: bool = False): + self.read_only = value + self.schema["ui"]["readonlyValue"] = self.read_only """ writing, reading, sorting, unsetting diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index 17f53b8..c271111 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -25,7 +25,9 @@ class to deal with reproschema response options # # self.options and self.schema - def __init__(self, name="valueConstraints", schemaVersion=None): + def __init__( + self, name="valueConstraints", multiple_choice=None, schemaVersion=None + ): super().__init__( at_id=name, schema_order=options_order, @@ -34,13 +36,8 @@ def __init__(self, name="valueConstraints", schemaVersion=None): ext=".jsonld", suffix="", ) - self.options = { - "valueType": "", - "minValue": 0, - "maxValue": 0, - "choices": [], - "multipleChoice": False, - } + self.options = {"choices": []} + self.set_multiple_choice(multiple_choice) def set_defaults(self): self.options["@context"] = self.schema["@context"] @@ -57,23 +54,33 @@ def unset(self, keys): for i in keys: self.options.pop(i, None) - def set_type(self, type): - self.options["valueType"] = f"xsd:{type}" + def set_type(self, value: str = None): + if value is not None: + self.options["valueType"] = f"xsd:{value}" - # TODO a nice thing to do would be to read the min and max value - # from the rest of the content of self.options - # could avoid having the user to input those - def set_min(self, value): - self.options["minValue"] = value + def values_all_options(self): + return [i["value"] for i in self.options["choices"] if "value" in i] - def set_max(self, value): - self.options["maxValue"] = value + def set_min(self, value: int = None): + if value is not None: + self.options["minValue"] = value + elif len(self.options["choices"]) > 1: + self.options["minValue"] = min(self.values_all_options()) - def set_length(self, value): - self.options["maxLength"] = value + def set_max(self, value: int = None): + if value is not None: + self.options["maxValue"] = value + elif len(self.options["choices"]) > 1: + self.options["maxValue"] = max(self.values_all_options()) - def set_multiple_choice(self, value): - self.options["multipleChoice"] = value + def set_length(self, value: int = None): + if value is not None: + self.options["maxLength"] = value + + def set_multiple_choice(self, value: bool = None): + if value is not None: + self.multiple_choice = value + self.options["multipleChoice"] = value def use_preset(self, URI): """ @@ -84,6 +91,8 @@ def use_preset(self, URI): def add_choice(self, choice, value, lang=DEFAULT_LANG): self.options["choices"].append({"name": {lang: choice}, "value": value}) + self.set_max() + self.set_min() def sort(self): reordered_dict = self.reorder_dict_skip_missing(self.options, self.schema_order) diff --git a/reproschema/models/tests/data/activities/default_schema.jsonld b/reproschema/models/tests/data/activities/default_schema.jsonld index 2647274..63c5950 100644 --- a/reproschema/models/tests/data/activities/default_schema.jsonld +++ b/reproschema/models/tests/data/activities/default_schema.jsonld @@ -13,8 +13,6 @@ }, "ui": { "shuffle": false, - "order": [], - "addProperties": [], "allow": [ "reproschema:AutoAdvance", "reproschema:AllowExport" diff --git a/reproschema/models/tests/data/protocols/default_schema.jsonld b/reproschema/models/tests/data/protocols/default_schema.jsonld index ff4df75..e9c76b4 100644 --- a/reproschema/models/tests/data/protocols/default_schema.jsonld +++ b/reproschema/models/tests/data/protocols/default_schema.jsonld @@ -20,8 +20,6 @@ "reproschema:AutoAdvance", "reproschema:AllowExport" ], - "shuffle": false, - "order": [], - "addProperties": [] + "shuffle": false } } diff --git a/reproschema/models/tests/data/response_options/valueConstraints.jsonld b/reproschema/models/tests/data/response_options/valueConstraints.jsonld index b935e9a..33ea751 100644 --- a/reproschema/models/tests/data/response_options/valueConstraints.jsonld +++ b/reproschema/models/tests/data/response_options/valueConstraints.jsonld @@ -1,10 +1,5 @@ { "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", "@id": "valueConstraints.jsonld", - "@type": "reproschema:ResponseOption", - "valueType": "", - "minValue": 0, - "maxValue": 0, - "choices": [], - "multipleChoice": false + "@type": "reproschema:ResponseOption" } diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index faed44f..79a0f6a 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -72,10 +72,7 @@ def test_radio(): response_options = ResponseOption() response_options.add_choice("Not at all", 0, "en") response_options.add_choice("Several days", 1, "en") - # TODO - # set_min and set_max cold probably be combined into a single method that gets - # those values from the content of the choice key - response_options.set_max(1) + response_options.set_multiple_choice(False) item.set_input_type_as_radio(response_options) @@ -107,7 +104,7 @@ def test_select(): response_options.add_choice("Response option 1", 0) response_options.add_choice("Response option 2", 1) response_options.add_choice("Response option 3", 2) - response_options.set_max(2) + response_options.set_multiple_choice(False) item.set_input_type_as_select(response_options) @@ -141,7 +138,6 @@ def test_slider(): response_options.add_choice("so so", 2) response_options.add_choice("a lot", 3) response_options.add_choice("very much", 4) - response_options.set_max(4) item.set_input_type_as_slider(response_options) @@ -170,7 +166,7 @@ def test_read_only(): item.set_filename("activity1_total_score") item.schema["prefLabel"] = "activity1_total_score" item.set_description("Score item for Activity 1") - item.set_read_only_value(True) + item.set_read_only(True) item.response_options.set_max(3) item.response_options.set_min(0) item.unset(["question"]) diff --git a/reproschema/models/tests/test_response_options.py b/reproschema/models/tests/test_response_options.py index d3a0c10..a4ef055 100644 --- a/reproschema/models/tests/test_response_options.py +++ b/reproschema/models/tests/test_response_options.py @@ -27,7 +27,6 @@ def test_example(): for i in range(1, 5): response_options.add_choice("", i) response_options.add_choice("Completely", 6) - response_options.set_max(6) response_options.write(response_options_dir) content, expected = load_jsons(response_options_dir, response_options) From 45a74138902e93658dc61200a6c7aa14046c5e24 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 31 Jul 2022 23:25:24 +0000 Subject: [PATCH 46/69] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .pre-commit-config.yaml | 2 +- pyproject.toml | 1 - reproschema/_version.py | 3 --- reproschema/cli.py | 5 ++++- reproschema/jsonldutils.py | 10 +++++++--- reproschema/tests/test_convert.py | 4 +++- reproschema/tests/test_validate.py | 5 ++++- reproschema/utils.py | 4 +++- reproschema/validate.py | 8 ++++++-- setup.py | 2 ++ versioneer.py | 2 -- 11 files changed, 30 insertions(+), 16 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09ac2b2..b6ef82e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -8,7 +8,7 @@ repos: - id: end-of-file-fixer - id: check-yaml - id: check-added-large-files - + - repo: https://github.com/psf/black rev: "22.6.0" hooks: diff --git a/pyproject.toml b/pyproject.toml index a5eacca..0d0aef7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,3 @@ [tool.pytest.ini_options] minversion = "4.4.0" addopts = "-ra -vv" - diff --git a/reproschema/_version.py b/reproschema/_version.py index da2b4c4..af50a41 100644 --- a/reproschema/_version.py +++ b/reproschema/_version.py @@ -3,12 +3,9 @@ # feature). Distribution tarballs (built by setup.py sdist) and build # directories (produced by setup.py build) will contain a much shorter file # that just contains the computed version number. - # This file is released into the public domain. Generated by # versioneer-0.18 (https://github.com/warner/python-versioneer) - """Git implementation of _version.py.""" - import errno import os import re diff --git a/reproschema/cli.py b/reproschema/cli.py index a8ff7ed..2e49400 100644 --- a/reproschema/cli.py +++ b/reproschema/cli.py @@ -1,8 +1,10 @@ import os + import click -from . import get_logger, set_logger_level from . import __version__ +from . import get_logger +from . import set_logger_level lgr = get_logger() @@ -13,6 +15,7 @@ def print_version(ctx, param, value): click.echo(__version__) ctx.exit() + # group to provide commands @click.group() @click.option( diff --git a/reproschema/jsonldutils.py b/reproschema/jsonldutils.py index f88f38f..a72496b 100644 --- a/reproschema/jsonldutils.py +++ b/reproschema/jsonldutils.py @@ -1,8 +1,12 @@ -from pyld import jsonld -from pyshacl import validate as shacl_validate import json import os -from .utils import start_server, stop_server, lgr + +from pyld import jsonld +from pyshacl import validate as shacl_validate + +from .utils import lgr +from .utils import start_server +from .utils import stop_server def load_file(path_or_url, started=False, http_kwargs={}): diff --git a/reproschema/tests/test_convert.py b/reproschema/tests/test_convert.py index dd59f21..7c99e51 100644 --- a/reproschema/tests/test_convert.py +++ b/reproschema/tests/test_convert.py @@ -1,7 +1,9 @@ import os -from ..jsonldutils import to_newformat + import pytest +from ..jsonldutils import to_newformat + @pytest.fixture def filename(): diff --git a/reproschema/tests/test_validate.py b/reproschema/tests/test_validate.py index 96e40db..6d8b16f 100644 --- a/reproschema/tests/test_validate.py +++ b/reproschema/tests/test_validate.py @@ -1,7 +1,10 @@ import os -from ..validate import validate_dir, validate + import pytest +from ..validate import validate +from ..validate import validate_dir + def test_validate(): os.chdir(os.path.dirname(__file__)) diff --git a/reproschema/utils.py b/reproschema/utils.py index 2f85d1a..69ff859 100644 --- a/reproschema/utils.py +++ b/reproschema/utils.py @@ -1,7 +1,9 @@ import os import threading -from http.server import HTTPServer, SimpleHTTPRequestHandler +from http.server import HTTPServer +from http.server import SimpleHTTPRequestHandler from tempfile import mkdtemp + import requests import requests_cache diff --git a/reproschema/validate.py b/reproschema/validate.py index 64b612e..85a0e83 100644 --- a/reproschema/validate.py +++ b/reproschema/validate.py @@ -1,6 +1,10 @@ import os -from .utils import start_server, stop_server, lgr -from .jsonldutils import load_file, validate_data + +from .jsonldutils import load_file +from .jsonldutils import validate_data +from .utils import lgr +from .utils import start_server +from .utils import stop_server def validate_dir(directory, shape_file, started=False, http_kwargs={}): diff --git a/setup.py b/setup.py index b96c493..02bf7da 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,9 @@ """ import sys + from setuptools import setup + import versioneer # Give setuptools a hint to complain if it's too old a version diff --git a/versioneer.py b/versioneer.py index 2b54540..b3a14fb 100644 --- a/versioneer.py +++ b/versioneer.py @@ -1,5 +1,4 @@ # Version: 0.18 - """The Versioneer - like a rocketeer, but for versions. The Versioneer @@ -274,7 +273,6 @@ https://creativecommons.org/publicdomain/zero/1.0/ . """ - from __future__ import print_function try: From 42cf00fd2a2f98870a4c63d27795940b8f30197a Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 1 Aug 2022 01:55:37 +0200 Subject: [PATCH 47/69] remove clutter --- .gitignore | 1 + Pipfile | 11 ----------- reproschema/models/tests/README.md | 11 ----------- 3 files changed, 1 insertion(+), 22 deletions(-) delete mode 100644 Pipfile diff --git a/.gitignore b/.gitignore index 8e838a7..e8b6741 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ **/tmp +Pipfile # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 71e4f7c..0000000 --- a/Pipfile +++ /dev/null @@ -1,11 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] - -[dev-packages] - -[requires] -python_version = "3.9" diff --git a/reproschema/models/tests/README.md b/reproschema/models/tests/README.md index 0ef4b7c..73a8cda 100644 --- a/reproschema/models/tests/README.md +++ b/reproschema/models/tests/README.md @@ -35,14 +35,3 @@ pip install -e . ``` More [here](../../README.md) - -## TODO - -- a lot of repeats in the test code base can be refactored - - especially for `test_items.py` a lot of those tests can be parametrized - (since apparently pytest allows this). - - the "clean up" after each passed test could be handle by a pytest fixture - - the helper functions are also nearly identical in all 3 test modules and - should be refactored - - some of the methods of the base class should probably be test on their own - rather than having tests for the sub classes that also "test" them. From cfe7e60feebc19c99320282d3a7a22e32fdce406 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 1 Aug 2022 10:49:45 +0200 Subject: [PATCH 48/69] do not include test in package --- reproschema/tests/__init__.py | 0 reproschema/tests/test_convert.py | 2 +- reproschema/tests/test_validate.py | 4 ++-- 3 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 reproschema/tests/__init__.py diff --git a/reproschema/tests/__init__.py b/reproschema/tests/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/reproschema/tests/test_convert.py b/reproschema/tests/test_convert.py index 7c99e51..a3ee2e7 100644 --- a/reproschema/tests/test_convert.py +++ b/reproschema/tests/test_convert.py @@ -2,7 +2,7 @@ import pytest -from ..jsonldutils import to_newformat +from reproschema.jsonldutils import to_newformat @pytest.fixture diff --git a/reproschema/tests/test_validate.py b/reproschema/tests/test_validate.py index 6d8b16f..e1e3576 100644 --- a/reproschema/tests/test_validate.py +++ b/reproschema/tests/test_validate.py @@ -2,8 +2,8 @@ import pytest -from ..validate import validate -from ..validate import validate_dir +from reproschema.validate import validate +from reproschema.validate import validate_dir def test_validate(): From faaf1040f06a3e792d314e2b33b17ae5cc5758c5 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 14 Aug 2022 22:59:19 +0200 Subject: [PATCH 49/69] refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor test pollution test pollution test pollution test pollution test pollution test pollution test pollution test pollution test pollution test pollution test pollution test pollution test pollution test pollution test pollution test pollution refactor refactor refactor refactor refactor refactor refactor refactor add type hint refactor refactor refactor refactor --- reproschema/models/activity.py | 103 ++-- reproschema/models/base.py | 476 +++++++++++------- reproschema/models/item.py | 186 +++---- reproschema/models/protocol.py | 114 ++--- reproschema/models/response_options.py | 114 ++--- .../data/activities/activity1_schema.jsonld | 41 +- .../data/activities/default_schema.jsonld | 3 - .../data/protocols/default_schema.jsonld | 7 - .../data/protocols/protocol1_schema.jsonld | 3 - reproschema/models/tests/test_activity.py | 63 +-- reproschema/models/tests/test_item.py | 57 ++- reproschema/models/tests/test_protocol.py | 43 +- .../models/tests/test_response_options.py | 29 +- reproschema/models/tests/test_ui.py | 19 +- reproschema/models/ui.py | 213 ++++++-- .../activities/items/activity1_total_score | 4 +- 16 files changed, 847 insertions(+), 628 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index 86aeebd..b16d277 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -1,23 +1,12 @@ +from pathlib import Path +from typing import Dict +from typing import Optional +from typing import Union + +from .base import DEFAULT_LANG from .base import SchemaBase from .item import Item -DEFAULT_LANG = "en" - -schema_order = [ - "@context", - "@type", - "@id", - "prefLabel", - "description", - "schemaVersion", - "version", - "preamble", - "citation", - "image", - "compute", - "ui", -] - class Activity(SchemaBase): """ @@ -26,51 +15,55 @@ class to deal with reproschema activities def __init__( self, - name="activity", - schemaVersion=None, - suffix="_schema", - ext=".jsonld", - visible: bool = True, - required: bool = False, - skippable: bool = True, + name: Optional[str] = "activity", + schemaVersion: Optional[str] = None, + prefLabel: Optional[Union[str, Dict[str, str]]] = "activity", + description: Optional[str] = "", + preamble: Optional[str] = None, + citation: Optional[str] = None, + suffix: Optional[str] = "_schema", + visible: Optional[bool] = True, + required: Optional[bool] = False, + skippable: Optional[bool] = True, + ext: Optional[str] = ".jsonld", + output_dir: Optional[Union[str, Path]] = Path.cwd(), + lang: Optional[str] = DEFAULT_LANG(), ): + + schema_order = [ + "@context", + "@type", + "@id", + "schemaVersion", + "version", + "prefLabel", + "description", + "preamble", + "citation", + "image", + "compute", + "ui", + ] + super().__init__( at_id=name, schemaVersion=schemaVersion, - type="reproschema:Activity", + at_type="reproschema:Activity", + prefLabel={lang: prefLabel}, + description=description, + preamble=preamble, + citation=citation, schema_order=schema_order, + visible=visible, + required=required, + skippable=skippable, suffix=suffix, ext=ext, + output_dir=output_dir, + lang=lang, ) - self.visible = visible - self.required = required - self.skippable = skippable - - def set_defaults(self, name=None): - if not name: - name = self.at_id - super().set_defaults(name) - self.set_preamble(name) - self.set_ui_default() - - def set_compute(self, variable, expression): - self.schema["compute"] = [ - {"variableName": variable, "jsExpression": expression} - ] + self.ui.shuffle = False + self.update() def append_item(self, item: Item): - item_property = { - "variableName": item.get_basename(), - "isAbout": item.URI, - "isVis": item.visible, - "requiredValue": item.required, - } - self.append_to_ui(item, item_property) - - """ - writing, reading, sorting, unsetting - """ - - def write(self, output_dir): - self.sort() - self._SchemaBase__write(output_dir) + self.ui.append(obj=item, variableName=item.get_basename()) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 108c2fe..2f1be65 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -1,4 +1,5 @@ import json +import os from collections import OrderedDict from pathlib import Path from typing import Dict @@ -6,256 +7,387 @@ from typing import Optional from typing import Union -import attrs from attrs import define from attrs import field +from attrs.converters import default_if_none +from attrs.validators import in_ from attrs.validators import instance_of +from attrs.validators import optional from .ui import UI -DEFAULT_LANG = "en" -DEFAULT_VERSION = "1.0.0-rc4" +def DEFAULT_LANG() -> str: + return "en" -@define +def DEFAULT_VERSION() -> str: + return "1.0.0-rc4" + + +@define(kw_only=True) class SchemaBase: - """_summary_ - - Returns - ------- - _type_ - _description_ - - Raises - ------ - ValueError - _description_ - ValueError - _description_ - ValueError - _description_ + + """ + Schema based attributes: REQUIRED """ + at_type: Optional[str] = field( + factory=(str), + converter=default_if_none(default="reproschema:Field"), # type: ignore + validator=in_( + [ + "reproschema:Protocol", + "reproschema:Activity", + "reproschema:Field", + "reproschema:ResponseOption", + ] + ), + ) + at_id: Optional[str] = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=[instance_of(str)], + ) + schemaVersion: Optional[str] = field( + default=DEFAULT_VERSION(), + converter=default_if_none(default=DEFAULT_VERSION()), # type: ignore + validator=[instance_of(str)], + ) + version: Optional[str] = field( + default=None, + converter=default_if_none(default="0.0.1"), # type: ignore + validator=[instance_of(str)], + ) + at_context: str = field( + validator=[instance_of(str)], + ) + + @at_context.default + def _default_context(self) -> str: + """ + For now we assume that the github repo will be where schema will be read from. + """ + URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" + VERSION = self.schemaVersion or DEFAULT_VERSION() + return URL + VERSION + "/contexts/generic" + """ - base class to deal with reproschema schemas + Schema based attributes: OPTIONAL - The content of the schema is stored in the dictionary ``self.schema`` + Can be found in the UI class + - order + - addProperties + - allow + """ - self.schema["@context"] - self.schema["@type"] - self.schema["schemaVersion"] - self.schema["version"] - self.schema["preamble"] - self.schema["citation"] - self.schema["image"] - ... + # TODO - When the output file is created, its content is simply dumped into a json - by the ``write`` method. + """ + Protocol, Activity, Field + """ + # altLabel + # associatedMedia + # about + prefLabel: dict = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of(dict)), + ) + description: str = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + + # Protocol only + # TODO landing_page is a dict or a list of dict? + landingPage: dict = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of(dict)), + ) """ + Protocol and Activity + """ + # cronTable + # messages + citation: str = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + compute: Optional[list] = field( + factory=(list), + converter=default_if_none(default=[]), # type: ignore + validator=optional(instance_of(list)), + ) - type: str = field(kw_only=True, default="", validator=[instance_of(str)]) - schemaVersion: str = field( - kw_only=True, - default=DEFAULT_VERSION, - converter=attrs.converters.default_if_none(default=DEFAULT_VERSION), # type: ignore + """ + Activity only + """ + # overrideProperties + + #: Activity and Field + preamble: dict = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of(dict)), + ) + + """ + Field only + """ + # image + # inputType + # additionalNotesObj + readonlyValue: Optional[bool] = field( + factory=(bool), + validator=optional(instance_of(bool)), + ) + question: dict = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of(dict)), + ) + + """ + UI related + """ + ui: UI = field( + validator=optional(instance_of(UI)), + ) + + @ui.default + def _default_ui(self) -> UI: + return UI(at_type=self.at_type, readonlyValue=self.readonlyValue) + + visible: Optional[bool] = field( + factory=(bool), + converter=default_if_none(default=True), # type: ignore + validator=optional(instance_of(bool)), + ) + required: Optional[bool] = field( + factory=(bool), + converter=default_if_none(default=False), # type: ignore + validator=optional(instance_of(bool)), + ) + skippable: Optional[bool] = field( + factory=(bool), + converter=default_if_none(default=True), # type: ignore + validator=optional(instance_of(bool)), + ) + + """ + Non schema based attributes: OPTIONAL + + Those attributes help with file management + and with printing json files with standardized key orders + """ + lang: Optional[str] = field( + factory=(str), + converter=default_if_none(default=DEFAULT_LANG()), # type: ignore + validator=optional(instance_of(str)), + ) + schema_order: Optional[list] = field( + factory=(list), + converter=default_if_none(default=[]), # type: ignore + validator=optional(instance_of(list)), + ) + output_dir = field( + default=None, + converter=default_if_none(default=Path.cwd()), # type: ignore + validator=optional(instance_of((str, Path))), + ) + suffix: Optional[str] = field( + factory=(str), + converter=default_if_none(default="_schema"), # type: ignore + validator=optional(instance_of(str)), ) - version: str = field(kw_only=True, default="0.0.1", validator=[instance_of(str)]) - at_id: str = field(kw_only=True, default="", validator=[instance_of(str)]) - at_context: str = field(kw_only=True, default="", validator=[instance_of(str)]) - prefLabel: dict = field(kw_only=True, default={}, validator=[instance_of(dict)]) - description: str = field(kw_only=True, default="", validator=[instance_of(str)]) - - URI = field(default=None) - lang = field(default=DEFAULT_LANG) - schema_order: list = field( - kw_only=True, + ext: Optional[str] = field( + factory=(str), + converter=default_if_none(default=".jsonld"), # type: ignore + validator=optional(instance_of(str)), + ) + URI: Optional[Path] = field( default=None, - converter=attrs.converters.default_if_none(default=[]), # type: ignore + converter=default_if_none(default=Path("")), # type: ignore + validator=optional(instance_of((str, Path))), + ) + + #: contains the content that is read from or dumped in JSON + schema: Optional[dict] = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of(dict)), ) - suffix = field(kw_only=True, default="_schema") - ext = field(kw_only=True, default=".jsonld") def __attrs_post_init__(self) -> None: - self.at_id = self.at_id.replace(" ", "_") + if self.description == "": self.description = self.at_id.replace("_", " ") - if self.at_context == "": - self.at_context: str = self.default_context() - if self.prefLabel == {}: - self.prefLabel = {self.lang: self.at_id} - self.schema = { - "@type": self.type, - "schemaVersion": self.schemaVersion, - "version": self.version, - "prefLabel": {}, - "description": self.description, - } - self.ui = UI(self.type) - self.update() + + self.set_pref_label() + self.set_filename() def update(self) -> None: + """Updates the schema content based on the attributes.""" self.schema["@id"] = self.at_id - self.schema["@type"] = self.type - self.schema["schemaVersion"] = self.schemaVersion - self.schema["version"] = self.version + self.schema["@type"] = self.at_type self.schema["@context"] = self.at_context - self.schema["prefLabel"] = self.prefLabel - self.schema["description"] = self.description + keys_to_update = [ + "schemaVersion", + "version", + "prefLabel", + "description", + "citation", + "preamble", + "landingPage", + "compute", + "question", + ] + for key in keys_to_update: + self.schema[key] = self.__getattribute__(key) + + self.update_ui() + + def update_ui(self) -> None: self.ui.update() self.schema["ui"] = self.ui.schema - # This probably needs some cleaning but is at the moment necessary to pass - # the context to the ResponseOption class - def default_context(self) -> str: - """ - For now we assume that the github repo will be where schema will be read from. - """ - URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" - VERSION = self.schemaVersion or DEFAULT_VERSION - return URL + VERSION + "/contexts/generic" + # TODO + def set_image(self, image) -> None: + self.schema["image"] = image - def set_defaults(self, name=None) -> None: - if not name: - name = self.at_id - self.set_filename(name) - "We use the ``name`` of this class instance for schema keys minus some underscore" - self.set_pref_label(name.replace("_", " ")) - self.set_description(name.replace("_", " ")) + """SETTERS + + These are "complex" setters to help set fields that are dictionaries, + or that use other attributes + or set several attributes at once + + """ def set_preamble( - self, preamble: Optional[str] = None, lang: str = DEFAULT_LANG + self, preamble: Optional[str] = None, lang: Optional[str] = None ) -> None: - if not preamble: - preamble = "" - self.schema["preamble"] = {lang: preamble} + if preamble is None: + return + if lang is None: + lang = self.lang + if not self.preamble: + self.preamble = {} + self.preamble[lang] = preamble + self.update() - def set_citation(self, citation: str) -> None: - self.schema["citation"] = citation + # TODO move to protocol class? + def set_landing_page(self, page: Optional[str] = None, lang: Optional[str] = None): + if page is None: + return + if lang is None: + lang = self.lang + self.landingPage = {"@id": page, "inLanguage": lang} + self.update() - def set_image(self, image) -> None: - self.schema["image"] = image + def set_compute(self, variable, expression): + self.compute = [{"variableName": variable, "jsExpression": expression}] + self.update() + + def set_filename(self, name: str = None) -> None: + if name is None: + name = self.at_id + if name.endswith(self.ext): + name = name.replace(self.ext, "") + if name.endswith(self.suffix): + name = name.replace(self.suffix, "") - def set_filename(self, name: str) -> None: name = name.replace(" ", "_") + self.at_id = f"{name}{self.suffix}{self.ext}" + self.URI = os.path.join(self.output_dir, self.at_id) self.update() - def set_pref_label(self, pref_label: str = None, lang: str = DEFAULT_LANG) -> None: - if not pref_label: + def set_pref_label( + self, pref_label: Optional[str] = None, lang: Optional[str] = None + ) -> None: + if pref_label is None: + if self.prefLabel == {} or self.prefLabel[DEFAULT_LANG()] in [ + "protocol", + "activity", + "item", + ]: + self.set_pref_label( + pref_label=self.at_id.replace("_", " "), lang=self.lang + ) return - self.prefLabel[lang] = pref_label - self.schema["prefLabel"] = self.prefLabel - def set_description(self, description: str) -> None: - self.description = description - self.schema["description"] = self.description - # self.update() + if lang is None: + lang = self.lang - """ - getters + self.prefLabel[lang] = pref_label + self.update() - to access the content of some of the keys of the schema - or some of the instance properties + """GETTERS """ def get_basename(self) -> str: return Path(self.at_id).stem - """ - UI: setters specific to the user interface keys of the schema - - The ui has its own dictionary. - - Mostly used by the Protocol and Activity class. - - """ - - def set_ui_default(self) -> None: - self.schema["ui"] = { - "shuffle": [], - "order": [], - "addProperties": [], - "allow": [], - } - self.set_ui_allow() - self.set_ui_shuffle() - self.ui.update() - - def set_ui_shuffle(self, shuffle: bool = False) -> None: - self.ui.shuffle = shuffle - self.schema["ui"]["shuffle"] = self.ui.shuffle - - def set_ui_allow( - self, - auto_advance: bool = True, - allow_export: bool = True, - disable_back: bool = False, - ) -> None: - self.ui.AutoAdvance(auto_advance) - self.ui.DisableBack(disable_back) - self.ui.AllowExport(allow_export) - self.schema["ui"]["allow"] = self.ui.allow - - def append_to_ui(self, obj, properties: dict) -> None: - if obj.skippable: - properties["allow"] = ["reproschema:Skipped"] - - self.ui.order.append(obj.URI) - self.ui.add_properties.append(properties) - self.schema["ui"]["order"].append(obj.URI) - self.schema["ui"]["addProperties"].append(properties) - """ writing, reading, sorting, unsetting - Editing and appending things to the dictionnary tends to give json output - that is not standardized: the @context can end up at the bottom for one file + Editing and appending things to the dictionary tends to give json output + that is not standardized. + For example: the `@context` can end up at the bottom for one file and stay at the top for another. So there are a couple of sorting methods to rearrange the keys of - the different dictionaries and those are called right before writing the jsonld file. + the different dictionaries and those are called right before writing the output file. Those methods enforces a certain order or keys in the output and also remove any empty or unknown keys. """ - # TODO allow for unknown keys to be added because otherwise adding new fields in the json always - # forces you to go and change those `schema_order` and `ui_order` otherwise they will not be added. - # - # This will require modifying the helper function `reorder_dict_skip_missing` - # - - def sort(self): + def sort(self) -> None: self.sort_schema() - self.sort_ui() + self.update_ui() def sort_schema(self) -> None: reordered_dict = self.reorder_dict_skip_missing(self.schema, self.schema_order) self.schema = reordered_dict - def sort_ui(self) -> None: - ui_order = self.ui.schema_order - reordered_dict = self.reorder_dict_skip_missing(self.schema["ui"], ui_order) - self.schema["ui"] = reordered_dict + def write(self, output_dir: Optional[Union[str, Path]] = None) -> None: + + self.sort() + + attrib_to_remove_if_empty = [ + "ui", + "preamble", + "landingPage", + "compute", + "question", + ] + for attrib in attrib_to_remove_if_empty: + if attrib in self.schema and self.schema[attrib] in [{}, [], ""]: + self.schema.pop(attrib) + + if output_dir is None: + output_dir = self.output_dir - def __write(self, output_dir: Union[str, Path]) -> None: - """ - Reused by the write method of the children classes - """ if isinstance(output_dir, str): output_dir = Path(output_dir) + + output_dir.mkdir(parents=True, exist_ok=True) + with open(output_dir.joinpath(self.at_id), "w") as ff: json.dump(self.schema, ff, sort_keys=False, indent=4) @classmethod def from_data(cls, data: dict): klass = cls() - if klass.type is None: + if klass.at_type is None: raise ValueError("SchemaBase cannot be used to instantiate class") - if klass.type != data["@type"]: - raise ValueError(f"Mismatch in type {data['@type']} != {klass.type}") + if klass.at_type != data["@type"]: + raise ValueError(f"Mismatch in type {data['@type']} != {klass.at_type}") klass.schema = data return klass @@ -268,7 +400,7 @@ def from_file(cls, filepath: Union[str, Path]): return cls.from_data(data) @staticmethod - def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> Dict: + def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> OrderedDict: """ reorders dictionary according to ``key_list`` removing any key with no associated value diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 45f184f..68bf755 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -1,21 +1,11 @@ +from pathlib import Path +from typing import Optional +from typing import Union + +from .base import DEFAULT_LANG from .base import SchemaBase from .response_options import ResponseOption -DEFAULT_LANG = "en" - -schema_order = [ - "@context", - "@type", - "@id", - "prefLabel", - "description", - "schemaVersion", - "version", - "ui", - "question", - "responseOptions", -] - class Item(SchemaBase): """ @@ -24,49 +14,84 @@ class to deal with reproschema items def __init__( self, - name: str = "item", - input_type: str = "text", - question: str = "", - schemaVersion=None, - suffix="", - ext=".jsonld", - visible: bool = True, - required: bool = False, - skippable: bool = True, - read_only: bool = False, + name: Optional[str] = "item", + input_type: Optional[str] = "text", + question: Optional[Union[dict, str]] = "", + schemaVersion: Optional[str] = None, + prefLabel="item", + description: Optional[str] = "", + preamble: Optional[str] = None, + visible: Optional[bool] = True, + required: Optional[bool] = False, + skippable: Optional[bool] = True, + read_only: Optional[bool] = None, + suffix: Optional[str] = "", + ext: Optional[str] = ".jsonld", + output_dir=Path.cwd(), + lang: Optional[str] = DEFAULT_LANG(), ): + + schema_order = [ + "@context", + "@type", + "@id", + "schemaVersion", + "version", + "prefLabel", + "description", + "preamble", + "question", + "responseOptions", + "ui", + ] + super().__init__( at_id=name, + at_type="reproschema:Field", schemaVersion=schemaVersion, - type="reproschema:Field", + prefLabel={lang: prefLabel}, + description=description, + preamble=preamble, schema_order=schema_order, + visible=visible, + required=required, + skippable=skippable, + readonlyValue=read_only, suffix=suffix, ext=ext, + output_dir=output_dir, + lang=lang, ) - self.schema["ui"] = self.ui.schema - self.schema["question"] = {self.lang: question} - self.input_type = input_type - self.visible = visible - self.required = required - self.skippable = skippable - self.question = question - self.read_only = (read_only,) - """ - The responseOptions dictionary is kept empty until the file has to be written - then it gets its content wit the method `set_response_options` - from the `options` dictionary of an instance of the ResponseOptions class - that is kept in ``self.response_options`` - """ + self.set_question(question=question) + self.response_options: ResponseOption = ResponseOption() self.set_response_options() - super().set_defaults(self.at_id) + self.input_type = input_type self.set_input_type() - def set_question(self, question, lang=DEFAULT_LANG): - # TODO add test to check adding several questions to an item - self.schema["question"][lang] = question + self.update() + + def set_question( + self, question: Optional[Union[str, dict]] = None, lang: Optional[str] = None + ) -> None: + + if question is None: + question = self.question + + if lang is None: + lang = self.lang + + if question == {}: + return + + if isinstance(question, str): + self.question[lang] = question + elif isinstance(question, dict): + self.question = question + + self.update() """ CREATE DIFFERENT ITEMS @@ -83,7 +108,7 @@ def set_question(self, question, lang=DEFAULT_LANG): # static: Static/Static.vue # StaticReadOnly: Static/Static.vue - def set_input_type(self): + def set_input_type(self) -> None: SUPPORTED_TYPES = ( "text", @@ -109,12 +134,12 @@ def set_input_type(self): ) if self.input_type == "text": - self.schema["ui"]["inputType"] = "text" + self.ui.inputType = "text" self.response_options.set_type("string") self.response_options.set_length(300) elif self.input_type == "multitext": - self.schema["ui"]["inputType"] = "multitext" + self.ui.inputType = "multitext" self.response_options.set_length(300) self.response_options.set_type("string") @@ -125,18 +150,18 @@ def set_input_type(self): self.set_input_type_numeric("float", "float") elif self.input_type == "year": - self.schema["ui"]["inputType"] = "year" + self.ui.inputType = "year" self.response_options.set_type("date") self.response_options.unset( ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] ) elif self.input_type == "date": - self.schema["ui"]["inputType"] = "date" + self.ui.inputType = "date" self.response_options.set_type("date") elif self.input_type == "time_range": - self.schema["ui"]["inputType"] = "timeRange" + self.ui.inputType = "timeRange" self.response_options.set_type("datetime") elif self.input_type == "language": @@ -149,11 +174,11 @@ def set_input_type(self): self.set_input_type_as_state() elif self.input_type == "email": - self.schema["ui"]["inputType"] = "email" + self.ui.inputType = "email" self.response_options.set_type("string") elif self.input_type == "id": - self.schema["ui"]["inputType"] = "pid" + self.ui.inputType = "pid" self.response_options.set_type("string") else: @@ -165,18 +190,15 @@ def set_input_type(self): """ ) - # to remove empty options keys - # self.response_options.sort() - - def set_input_type_numeric(self, arg0, arg1): - self.schema["ui"]["inputType"] = arg0 + def set_input_type_numeric(self, arg0: str, arg1: str) -> None: + self.ui.inputType = arg0 self.response_options.set_type(arg1) """ input types with preset response choices """ - def set_input_type_as_language(self): + def set_input_type_as_language(self) -> None: URL = self.set_input_from_preset( "https://raw.githubusercontent.com/ReproNim/reproschema-library/", "selectLanguage", @@ -184,7 +206,7 @@ def set_input_type_as_language(self): self.response_options.set_multiple_choice(True) self.response_options.use_preset(f"{URL}master/resources/languages.json") - def set_input_type_as_country(self): + def set_input_type_as_country(self) -> None: URL = self.set_input_from_preset( "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json", "selectCountry", @@ -192,16 +214,16 @@ def set_input_type_as_country(self): self.response_options.use_preset(URL) self.response_options.set_length(50) - def set_input_type_as_state(self): + def set_input_type_as_state(self) -> None: URL = self.set_input_from_preset( "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json", "selectState", ) self.response_options.use_preset(URL) - def set_input_from_preset(self, arg0, arg1): + def set_input_from_preset(self, arg0, arg1: str) -> str: result = arg0 - self.schema["ui"]["inputType"] = arg1 + self.ui.inputType = arg1 self.response_options.set_type("string") return result @@ -211,59 +233,43 @@ def set_input_from_preset(self, arg0, arg1): Those methods require an instance of ResponseOptions as input and it will replace the one initialized in the construction. - - Most likely a bad idea and a confusing API from the user perpective: - probably better to set the input type and then let the user construct - the response choices via calls to the methods of - - self.response_options """ - def set_input_type_as_radio(self, response_options: ResponseOption): + def set_input_type_as_radio(self, response_options: ResponseOption) -> None: self.set_input_type_rasesli("radio", response_options) - def set_input_type_as_select(self, response_options: ResponseOption): + def set_input_type_as_select(self, response_options: ResponseOption) -> None: self.set_input_type_rasesli("select", response_options) - def set_input_type_as_slider(self, response_options: ResponseOption): + def set_input_type_as_slider(self, response_options: ResponseOption) -> None: response_options.set_multiple_choice(False) self.set_input_type_rasesli("slider", response_options) - def set_input_type_rasesli(self, arg0, response_options: ResponseOption): - self.schema["ui"]["inputType"] = arg0 + def set_input_type_rasesli(self, arg0, response_options: ResponseOption) -> None: + self.ui.inputType = arg0 response_options.set_type("integer") self.response_options = response_options - """ - UI - """ - - def set_read_only(self, value: bool = False): - self.read_only = value - self.schema["ui"]["readonlyValue"] = self.read_only - """ writing, reading, sorting, unsetting """ - def set_response_options(self): + def set_response_options(self) -> None: """ Passes the content of the response options to the schema of the item. To be done before writing the item """ - self.schema["responseOptions"] = self.response_options.options + self.schema["responseOptions"] = self.response_options.schema - def unset(self, keys): + def unset(self, keys) -> None: """ Mostly used to remove some empty keys from the schema. Rarely used. """ for i in keys: self.schema.pop(i, None) - def write(self, output_dir): - self.sort() + def write(self, output_dir=None) -> None: + if output_dir is None: + output_dir = self.output_dir self.set_response_options() - self._SchemaBase__write(output_dir) - - def sort(self): - self.sort_schema() + super().write(output_dir) diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index 681650d..dd38d0f 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -1,26 +1,13 @@ +from pathlib import Path +from typing import Dict +from typing import Optional +from typing import Union + from .activity import Activity +from .base import DEFAULT_LANG from .base import SchemaBase -DEFAULT_LANG = "en" - -schema_order = [ - "@context", - "@type", - "@id", - "prefLabel", - "description", - "schemaVersion", - "version", - "landingPage", - "preamble", - "citation", - "image", - "compute", - "ui", -] - - class Protocol(SchemaBase): """ class to deal with reproschema protocols @@ -28,68 +15,51 @@ class to deal with reproschema protocols def __init__( self, - name="protocol", - schemaVersion=None, - prefLabel="protocol", - suffix="_schema", - ext=".jsonld", - lang=DEFAULT_LANG, + name: Optional[str] = "protocol", + schemaVersion: Optional[str] = None, + prefLabel: Optional[Union[str, Dict[str, str]]] = "protocol", + description: Optional[str] = "", + landingPage: Optional[dict] = None, + citation: Optional[str] = None, + suffix: Optional[str] = "_schema", + ext: Optional[str] = ".jsonld", + output_dir: Optional[Union[str, Path]] = Path.cwd(), + lang: Optional[str] = DEFAULT_LANG(), ): - """ - Rely on the parent class for construction of the instance - """ + + schema_order = [ + "@context", + "@type", + "@id", + "schemaVersion", + "version", + "prefLabel", + "description", + "landingPage", + "citation", + "image", + "compute", + "ui", + ] + super().__init__( at_id=name, + at_type="reproschema:Protocol", schemaVersion=schemaVersion, - type="reproschema:Protocol", prefLabel={lang: prefLabel}, + description=description, + landingPage=landingPage, + citation=citation, schema_order=schema_order, suffix=suffix, ext=ext, + output_dir=output_dir, lang=lang, ) - - def set_defaults(self, name=None): - if not name: - name = self.at_id - super().set_defaults(name) - self.set_landing_page("README-en.md") - self.set_preamble(name) - self.set_ui_default() - - def set_landing_page(self, landing_page_uri: str, lang=DEFAULT_LANG): - self.schema["landingPage"] = {"@id": landing_page_uri, "inLanguage": lang} + self.ui.shuffle = False + self.update() def append_activity(self, activity: Activity): - """ - We get from an activity instance the info we need to update the protocol scheme. - - This appends the activity after all the other ones. - - So this means the order of the activities will be dependent - on the order in which they are "read". - - This implementation assumes that the activities are read - from a list and added one after the other. - """ - # TODO - # - find a way to reorder, remove or add an activity - # at any point in the protocol - - activity_property = { - # variable name is name of activity without prefix - "variableName": activity.get_basename().replace("_schema", ""), - "isAbout": activity.URI, - "prefLabel": activity.prefLabel, - "isVis": activity.visible, - "requiredValue": activity.required, - } - self.append_to_ui(activity, activity_property) - - """ - writing, reading, sorting, unsetting - """ - - def write(self, output_dir): - self.sort() - self._SchemaBase__write(output_dir) + self.ui.append( + obj=activity, variableName=activity.get_basename().replace("_schema", "") + ) diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index c271111..438dcd6 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -1,17 +1,8 @@ -from .base import SchemaBase - -DEFAULT_LANG = "en" +from pathlib import Path +from typing import Optional -options_order = [ - "@context", - "@type", - "@id", - "valueType", - "minValue", - "maxValue", - "multipleChoice", - "choices", -] +from .base import DEFAULT_LANG +from .base import SchemaBase class ResponseOption(SchemaBase): @@ -19,86 +10,89 @@ class ResponseOption(SchemaBase): class to deal with reproschema response options """ - # the dictionary that keeps track of the content of the response options should - # be called "schema" and not "options" so as to be able to make proper use of the - # methods of the parent class and avoid copying content between - # - # self.options and self.schema - def __init__( - self, name="valueConstraints", multiple_choice=None, schemaVersion=None + self, + name="valueConstraints", + multiple_choice=None, + schemaVersion=None, + output_dir=Path.cwd(), ): + + options_order = [ + "@context", + "@type", + "@id", + "valueType", + "minValue", + "maxValue", + "multipleChoice", + "choices", + ] + super().__init__( at_id=name, + at_type="reproschema:ResponseOption", schema_order=options_order, schemaVersion=schemaVersion, - type="reproschema:ResponseOption", ext=".jsonld", suffix="", + output_dir=output_dir, ) - self.options = {"choices": []} - self.set_multiple_choice(multiple_choice) - def set_defaults(self): - self.options["@context"] = self.schema["@context"] - self.options["@type"] = self.type - self.set_filename(self.at_id) + self.schema = {"choices": []} + self.set_multiple_choice(multiple_choice) - def set_filename(self, name: str): - super().set_filename(name) - self.options["@id"] = self.at_id + def set_defaults(self) -> None: + self.schema["@type"] = self.at_type + self.set_filename() - def unset(self, keys): + def unset(self, keys) -> None: if type(keys) == str: keys = [keys] for i in keys: - self.options.pop(i, None) + self.schema.pop(i, None) - def set_type(self, value: str = None): + def set_type(self, value: str = None) -> None: if value is not None: - self.options["valueType"] = f"xsd:{value}" + self.schema["valueType"] = f"xsd:{value}" def values_all_options(self): - return [i["value"] for i in self.options["choices"] if "value" in i] + return [i["value"] for i in self.schema["choices"] if "value" in i] - def set_min(self, value: int = None): + def set_min(self, value: int = None) -> None: if value is not None: - self.options["minValue"] = value - elif len(self.options["choices"]) > 1: - self.options["minValue"] = min(self.values_all_options()) + self.schema["minValue"] = value + elif len(self.schema["choices"]) > 1: + self.schema["minValue"] = min(self.values_all_options()) - def set_max(self, value: int = None): + def set_max(self, value: int = None) -> None: if value is not None: - self.options["maxValue"] = value - elif len(self.options["choices"]) > 1: - self.options["maxValue"] = max(self.values_all_options()) + self.schema["maxValue"] = value + elif len(self.schema["choices"]) > 1: + self.schema["maxValue"] = max(self.values_all_options()) - def set_length(self, value: int = None): + def set_length(self, value: int = None) -> None: if value is not None: - self.options["maxLength"] = value + self.schema["maxLength"] = value + + # TODO - def set_multiple_choice(self, value: bool = None): + def set_multiple_choice(self, value: bool = None) -> None: if value is not None: self.multiple_choice = value - self.options["multipleChoice"] = value + self.schema["multipleChoice"] = value - def use_preset(self, URI): + def use_preset(self, URI) -> None: """ In case the list response options are read from another file like for languages, country, state... """ - self.options["choices"] = URI + self.schema["choices"] = URI - def add_choice(self, choice, value, lang=DEFAULT_LANG): - self.options["choices"].append({"name": {lang: choice}, "value": value}) + def add_choice(self, choice, value, lang: Optional[str] = None) -> None: + if lang is None: + lang = DEFAULT_LANG() + # TODO replace existing choice if already set + self.schema["choices"].append({"name": {lang: choice}, "value": value}) self.set_max() self.set_min() - - def sort(self): - reordered_dict = self.reorder_dict_skip_missing(self.options, self.schema_order) - self.options = reordered_dict - - def write(self, output_dir): - self.sort() - self.schema = self.options - self._SchemaBase__write(output_dir) diff --git a/reproschema/models/tests/data/activities/activity1_schema.jsonld b/reproschema/models/tests/data/activities/activity1_schema.jsonld index e3c4aee..0666968 100644 --- a/reproschema/models/tests/data/activities/activity1_schema.jsonld +++ b/reproschema/models/tests/data/activities/activity1_schema.jsonld @@ -8,14 +8,14 @@ "description": "Activity example 1", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", + "preamble": { + "en": "Over the last 2 weeks, how often have you been bothered by any of the following problems?" + }, "citation": "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", "image": { "@type": "AudioObject", "contentUrl": "http://example.com/sample-image.png" }, - "preamble": { - "en": "Over the last 2 weeks, how often have you been bothered by any of the following problems?" - }, "compute": [ { "variableName": "activity1_total_score", @@ -23,35 +23,44 @@ } ], "ui": { + "shuffle": false, + "order": [ + "items/item1.jsonld", + "../other_dir/item_two.jsonld", + "items/activity1_total_score" + ], "addProperties": [ { - "isAbout": "items/item1.jsonld", "variableName": "item1", - "requiredValue": true, - "isVis": true + "isAbout": "items/item1.jsonld", + "prefLabel": { + "en": "item1" + }, + "isVis": true, + "requiredValue": true }, { "variableName": "item_two", "isAbout": "../other_dir/item_two.jsonld", - "requiredValue": true, + "prefLabel": { + "en": "item2" + }, "isVis": true, + "requiredValue": true, "allow": [ "reproschema:Skipped" ] }, { - "isAbout": "items/activity1_total_score", "variableName": "activity1_total_score", - "requiredValue": true, - "isVis": false + "isAbout": "items/activity1_total_score", + "prefLabel": { + "en": "activity1 total score" + }, + "isVis": false, + "requiredValue": true } ], - "order": [ - "items/item1.jsonld", - "../other_dir/item_two.jsonld", - "items/activity1_total_score" - ], - "shuffle": false, "allow": [ "reproschema:AutoAdvance", "reproschema:AllowExport" diff --git a/reproschema/models/tests/data/activities/default_schema.jsonld b/reproschema/models/tests/data/activities/default_schema.jsonld index 63c5950..121e4c8 100644 --- a/reproschema/models/tests/data/activities/default_schema.jsonld +++ b/reproschema/models/tests/data/activities/default_schema.jsonld @@ -8,9 +8,6 @@ "description": "default", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", - "preamble": { - "en": "default" - }, "ui": { "shuffle": false, "allow": [ diff --git a/reproschema/models/tests/data/protocols/default_schema.jsonld b/reproschema/models/tests/data/protocols/default_schema.jsonld index e9c76b4..ee6fb51 100644 --- a/reproschema/models/tests/data/protocols/default_schema.jsonld +++ b/reproschema/models/tests/data/protocols/default_schema.jsonld @@ -8,13 +8,6 @@ "description": "default", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", - "landingPage": { - "@id": "README-en.md", - "inLanguage": "en" - }, - "preamble": { - "en": "default" - }, "ui": { "allow": [ "reproschema:AutoAdvance", diff --git a/reproschema/models/tests/data/protocols/protocol1_schema.jsonld b/reproschema/models/tests/data/protocols/protocol1_schema.jsonld index 303ba2e..4b1aa9d 100644 --- a/reproschema/models/tests/data/protocols/protocol1_schema.jsonld +++ b/reproschema/models/tests/data/protocols/protocol1_schema.jsonld @@ -12,9 +12,6 @@ "@id": "http://example.com/sample-readme.md", "inLanguage": "en" }, - "preamble": { - "en": "protocol1" - }, "ui": { "addProperties": [ { diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index 9dd8397..05dc195 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -14,13 +14,11 @@ def test_default(): """ FYI: The default activity does not conform to the schema - so `reproschema validate` will complain if you run it in this + so `reproschema validate` will complain if you run it on this """ - activity = Activity(name="default") - activity.set_defaults() - - activity.write(activity_dir) + activity = Activity(name="default", output_dir=activity_dir) + activity.write() activity_content, expected = load_jsons(activity_dir, activity) assert activity_content == expected @@ -30,54 +28,45 @@ def test_default(): def test_activity(): - activity = Activity(name="activity1") - activity.set_defaults() - activity.set_description("Activity example 1") - activity.set_pref_label("Example 1") + activity = Activity( + name="activity1", + prefLabel="Example 1", + description="Activity example 1", + citation="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", + output_dir=activity_dir, + ) activity.set_preamble( "Over the last 2 weeks, how often have you been bothered by any of the following problems?" ) - activity.set_citation("https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/") activity.set_image( {"@type": "AudioObject", "contentUrl": "http://example.com/sample-image.png"} ) activity.set_compute("activity1_total_score", "item1 + item2") - item_1 = Item(name="item1") - # TODO - # probably want to have items/item_name be a default - item_1.URI = os.path.join("items", item_1.at_id) - # TODO - # We probably want a method to change those values rather that modifying - # the instance directly - item_1.skippable = False - item_1.required = True - """ - Items are appended and this updates the the ``ui`` ``order`` and ``addProperties`` - """ - activity.append_item(item_1) + item_1 = Item(name="item1", skippable=False, required=True, output_dir="items") - item_2 = Item(name="item2") + item_2 = Item( + name="item2", required=True, output_dir=os.path.join("..", "other_dir") + ) item_2.set_filename("item_two") - """ - In this case the URI is relative to where the activity file will be saved - """ - item_2.URI = os.path.join("..", "other_dir", item_2.at_id) - item_2.required = True - activity.append_item(item_2) - """ By default all files are save with a jsonld extension but this can be changed """ - item_3 = Item(name="activity1_total_score", ext="") - item_3.URI = os.path.join("items", item_3.at_id) - item_3.skippable = False - item_3.required = True - item_3.visible = False + item_3 = Item( + name="activity1_total_score", + ext="", + skippable=False, + required=True, + visible=False, + output_dir="items", + ) + + activity.append_item(item_1) + activity.append_item(item_2) activity.append_item(item_3) - activity.write(activity_dir) + activity.write() activity_content, expected = load_jsons(activity_dir, activity) assert activity_content == expected diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index 79a0f6a..a2794ff 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -15,9 +15,10 @@ def test_default(): - item = Item(name="default") + item = Item(name="default", output_dir=item_dir) + print(item.question) - item.write(item_dir) + item.write() item_content, expected = load_jsons(item_dir, item) assert item_content == expected @@ -43,9 +44,11 @@ def test_default(): ) def test_items(name, question, input_type): - item = Item(name=name, question=question, input_type=input_type) + item = Item( + name=name, question=question, input_type=input_type, output_dir=item_dir + ) - item.write(item_dir) + item.write() item_content, expected = load_jsons(item_dir, item) assert item_content == expected @@ -67,28 +70,27 @@ def test_items(name, question, input_type): def test_radio(): - item = Item(name="radio", question="question for radio item") - - response_options = ResponseOption() + response_options = ResponseOption(multiple_choice=False) response_options.add_choice("Not at all", 0, "en") response_options.add_choice("Several days", 1, "en") - response_options.set_multiple_choice(False) + item = Item(name="radio", question="question for radio item", output_dir=item_dir) item.set_input_type_as_radio(response_options) - item.write(item_dir) + item.write() item_content, expected = load_jsons(item_dir, item) assert item_content == expected clean_up(item_dir, item) item.set_filename("radio multiple") - item.set_description("radio multiple") + item.description = "radio multiple" + item.update() item.set_pref_label("radio multiple") item.set_question("question for radio item with multiple responses") response_options.set_multiple_choice(True) item.set_input_type_as_radio(response_options) - item.write(item_dir) + item.write() item_content, expected = load_jsons(item_dir, item) assert item_content == expected @@ -98,29 +100,28 @@ def test_radio(): def test_select(): - item = Item(name="select", question="question for select item") - - response_options = ResponseOption() + response_options = ResponseOption(multiple_choice=False) response_options.add_choice("Response option 1", 0) response_options.add_choice("Response option 2", 1) response_options.add_choice("Response option 3", 2) - response_options.set_multiple_choice(False) + item = Item(name="select", question="question for select item", output_dir=item_dir) item.set_input_type_as_select(response_options) - item.write(item_dir) + item.write() item_content, expected = load_jsons(item_dir, item) assert item_content == expected clean_up(item_dir, item) item.set_filename("select multiple") - item.set_description("select multiple") + item.description = "select multiple" + item.update() item.set_pref_label("select multiple") item.set_question("question for select item with multiple responses") response_options.set_multiple_choice(True) item.set_input_type_as_select(response_options) - item.write(item_dir) + item.write() item_content, expected = load_jsons(item_dir, item) assert item_content == expected @@ -130,8 +131,6 @@ def test_select(): def test_slider(): - item = Item(name="slider", question="question for slider item") - response_options = ResponseOption() response_options.add_choice("not at all", 0) response_options.add_choice("a bit", 1) @@ -139,9 +138,10 @@ def test_slider(): response_options.add_choice("a lot", 3) response_options.add_choice("very much", 4) + item = Item(name="slider", question="question for slider item", output_dir=item_dir) item.set_input_type_as_slider(response_options) - item.write(item_dir) + item.write() item_content, expected = load_jsons(item_dir, item) assert item_content == expected @@ -160,18 +160,23 @@ def test_slider(): def test_read_only(): - item = Item(name="activity1_total_score", ext="", input_type="int") + item = Item( + name="activity1_total_score", + ext="", + input_type="int", + read_only=True, + output_dir=item_dir, + ) item.at_context = "../../../contexts/generic" + item.description = "Score item for Activity 1" item.update() item.set_filename("activity1_total_score") - item.schema["prefLabel"] = "activity1_total_score" - item.set_description("Score item for Activity 1") - item.set_read_only(True) + item.set_pref_label("activity1_total_score") item.response_options.set_max(3) item.response_options.set_min(0) item.unset(["question"]) - item.write(item_dir) + item.write() output_file = os.path.join(item_dir, item.at_id) item_content = read_json(output_file) diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py index 6dd8799..8410c1c 100644 --- a/reproschema/models/tests/test_protocol.py +++ b/reproschema/models/tests/test_protocol.py @@ -18,9 +18,8 @@ def test_default(): so `reproschema validate` will complain if you run it in this """ - protocol = Protocol(name="default") - protocol.set_defaults() - protocol.write(protocol_dir) + protocol = Protocol(name="default", output_dir=protocol_dir) + protocol.write() protocol_content, expected = load_jsons(protocol_dir, protocol) assert protocol_content == expected @@ -30,24 +29,30 @@ def test_default(): def test_protocol(): - protocol = Protocol(name="protocol1") - protocol.set_defaults() - protocol.set_pref_label(pref_label="Protocol1", lang="en") - protocol.set_description("example Protocol") - protocol.set_landing_page("http://example.com/sample-readme.md") - - auto_advance = True - allow_export = True - disable_back = True - protocol.set_ui_allow(auto_advance, allow_export, disable_back) - - activity_1 = Activity() - activity_1.set_defaults("activity1") - activity_1.set_pref_label("Screening") - activity_1.URI = os.path.join("..", "activities", activity_1.at_id) + protocol = Protocol( + name="protocol1", + prefLabel="Protocol1", + lang="en", + description="example Protocol", + output_dir=protocol_dir, + ) + protocol.set_preamble(preamble="protocol1", lang="en") + protocol.set_landing_page(page="http://example.com/sample-readme.md") + protocol.ui.AutoAdvance = True + protocol.ui.AllowExport = True + protocol.ui.DisableBack = True + protocol.update() + + activity_1 = Activity( + name="activity1", + prefLabel="Screening", + lang="en", + output_dir=os.path.join("..", "activities"), + ) + protocol.append_activity(activity_1) - protocol.write(protocol_dir) + protocol.write() protocol_content, expected = load_jsons(protocol_dir, protocol) assert protocol_content == expected diff --git a/reproschema/models/tests/test_response_options.py b/reproschema/models/tests/test_response_options.py index a4ef055..6b94c63 100644 --- a/reproschema/models/tests/test_response_options.py +++ b/reproschema/models/tests/test_response_options.py @@ -1,3 +1,4 @@ +from utils import clean_up from utils import load_jsons from utils import output_dir @@ -6,19 +7,9 @@ response_options_dir = output_dir("response_options") -def test_default(): - - response_options = ResponseOption() - response_options.set_defaults() - - response_options.write(response_options_dir) - content, expected = load_jsons(response_options_dir, response_options) - assert content == expected - - def test_example(): - response_options = ResponseOption() + response_options = ResponseOption(output_dir=response_options_dir) response_options.set_defaults() response_options.set_filename("example") response_options.set_type("integer") @@ -28,6 +19,20 @@ def test_example(): response_options.add_choice("", i) response_options.add_choice("Completely", 6) - response_options.write(response_options_dir) + response_options.write() + content, expected = load_jsons(response_options_dir, response_options) + assert content == expected + + clean_up(response_options_dir, response_options) + + +def test_default(): + + response_options = ResponseOption(output_dir=response_options_dir) + response_options.set_defaults() + + response_options.write() content, expected = load_jsons(response_options_dir, response_options) assert content == expected + + clean_up(response_options_dir, response_options) diff --git a/reproschema/models/tests/test_ui.py b/reproschema/models/tests/test_ui.py index 13cd21b..c195fc3 100644 --- a/reproschema/models/tests/test_ui.py +++ b/reproschema/models/tests/test_ui.py @@ -1,5 +1,20 @@ +from collections import OrderedDict + from reproschema.models.ui import UI -a = UI(type="reproschema:Field") -print(a) +def test_field(): + a = UI(at_type="reproschema:Field") + assert a.schema_order == ["inputType", "readonlyValue"] + print(a.schema) + assert a.schema == OrderedDict() + + +def test_protocol(): + a = UI(at_type="reproschema:Protocol", shuffle=False) + assert a.schema == OrderedDict( + { + "shuffle": False, + "allow": ["reproschema:AutoAdvance", "reproschema:AllowExport"], + } + ) diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index 5be5f17..6380950 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -1,88 +1,195 @@ from collections import OrderedDict +from typing import Any from typing import Dict from typing import List +from typing import Optional -from attrs import converters from attrs import define from attrs import field +from attrs.converters import default_if_none +from attrs.validators import in_ +from attrs.validators import instance_of +from attrs.validators import optional -@define +@define( + kw_only=True, +) class UI: - type = field(default=[None]) - add_properties: list = field(default=[]) - order: List[str] = field(default=[None]) - allow: list = field(default=[]) - shuffle: bool = field(default=False) - auto_advance: bool = field(default=True) - disable_back: bool = field(default=False) - allow_export: bool = field(default=True) - - schema: dict = field( - default={ + + SUPPORTED_INPUT_TYPES = ( + "text", + "multitext", + "number", + "float", + "date", + "time", + "timeRange", + "year", + "selectLanguage", + "selectCountry", + "selectState", + "email", + "pid", + "select", + "radio", + "slider", + ) + + at_type: str = field( + factory=str, + validator=in_( + [ + "reproschema:Protocol", + "reproschema:Activity", + "reproschema:Field", + "reproschema:ResponseOption", + ] + ), + ) + + shuffle: Optional[bool] = field( + default=None, + validator=optional(instance_of(bool)), + ) + + addProperties: Optional[List[Dict[str, Any]]] = field( + factory=(list), + validator=optional(instance_of(list)), + ) + + order: List[str] = field( + factory=(list), + validator=optional(instance_of(list)), + ) + + AutoAdvance: Optional[bool] = field( + default=None, + converter=default_if_none(default=True), # type: ignore + validator=optional(instance_of(bool)), + ) + + DisableBack: Optional[bool] = field( + default=None, + converter=default_if_none(default=False), # type: ignore + validator=optional(instance_of(bool)), + ) + + AllowExport: Optional[bool] = field( + default=None, + converter=default_if_none(default=True), # type: ignore + validator=optional(instance_of(bool)), + ) + + readonlyValue: Optional[bool] = field( + default=None, + validator=optional(instance_of(bool)), + ) + + inputType: Optional[str] = field( + default=None, validator=optional(in_(SUPPORTED_INPUT_TYPES)) + ) + + allow: Optional[List[str]] = field( + default=None, + converter=default_if_none(default=[]), # type: ignore + validator=optional(instance_of(list)), + ) + + schema_order: Optional[List[str]] = field( + validator=optional(instance_of(list)), + ) + + @schema_order.default + def _default_schema_order(self) -> list: + default = ["shuffle", "order", "addProperties", "allow"] + if self.at_type == "reproschema:Field": + default = ["inputType", "readonlyValue"] + return default + + schema: Optional[Dict[str, Any]] = field( + validator=optional(instance_of(dict)), + ) + + @schema.default + def _default_schema(self) -> dict: + default = { "shuffle": [], "order": [], "addProperties": [], "allow": [], } - ) - - schema_order = field( - default=["shuffle", "order", "addProperties", "allow"], - converter=converters.default_if_none(default=[]), # type: ignore - ) + if self.at_type == "reproschema:Field": + default = {"inputType": [], "readonlyValue": []} + return default def __attrs_post_init__(self) -> None: - if self.type == "reproschema:Field": - self.schema_order = ["inputType", "readonlyValue"] - self.schema = {"inputType": [], "readonlyValue": []} - def AutoAdvance(self, value: bool = None) -> None: - if not value: - return - self.auto_advance = value - if self.auto_advance and "reproschema:AutoAdvance" not in self.allow: - self.allow.append("reproschema:AutoAdvance") - elif not self.auto_advance and "reproschema:AutoAdvance" in self.allow: - self.allow.remove("reproschema:AutoAdvance") - - def DisableBack(self, value: bool = None) -> None: - if not value: - return - self.disable_back = value - if self.disable_back and "reproschema:DisableBack" not in self.allow: - self.allow.append("reproschema:DisableBack") - elif not self.disable_back and "reproschema:DisableBack" in self.allow: - self.allow.remove("reproschema:DisableBack") - - def AllowExport(self, value: bool = None) -> None: - if not value: - return - self.allow_export = value - if self.allow_export and "reproschema:AllowExport" not in self.allow: - self.allow.append("reproschema:AllowExport") - elif not self.allow_export and "reproschema:AllowExport" in self.allow: - self.allow.remove("reproschema:AllowExport") + if self.at_type == "reproschema:ResponseOption": + self.AllowExport = False + self.AutoAdvance = False + + self.update() + + def append(self, obj=None, variableName: Optional[str] = None) -> None: + obj_properties = { + "variableName": variableName, + "isAbout": obj.URI, + "prefLabel": obj.prefLabel, + "isVis": obj.visible, + "requiredValue": obj.required, + } + if obj.skippable: + obj_properties["allow"] = ["reproschema:Skipped"] + + self.order.append(obj.URI) + self.addProperties.append(obj_properties) + self.update() def update(self) -> None: + + for attrib in ["DisableBack", "AutoAdvance", "AllowExport"]: + if ( + self.__getattribute__(attrib) + and f"reproschema:{attrib}" not in self.allow + ): + self.allow.append(f"reproschema:{attrib}") + elif ( + not self.__getattribute__(attrib) + and f"reproschema:{attrib}" in self.allow + ): + self.allow.remove(f"reproschema:{attrib}") + + if self.readonlyValue is not None: + self.schema["readonlyValue"] = self.readonlyValue + self.schema["shuffle"] = self.shuffle self.schema["order"] = self.order - self.schema["addProperties"] = self.add_properties + self.schema["addProperties"] = self.addProperties self.schema["allow"] = self.allow - self.schema = self.sort() - def sort(self) -> Dict: + self.schema["inputType"] = self.inputType + + self.sort() + + def sort(self) -> Dict[str, Any]: + if self.schema is None: + return reordered_dict = self.reorder_dict_skip_missing(self.schema, self.schema_order) self.schema = reordered_dict return reordered_dict @staticmethod - def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> Dict: + def reorder_dict_skip_missing( + old_dict: Dict[Any, Any], key_list: List[Any] + ) -> Dict: """ reorders dictionary according to ``key_list`` removing any key with no associated value or that is not in the key list """ return OrderedDict( - (k, old_dict[k]) for k in key_list if (k in old_dict and old_dict[k]) + (k, old_dict[k]) + for k in key_list + if (k in old_dict and old_dict[k] not in [[], "", None]) ) diff --git a/reproschema/tests/data/activities/items/activity1_total_score b/reproschema/tests/data/activities/items/activity1_total_score index 5ed7f84..150d9c0 100644 --- a/reproschema/tests/data/activities/items/activity1_total_score +++ b/reproschema/tests/data/activities/items/activity1_total_score @@ -2,7 +2,9 @@ "@context": "../../../contexts/generic", "@type": "reproschema:Field", "@id": "activity1_total_score", - "prefLabel": "activity1_total_score", + "prefLabel": { + "en": "activity1_total_score" + }, "description": "Score item for Activity 1", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", From dd4df022a636a51f1ad5d1f3f80373abfc3b29ff Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sun, 14 Aug 2022 23:37:00 +0200 Subject: [PATCH 50/69] add test for protocol from reproschema repo --- reproschema/models/base.py | 2 +- .../tests/data/activities/activity1.jsonld | 59 +++++++++++++++++++ .../tests/data/protocols/protocol1.jsonld | 45 ++++++++++++++ reproschema/models/tests/test_protocol.py | 43 ++++++++++++++ reproschema/models/ui.py | 6 +- 5 files changed, 152 insertions(+), 3 deletions(-) create mode 100644 reproschema/models/tests/data/activities/activity1.jsonld create mode 100644 reproschema/models/tests/data/protocols/protocol1.jsonld diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 2f1be65..faa3411 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -94,6 +94,7 @@ def _default_context(self) -> str: converter=default_if_none(default={}), # type: ignore validator=optional(instance_of(dict)), ) + # TODO description is language specific description: str = field( factory=(str), converter=default_if_none(default=""), # type: ignore @@ -170,7 +171,6 @@ def _default_ui(self) -> UI: ) required: Optional[bool] = field( factory=(bool), - converter=default_if_none(default=False), # type: ignore validator=optional(instance_of(bool)), ) skippable: Optional[bool] = field( diff --git a/reproschema/models/tests/data/activities/activity1.jsonld b/reproschema/models/tests/data/activities/activity1.jsonld new file mode 100644 index 0000000..6a533ee --- /dev/null +++ b/reproschema/models/tests/data/activities/activity1.jsonld @@ -0,0 +1,59 @@ +{ + "@context": "../../contexts/generic", + "@type": "reproschema:Activity", + "@id": "activity1.jsonld", + "prefLabel": "Example 1", + "description": "Activity example 1", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "citation": "https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", + "image": { + "@type": "AudioObject", + "contentUrl": "http://example.com/sample-image.png" + }, + "preamble": { + "en": "Over the last 2 weeks, how often have you been bothered by any of the following problems?", + "es": "Durante las últimas 2 semanas, ¿con qué frecuencia le han molestado los siguintes problemas?" + }, + "compute": [ + { + "variableName": "activity1_total_score", + "jsExpression": "item1 + item2" + } + ], + "messages": [ + { + "message": "Test message: Triggered when item1 value is greater than 1", + "jsExpression": "item1 > 1" + } + ], + "ui": { + "addProperties": [ + { "isAbout": "items/item1.jsonld", + "variableName": "item1", + "requiredValue": true, + "isVis": true, + "randomMaxDelay": "PT2H", + "limit": "P2D", + "schedule": "R/2020-08-01T08:00:00Z/P1D" + }, + { "isAbout": "items/item2.jsonld", + "variableName": "item2", + "requiredValue": true, + "isVis": true, + "allow": ["reproschema:Skipped"] + }, + { "isAbout": "items/activity1_total_score", + "variableName": "activity1_total_score", + "requiredValue": true, + "isVis": false + } + ], + "order": [ + "items/item1.jsonld", + "items/item2.jsonld", + "items/activity1_total_score" + ], + "shuffle": false + } +} diff --git a/reproschema/models/tests/data/protocols/protocol1.jsonld b/reproschema/models/tests/data/protocols/protocol1.jsonld new file mode 100644 index 0000000..580d099 --- /dev/null +++ b/reproschema/models/tests/data/protocols/protocol1.jsonld @@ -0,0 +1,45 @@ +{ + "@context": "../../contexts/generic", + "@type": "reproschema:Protocol", + "@id": "protocol1.jsonld", + "prefLabel": { + "en": "Protocol1", + "es": "Protocol1_es" + }, + "description": "example Protocol", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "landingPage": {"@id": "http://example.com/sample-readme.md", + "inLanguage": "en"}, + "messages": [ + { + "message": "Test message: Triggered when item1 value is greater than 0", + "jsExpression": "item1 > 0" + } + ], + "ui": { + "addProperties": [ + { + "isAbout": "../activities/activity1.jsonld", + "variableName": "activity1", + "prefLabel": { + "en": "Screening", + "es": "Screening_es" + }, + "isVis": true, + "schedule": "R5/2008-01-01T13:00:00Z/P1Y2M10DT2H30M", + "randomMaxDelay": "PT12H", + "limit": "P1W/2020-08-01T13:00:00Z" + } + ], + "order": [ + "../activities/activity1.jsonld" + ], + "shuffle": false, + "allow": [ + "reproschema:AutoAdvance", + "reproschema:AllowExport", + "reproschema:DisableBack" + ] + } +} diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py index 8410c1c..4dbd502 100644 --- a/reproschema/models/tests/test_protocol.py +++ b/reproschema/models/tests/test_protocol.py @@ -58,3 +58,46 @@ def test_protocol(): assert protocol_content == expected clean_up(protocol_dir, protocol) + + +def test_protocol_1(): + + protocol = Protocol( + name="protocol1", + prefLabel="Protocol1", + lang="en", + description="example Protocol", + output_dir=protocol_dir, + suffix="", + ) + protocol.set_pref_label(pref_label="Protocol1_es", lang="es") + protocol.set_landing_page(page="http://example.com/sample-readme.md", lang="en") + protocol.ui.AutoAdvance = True + protocol.ui.AllowExport = True + protocol.ui.DisableBack = True + protocol.at_context = "../../contexts/generic" + protocol.update() + + activity_1 = Activity( + name="activity1", + prefLabel="Screening", + lang="en", + suffix="", + output_dir=os.path.join("..", "activities"), + visible=True, + skippable=False, + required=None, + ) + + activity_1.ui.shuffle = False + activity_1.update() + activity_1.set_pref_label(pref_label="Screening_es", lang="es") + + protocol.append_activity(activity_1) + + protocol.write() + + protocol_content, expected = load_jsons(protocol_dir, protocol) + assert protocol_content == expected + + clean_up(protocol_dir, protocol) diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index 6380950..7966508 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -137,9 +137,11 @@ def append(self, obj=None, variableName: Optional[str] = None) -> None: "isAbout": obj.URI, "prefLabel": obj.prefLabel, "isVis": obj.visible, - "requiredValue": obj.required, } - if obj.skippable: + if obj.required is not None: + obj_properties["requiredValue"] = obj.required + + if obj.skippable is True: obj_properties["allow"] = ["reproschema:Skipped"] self.order.append(obj.URI) From 5536b6ab15fc15bb41e8ccf43eb1b2f9458c9316 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 09:30:06 +0200 Subject: [PATCH 51/69] refactor refactor --- reproschema/models/activity.py | 19 ++-- reproschema/models/base.py | 78 ++++++++++------ reproschema/models/item.py | 90 ++++++++----------- reproschema/models/protocol.py | 20 ++--- .../tests/data/activities/activity1.jsonld | 2 +- .../models/tests/data/items/item1.jsonld | 72 +++++++++++++++ .../models/tests/data/items/item2.jsonld | 40 +++++++++ .../data/protocols/protocol1_schema.jsonld | 79 ++++++++-------- reproschema/models/tests/test_activity.py | 68 +++++++++++++- reproschema/models/tests/test_item.py | 16 ++-- reproschema/models/ui.py | 4 +- 11 files changed, 336 insertions(+), 152 deletions(-) create mode 100644 reproschema/models/tests/data/items/item1.jsonld create mode 100644 reproschema/models/tests/data/items/item2.jsonld diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index b16d277..8f186e0 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -3,6 +3,7 @@ from typing import Optional from typing import Union +from .base import COMMON_SCHEMA_ORDER from .base import DEFAULT_LANG from .base import SchemaBase from .item import Item @@ -17,10 +18,12 @@ def __init__( self, name: Optional[str] = "activity", schemaVersion: Optional[str] = None, - prefLabel: Optional[Union[str, Dict[str, str]]] = "activity", + prefLabel: Optional[str] = "activity", + altLabel: Optional[Dict[str, str]] = None, description: Optional[str] = "", preamble: Optional[str] = None, citation: Optional[str] = None, + image: Optional[Union[str, Dict[str, str]]] = None, suffix: Optional[str] = "_schema", visible: Optional[bool] = True, required: Optional[bool] = False, @@ -30,19 +33,9 @@ def __init__( lang: Optional[str] = DEFAULT_LANG(), ): - schema_order = [ - "@context", - "@type", - "@id", - "schemaVersion", - "version", - "prefLabel", - "description", - "preamble", + schema_order = COMMON_SCHEMA_ORDER() + [ "citation", - "image", "compute", - "ui", ] super().__init__( @@ -50,9 +43,11 @@ def __init__( schemaVersion=schemaVersion, at_type="reproschema:Activity", prefLabel={lang: prefLabel}, + altLabel=altLabel, description=description, preamble=preamble, citation=citation, + image=image, schema_order=schema_order, visible=visible, required=required, diff --git a/reproschema/models/base.py b/reproschema/models/base.py index faa3411..fea6e4b 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -25,6 +25,22 @@ def DEFAULT_VERSION() -> str: return "1.0.0-rc4" +def COMMON_SCHEMA_ORDER() -> list: + return [ + "@context", + "@type", + "@id", + "schemaVersion", + "version", + "prefLabel", + "altLabel", + "description", + "preamble", + "image", + "ui", + ] + + @define(kw_only=True) class SchemaBase: @@ -79,6 +95,7 @@ def _default_context(self) -> str: - order - addProperties - allow + - about """ # TODO @@ -86,21 +103,33 @@ def _default_context(self) -> str: """ Protocol, Activity, Field """ - # altLabel # associatedMedia - # about prefLabel: dict = field( factory=(dict), converter=default_if_none(default={}), # type: ignore validator=optional(instance_of(dict)), ) - # TODO description is language specific + altLabel: dict = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of(dict)), + ) + # TODO description is language specific? description: str = field( factory=(str), converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) + image: Optional[Union[str, Dict[str, str]]] = field( + default=None, + validator=optional(instance_of((str, dict))), + ) + preamble: dict = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of(dict)), + ) # Protocol only # TODO landing_page is a dict or a list of dict? landingPage: dict = field( @@ -130,19 +159,16 @@ def _default_context(self) -> str: """ # overrideProperties - #: Activity and Field - preamble: dict = field( - factory=(dict), - converter=default_if_none(default={}), # type: ignore - validator=optional(instance_of(dict)), - ) - """ Field only """ - # image # inputType # additionalNotesObj + inputType: str = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) readonlyValue: Optional[bool] = field( factory=(bool), validator=optional(instance_of(bool)), @@ -240,8 +266,10 @@ def update(self) -> None: "schemaVersion", "version", "prefLabel", + "altLabel", "description", "citation", + "image", "preamble", "landingPage", "compute", @@ -256,10 +284,6 @@ def update_ui(self) -> None: self.ui.update() self.schema["ui"] = self.ui.schema - # TODO - def set_image(self, image) -> None: - self.schema["image"] = image - """SETTERS These are "complex" setters to help set fields that are dictionaries, @@ -307,6 +331,16 @@ def set_filename(self, name: str = None) -> None: self.URI = os.path.join(self.output_dir, self.at_id) self.update() + def set_alt_label( + self, alt_label: Optional[str] = None, lang: Optional[str] = None + ) -> None: + if alt_label is None: + return + if lang is None: + lang = self.lang + self.alt_label[lang] = alt_label + self.update() + def set_pref_label( self, pref_label: Optional[str] = None, lang: Optional[str] = None ) -> None: @@ -359,16 +393,10 @@ def write(self, output_dir: Optional[Union[str, Path]] = None) -> None: self.sort() - attrib_to_remove_if_empty = [ - "ui", - "preamble", - "landingPage", - "compute", - "question", - ] - for attrib in attrib_to_remove_if_empty: - if attrib in self.schema and self.schema[attrib] in [{}, [], ""]: - self.schema.pop(attrib) + tmp = dict(self.schema) + for key in tmp: + if self.schema[key] in [{}, [], ""]: + self.schema.pop(key) if output_dir is None: output_dir = self.output_dir diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 68bf755..a878681 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -1,7 +1,9 @@ from pathlib import Path +from typing import Dict from typing import Optional from typing import Union +from .base import COMMON_SCHEMA_ORDER from .base import DEFAULT_LANG from .base import SchemaBase from .response_options import ResponseOption @@ -18,8 +20,10 @@ def __init__( input_type: Optional[str] = "text", question: Optional[Union[dict, str]] = "", schemaVersion: Optional[str] = None, - prefLabel="item", + prefLabel: Optional[str] = "item", + altLabel: Optional[Dict[str, str]] = None, description: Optional[str] = "", + image: Optional[Union[str, Dict[str, str]]] = None, preamble: Optional[str] = None, visible: Optional[bool] = True, required: Optional[bool] = False, @@ -31,27 +35,21 @@ def __init__( lang: Optional[str] = DEFAULT_LANG(), ): - schema_order = [ - "@context", - "@type", - "@id", - "schemaVersion", - "version", - "prefLabel", - "description", - "preamble", + schema_order = COMMON_SCHEMA_ORDER() + [ "question", "responseOptions", - "ui", ] super().__init__( at_id=name, at_type="reproschema:Field", + inputType=input_type, schemaVersion=schemaVersion, prefLabel={lang: prefLabel}, + altLabel=altLabel, description=description, preamble=preamble, + image=image, schema_order=schema_order, visible=visible, required=required, @@ -68,7 +66,6 @@ def __init__( self.response_options: ResponseOption = ResponseOption() self.set_response_options() - self.input_type = input_type self.set_input_type() self.update() @@ -113,87 +110,78 @@ def set_input_type(self) -> None: SUPPORTED_TYPES = ( "text", "multitext", - "int", + "integer", "float", "date", "time", - "time_range", - "language", - "country", - "state", + "timeRange", + "selectLanguage", + "selectCountry", + "selectState", "email", - "id", + "pid", ) - if not self.input_type or self.input_type in ["select", "radio", "slider"]: - return - - if self.input_type in SUPPORTED_TYPES: + if self.inputType in SUPPORTED_TYPES: self.response_options.unset( ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] ) - if self.input_type == "text": - self.ui.inputType = "text" + self.ui.inputType = self.inputType if self.inputType != "integer" else "number" + + if not self.inputType or self.inputType in ["select", "radio", "slider"]: + return + + if self.inputType == "text": self.response_options.set_type("string") self.response_options.set_length(300) - elif self.input_type == "multitext": - self.ui.inputType = "multitext" + elif self.inputType == "multitext": self.response_options.set_length(300) self.response_options.set_type("string") - elif self.input_type == "int": - self.set_input_type_numeric("number", "integer") + elif self.inputType == "integer": + self.response_options.set_type("integer") - elif self.input_type == "float": - self.set_input_type_numeric("float", "float") + elif self.inputType == "float": + self.response_options.set_type("float") - elif self.input_type == "year": - self.ui.inputType = "year" + elif self.inputType == "year": self.response_options.set_type("date") self.response_options.unset( ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] ) - elif self.input_type == "date": - self.ui.inputType = "date" + elif self.inputType == "date": self.response_options.set_type("date") - elif self.input_type == "time_range": - self.ui.inputType = "timeRange" + elif self.inputType == "timeRange": self.response_options.set_type("datetime") - elif self.input_type == "language": + elif self.inputType == "selectLanguage": self.set_input_type_as_language() - elif self.input_type == "country": + elif self.inputType == "selectCountry": self.set_input_type_as_country() - elif self.input_type == "state": + elif self.inputType == "selectState": self.set_input_type_as_state() - elif self.input_type == "email": - self.ui.inputType = "email" + elif self.inputType == "email": self.response_options.set_type("string") - elif self.input_type == "id": - self.ui.inputType = "pid" + elif self.inputType == "pid": self.response_options.set_type("string") else: raise ValueError( f""" - Input_type {self.input_type} not supported. + Input_type {self.inputType} not supported. Supported input_types are: {SUPPORTED_TYPES} """ ) - def set_input_type_numeric(self, arg0: str, arg1: str) -> None: - self.ui.inputType = arg0 - self.response_options.set_type(arg1) - """ input types with preset response choices """ @@ -201,7 +189,6 @@ def set_input_type_numeric(self, arg0: str, arg1: str) -> None: def set_input_type_as_language(self) -> None: URL = self.set_input_from_preset( "https://raw.githubusercontent.com/ReproNim/reproschema-library/", - "selectLanguage", ) self.response_options.set_multiple_choice(True) self.response_options.use_preset(f"{URL}master/resources/languages.json") @@ -209,7 +196,6 @@ def set_input_type_as_language(self) -> None: def set_input_type_as_country(self) -> None: URL = self.set_input_from_preset( "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json", - "selectCountry", ) self.response_options.use_preset(URL) self.response_options.set_length(50) @@ -217,13 +203,11 @@ def set_input_type_as_country(self) -> None: def set_input_type_as_state(self) -> None: URL = self.set_input_from_preset( "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json", - "selectState", ) self.response_options.use_preset(URL) - def set_input_from_preset(self, arg0, arg1: str) -> str: + def set_input_from_preset(self, arg0) -> str: result = arg0 - self.ui.inputType = arg1 self.response_options.set_type("string") return result diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index dd38d0f..e81e79f 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -4,6 +4,7 @@ from typing import Union from .activity import Activity +from .base import COMMON_SCHEMA_ORDER from .base import DEFAULT_LANG from .base import SchemaBase @@ -17,29 +18,23 @@ def __init__( self, name: Optional[str] = "protocol", schemaVersion: Optional[str] = None, - prefLabel: Optional[Union[str, Dict[str, str]]] = "protocol", + prefLabel: Optional[str] = "protocol", + altLabel: Optional[Dict[str, str]] = None, description: Optional[str] = "", + preamble: Optional[str] = None, landingPage: Optional[dict] = None, citation: Optional[str] = None, + image: Optional[Union[str, Dict[str, str]]] = None, suffix: Optional[str] = "_schema", ext: Optional[str] = ".jsonld", output_dir: Optional[Union[str, Path]] = Path.cwd(), lang: Optional[str] = DEFAULT_LANG(), ): - schema_order = [ - "@context", - "@type", - "@id", - "schemaVersion", - "version", - "prefLabel", - "description", + schema_order = COMMON_SCHEMA_ORDER() + [ "landingPage", "citation", - "image", "compute", - "ui", ] super().__init__( @@ -47,9 +42,12 @@ def __init__( at_type="reproschema:Protocol", schemaVersion=schemaVersion, prefLabel={lang: prefLabel}, + altLabel=altLabel, description=description, landingPage=landingPage, + preamble=preamble, citation=citation, + image=image, schema_order=schema_order, suffix=suffix, ext=ext, diff --git a/reproschema/models/tests/data/activities/activity1.jsonld b/reproschema/models/tests/data/activities/activity1.jsonld index 6a533ee..6f2100c 100644 --- a/reproschema/models/tests/data/activities/activity1.jsonld +++ b/reproschema/models/tests/data/activities/activity1.jsonld @@ -2,7 +2,7 @@ "@context": "../../contexts/generic", "@type": "reproschema:Activity", "@id": "activity1.jsonld", - "prefLabel": "Example 1", + "prefLabel": {"en": "Example 1"}, "description": "Activity example 1", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", diff --git a/reproschema/models/tests/data/items/item1.jsonld b/reproschema/models/tests/data/items/item1.jsonld new file mode 100644 index 0000000..9210612 --- /dev/null +++ b/reproschema/models/tests/data/items/item1.jsonld @@ -0,0 +1,72 @@ +{ + "@context": "../../../contexts/generic", + "@type": "reproschema:Field", + "@id": "item1.jsonld", + "prefLabel": "item1", + "description": "Q1 of example 1", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "audio": { + "@type": "AudioObject", + "contentUrl": "http://media.freesound.org/sample-file.mp4" + }, + "image": { + "@type": "ImageObject", + "contentUrl": "http://example.com/sample-image.jpg" + }, + "question": { + "en": "Little interest or pleasure in doing things", + "es": "Poco interés o placer en hacer cosas" + }, + "ui": { + "inputType": "radio" + }, + "responseOptions": { + "valueType": "xsd:integer", + "minValue": 0, + "maxValue": 3, + "multipleChoice": false, + "choices": [ + { + "name": { + "en": "Not at all", + "es": "Para nada" + }, + "value": 0 + }, + { + "name": { + "en": "Several days", + "es": "Varios días" + }, + "value": "a" + }, + { + "name": { + "en": "More than half the days", + "es": "Más de la mitad de los días" + }, + "value": {"@id": "http://example.com/choice3" } + }, + { + "name": { + "en": "Nearly everyday", + "es": "Casi todos los días" + }, + "value": {"@value": "choice-with-lang", "@language": "en"} + } + ] + }, + "additionalNotesObj": [ + { + "source": "redcap", + "column": "notes", + "value": "some extra note" + }, + { + "source": "redcap", + "column": "notes", + "value": {"@id": "http://example.com/iri-example"} + } + ] +} diff --git a/reproschema/models/tests/data/items/item2.jsonld b/reproschema/models/tests/data/items/item2.jsonld new file mode 100644 index 0000000..cf1b37a --- /dev/null +++ b/reproschema/models/tests/data/items/item2.jsonld @@ -0,0 +1,40 @@ +{ + "@context": "../../../contexts/generic", + "@type": "reproschema:Field", + "@id": "item2.jsonld", + "prefLabel": "item2", + "description": "Q2 of example 1", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "question": { + "en": "Current temperature.", + "es": "Fiebre actual." + }, + "video": { + "@type": "VideoObject", + "contentUrl": "http://media.freesound.org/data/0/previews/719__elmomo__12oclock_girona_preview.mp4" + }, + "imageUrl": "http://example.com/sample-image.jpg", + "ui": { + "inputType": "float" + }, + "responseOptions": { + "valueType": "xsd:float", + "unitOptions": [ + { + "prefLabel": { + "en": "Fahrenheit", + "es": "Fahrenheit" + }, + "value": "°F" + }, + { + "prefLabel": { + "en": "Celsius", + "es": "Celsius" + }, + "value": "°C" + } + ] + } +} diff --git a/reproschema/models/tests/data/protocols/protocol1_schema.jsonld b/reproschema/models/tests/data/protocols/protocol1_schema.jsonld index 4b1aa9d..2620c91 100644 --- a/reproschema/models/tests/data/protocols/protocol1_schema.jsonld +++ b/reproschema/models/tests/data/protocols/protocol1_schema.jsonld @@ -1,40 +1,43 @@ { - "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", - "@type": "reproschema:Protocol", - "@id": "protocol1_schema.jsonld", - "prefLabel": { - "en": "Protocol1" - }, - "description": "example Protocol", - "schemaVersion": "1.0.0-rc4", - "version": "0.0.1", - "landingPage": { - "@id": "http://example.com/sample-readme.md", - "inLanguage": "en" - }, - "ui": { - "addProperties": [ - { - "isAbout": "../activities/activity1_schema.jsonld", - "variableName": "activity1", - "prefLabel": { - "en": "Screening" - }, - "isVis": true, - "requiredValue": false, - "allow": [ - "reproschema:Skipped" - ] - } - ], - "order": [ - "../activities/activity1_schema.jsonld" - ], - "shuffle": false, - "allow": [ - "reproschema:AutoAdvance", - "reproschema:AllowExport", - "reproschema:DisableBack" - ] - } + "@context": "https://raw.githubusercontent.com/ReproNim/reproschema/1.0.0-rc4/contexts/generic", + "@type": "reproschema:Protocol", + "@id": "protocol1_schema.jsonld", + "schemaVersion": "1.0.0-rc4", + "version": "0.0.1", + "prefLabel": { + "en": "Protocol1" + }, + "description": "example Protocol", + "preamble": { + "en": "protocol1" + }, + "ui": { + "shuffle": false, + "order": [ + "../activities/activity1_schema.jsonld" + ], + "addProperties": [ + { + "variableName": "activity1", + "isAbout": "../activities/activity1_schema.jsonld", + "prefLabel": { + "en": "Screening" + }, + "isVis": true, + "requiredValue": false, + "allow": [ + "reproschema:Skipped" + ] + } + ], + "allow": [ + "reproschema:AutoAdvance", + "reproschema:AllowExport", + "reproschema:DisableBack" + ] + }, + "landingPage": { + "@id": "http://example.com/sample-readme.md", + "inLanguage": "en" + } } diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index 05dc195..4c13085 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -1,4 +1,5 @@ import os +from unittest import skip from utils import clean_up from utils import load_jsons @@ -38,9 +39,11 @@ def test_activity(): activity.set_preamble( "Over the last 2 weeks, how often have you been bothered by any of the following problems?" ) - activity.set_image( - {"@type": "AudioObject", "contentUrl": "http://example.com/sample-image.png"} - ) + activity.image = { + "@type": "AudioObject", + "contentUrl": "http://example.com/sample-image.png", + } + activity.set_compute("activity1_total_score", "item1 + item2") item_1 = Item(name="item1", skippable=False, required=True, output_dir="items") @@ -71,3 +74,62 @@ def test_activity(): assert activity_content == expected clean_up(activity_dir, activity) + + +def test_activity_1(): + + activity = Activity( + name="activity1", + prefLabel="Example 1", + description="Activity example 1", + citation="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", + output_dir=activity_dir, + suffix="", + ) + activity.set_preamble( + preamble="Over the last 2 weeks, how often have you been bothered by any of the following problems?" + ) + activity.set_preamble( + preamble="Durante las últimas 2 semanas, ¿con qué frecuencia le han molestado los siguintes problemas?", + lang="es", + ) + activity.image = { + "@type": "AudioObject", + "contentUrl": "http://example.com/sample-image.png", + } + activity.at_context = "../../contexts/generic" + activity.ui.AutoAdvance = False + activity.ui.AllowExport = False + activity.update() + + activity.set_compute(variable="activity1_total_score", expression="item1 + item2") + + item_1 = Item(name="item1", skippable=False, required=True, output_dir="items") + item_1.prefLabel = {} + activity.update() + + item_2 = Item(name="item2", skippable=True, required=True, output_dir="items") + item_2.set_filename("item2") + item_2.prefLabel = {} + activity.update() + + item_3 = Item( + name="activity1_total_score", + ext="", + skippable=False, + required=True, + visible=False, + output_dir="items", + ) + item_3.prefLabel = {} + activity.update() + + activity.append_item(item_1) + activity.append_item(item_2) + activity.append_item(item_3) + + activity.write() + activity_content, expected = load_jsons(activity_dir, activity) + assert activity_content == expected + + clean_up(activity_dir, activity) diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index a2794ff..c3f31d3 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -16,7 +16,7 @@ def test_default(): item = Item(name="default", output_dir=item_dir) - print(item.question) + print(item.schema_order) item.write() item_content, expected = load_jsons(item_dir, item) @@ -31,15 +31,15 @@ def test_default(): ("text", "question for text item", "text"), ("multitext", "item with several text field", "multitext"), ("email", "input email address", "email"), - ("participant id", "input the participant id number", "id"), + ("participant id", "input the participant id number", "pid"), ("date", "input a date", "date"), - ("time range", "input a time range", "time_range"), + ("time range", "input a time range", "timeRange"), ("year", "input a year", "year"), - ("language", "item to select several language", "language"), - ("country", "select a country", "country"), - ("state", "select a USA state", "state"), + ("language", "item to select several language", "selectLanguage"), + ("country", "select a country", "selectCountry"), + ("state", "select a USA state", "selectState"), ("float", "item to input a float", "float"), - ("integer", "item to input a integer", "int"), + ("integer", "item to input a integer", "integer"), ], ) def test_items(name, question, input_type): @@ -163,7 +163,7 @@ def test_read_only(): item = Item( name="activity1_total_score", ext="", - input_type="int", + input_type="integer", read_only=True, output_dir=item_dir, ) diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index 7966508..cc9920b 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -135,9 +135,11 @@ def append(self, obj=None, variableName: Optional[str] = None) -> None: obj_properties = { "variableName": variableName, "isAbout": obj.URI, - "prefLabel": obj.prefLabel, "isVis": obj.visible, } + if obj.prefLabel not in [None, "", {}]: + obj_properties["prefLabel"] = obj.prefLabel + if obj.required is not None: obj_properties["requiredValue"] = obj.required From 6772c2067c402c32108deb76557595b9ac5ecf64 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 12:06:45 +0200 Subject: [PATCH 52/69] create new response option class use new response option class rm old response option class --- reproschema/models/item.py | 41 +- reproschema/models/response_options.py | 387 +++++++++++++++--- reproschema/models/tests/test_item.py | 30 +- .../models/tests/test_response_options.py | 25 +- reproschema/models/utils.py | 31 +- 5 files changed, 402 insertions(+), 112 deletions(-) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index a878681..47b45e8 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -133,30 +133,32 @@ def set_input_type(self) -> None: return if self.inputType == "text": - self.response_options.set_type("string") - self.response_options.set_length(300) + self.response_options.set_valueType("string") + self.response_options.maxLength = 300 + self.response_options.update() elif self.inputType == "multitext": - self.response_options.set_length(300) - self.response_options.set_type("string") + self.response_options.set_valueType("string") + self.response_options.maxLength = 300 + self.response_options.update() elif self.inputType == "integer": - self.response_options.set_type("integer") + self.response_options.set_valueType("integer") elif self.inputType == "float": - self.response_options.set_type("float") + self.response_options.set_valueType("float") elif self.inputType == "year": - self.response_options.set_type("date") + self.response_options.set_valueType("date") self.response_options.unset( ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] ) elif self.inputType == "date": - self.response_options.set_type("date") + self.response_options.set_valueType("date") elif self.inputType == "timeRange": - self.response_options.set_type("datetime") + self.response_options.set_valueType("datetime") elif self.inputType == "selectLanguage": self.set_input_type_as_language() @@ -168,10 +170,10 @@ def set_input_type(self) -> None: self.set_input_type_as_state() elif self.inputType == "email": - self.response_options.set_type("string") + self.response_options.set_valueType("string") elif self.inputType == "pid": - self.response_options.set_type("string") + self.response_options.set_valueType("string") else: @@ -190,15 +192,15 @@ def set_input_type_as_language(self) -> None: URL = self.set_input_from_preset( "https://raw.githubusercontent.com/ReproNim/reproschema-library/", ) - self.response_options.set_multiple_choice(True) + self.response_options.multipleChoice = True self.response_options.use_preset(f"{URL}master/resources/languages.json") def set_input_type_as_country(self) -> None: URL = self.set_input_from_preset( "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json", ) + self.response_options.maxLength = 50 self.response_options.use_preset(URL) - self.response_options.set_length(50) def set_input_type_as_state(self) -> None: URL = self.set_input_from_preset( @@ -208,7 +210,7 @@ def set_input_type_as_state(self) -> None: def set_input_from_preset(self, arg0) -> str: result = arg0 - self.response_options.set_type("string") + self.response_options.set_valueType("string") return result @@ -226,12 +228,13 @@ def set_input_type_as_select(self, response_options: ResponseOption) -> None: self.set_input_type_rasesli("select", response_options) def set_input_type_as_slider(self, response_options: ResponseOption) -> None: - response_options.set_multiple_choice(False) + response_options.multipleChoice = False + response_options.update() self.set_input_type_rasesli("slider", response_options) def set_input_type_rasesli(self, arg0, response_options: ResponseOption) -> None: self.ui.inputType = arg0 - response_options.set_type("integer") + response_options.set_valueType("integer") self.response_options = response_options """ @@ -243,6 +246,12 @@ def set_response_options(self) -> None: Passes the content of the response options to the schema of the item. To be done before writing the item """ + self.response_options.update() + self.response_options.sort() + self.response_options.drop_empty_values_from_schema() + self.response_options.schema.pop("@id") + self.response_options.schema.pop("@type") + self.response_options.schema.pop("@context") self.schema["responseOptions"] = self.response_options.schema def unset(self, keys) -> None: diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index 438dcd6..e37ccd5 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -1,46 +1,243 @@ +import json +import os from pathlib import Path +from typing import Any +from typing import Dict +from typing import List from typing import Optional +from typing import Union + +from attrs import define +from attrs import field +from attrs.converters import default_if_none +from attrs.validators import in_ +from attrs.validators import instance_of +from attrs.validators import optional from .base import DEFAULT_LANG -from .base import SchemaBase +from .base import DEFAULT_VERSION +from .utils import reorder_dict_skip_missing + + +@define(kw_only=True) +class Choice: + + name: Optional[Union[str, Dict[str, str]]] = field( + default=None, + converter=default_if_none(default=""), + validator=optional(instance_of((str, dict))), + ) + value = field(default=None) + image: Optional[Union[str, Dict[str, str]]] = field( + default=None, validator=optional(instance_of((str, dict))) + ) + + lang: Optional[str] = field( + default=None, + converter=default_if_none(default=DEFAULT_LANG()), # type: ignore + validator=optional(instance_of(str)), + ) + schema_order: Optional[list] = field( + default=None, + converter=default_if_none( + default=[ + "name", + "value", + "image", + ] + ), # type: ignore + validator=optional(instance_of(list)), + ) + + #: contains the content that is read from or dumped in JSON + schema: Optional[dict] = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of(dict)), + ) + + def __attrs_post_init__(self) -> None: + + if isinstance(self.name, str): + self.name = {self.lang: self.name} + + self.update() + self.sort() + self.drop_empty_values_from_schema() + + def update(self) -> None: + """Updates the schema content based on the attributes.""" + + for key in self.schema_order: + self.schema[key] = self.__getattribute__(key) + + def sort(self) -> None: + + reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) + + self.schema = reordered_dict + + def drop_empty_values_from_schema(self) -> None: + tmp = dict(self.schema) + for key in tmp: + if self.schema[key] in [{}, [], ""]: + self.schema.pop(key) + self.schema = dict(self.schema) + + +@define(kw_only=True) +class ResponseOption: + + SUPPORTED_VALUE_TYPES = ( + "", + "xsd:string", + "xsd:integer", + "xsd:float", + "xsd:date", + "xsd:datetime", + "xsd:timeRange", + ) + + at_type: Optional[str] = field( + default=None, + converter=default_if_none(default="reproschema:ResponseOption"), # type: ignore + validator=in_( + [ + "reproschema:ResponseOption", + ] + ), + ) + at_id: Optional[str] = field( + default=None, + converter=default_if_none(default="valueConstraints"), # type: ignore + validator=[instance_of(str)], + ) + schemaVersion: Optional[str] = field( + default=DEFAULT_VERSION(), + converter=default_if_none(default=DEFAULT_VERSION()), # type: ignore + validator=[instance_of(str)], + ) + at_context: str = field( + validator=[instance_of(str)], + ) + + @at_context.default + def _default_context(self) -> str: + """ + For now we assume that the github repo will be where schema will be read from. + """ + URL = "https://raw.githubusercontent.com/ReproNim/reproschema/" + VERSION = self.schemaVersion or DEFAULT_VERSION() + return URL + VERSION + "/contexts/generic" + + valueType: str = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(in_(SUPPORTED_VALUE_TYPES)), + ) + + choices: Optional[Union[str, List[Choice]]] = field( + factory=(list), + converter=default_if_none(default=[]), # type: ignore + validator=optional(instance_of((str, list))), + ) + + multipleChoice: bool = field( + default=None, + validator=optional(instance_of(bool)), + ) + + minValue: int = field( + default=None, + validator=optional(instance_of(int)), + ) + + maxValue: int = field( + default=None, + validator=optional(instance_of(int)), + ) + + unitOptions: str = field( + default=None, + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + + unitCode: str = field( + default=None, + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + datumType: str = field( + default=None, + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + + maxLength: int = field( + default=None, + validator=optional(instance_of(int)), + ) -class ResponseOption(SchemaBase): """ - class to deal with reproschema response options + Non schema based attributes: OPTIONAL + + Those attributes help with file management + and with printing json files with standardized key orders """ + lang: Optional[str] = field( + default=None, + converter=default_if_none(default=DEFAULT_LANG()), # type: ignore + validator=optional(instance_of(str)), + ) + schema_order: Optional[list] = field( + default=None, + converter=default_if_none( + default=[ + "@type", + "@context", + "@id", + "valueType", + "choices", + "multipleChoice", + "minValue", + "maxValue", + "unitOptions", + "unitCode", + "datumType", + "maxLength", + ] + ), # type: ignore + validator=optional(instance_of(list)), + ) + output_dir = field( + default=None, + converter=default_if_none(default=Path.cwd()), # type: ignore + validator=optional(instance_of((str, Path))), + ) + suffix: Optional[str] = field( + default=None, + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + ext: Optional[str] = field( + default=None, + converter=default_if_none(default=".jsonld"), # type: ignore + validator=optional(instance_of(str)), + ) + URI: Optional[Path] = field( + default=None, + converter=default_if_none(default=Path("")), # type: ignore + validator=optional(instance_of((str, Path))), + ) - def __init__( - self, - name="valueConstraints", - multiple_choice=None, - schemaVersion=None, - output_dir=Path.cwd(), - ): - - options_order = [ - "@context", - "@type", - "@id", - "valueType", - "minValue", - "maxValue", - "multipleChoice", - "choices", - ] - - super().__init__( - at_id=name, - at_type="reproschema:ResponseOption", - schema_order=options_order, - schemaVersion=schemaVersion, - ext=".jsonld", - suffix="", - output_dir=output_dir, - ) - - self.schema = {"choices": []} - self.set_multiple_choice(multiple_choice) + #: contains the content that is read from or dumped in JSON + schema: Optional[dict] = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of(dict)), + ) def set_defaults(self) -> None: self.schema["@type"] = self.at_type @@ -52,47 +249,121 @@ def unset(self, keys) -> None: for i in keys: self.schema.pop(i, None) - def set_type(self, value: str = None) -> None: + def set_valueType(self, value: str = None) -> None: if value is not None: - self.schema["valueType"] = f"xsd:{value}" + self.valueType = f"xsd:{value}" + self.update() def values_all_options(self): - return [i["value"] for i in self.schema["choices"] if "value" in i] + return [i["value"] for i in self.choices if "value" in i] def set_min(self, value: int = None) -> None: if value is not None: - self.schema["minValue"] = value - elif len(self.schema["choices"]) > 1: - self.schema["minValue"] = min(self.values_all_options()) + self.minValue = value + elif len(self.choices) > 1: + self.minValue = min(self.values_all_options()) def set_max(self, value: int = None) -> None: if value is not None: - self.schema["maxValue"] = value - elif len(self.schema["choices"]) > 1: - self.schema["maxValue"] = max(self.values_all_options()) - - def set_length(self, value: int = None) -> None: - if value is not None: - self.schema["maxLength"] = value - - # TODO - - def set_multiple_choice(self, value: bool = None) -> None: - if value is not None: - self.multiple_choice = value - self.schema["multipleChoice"] = value + self.maxValue = value + elif len(self.choices) > 1: + self.maxValue = max(self.values_all_options()) + self.update() def use_preset(self, URI) -> None: """ In case the list response options are read from another file like for languages, country, state... """ - self.schema["choices"] = URI + self.choices = URI + self.update() - def add_choice(self, choice, value, lang: Optional[str] = None) -> None: + def add_choice( + self, + name: Optional[str] = None, + value: Any = None, + lang: Optional[str] = None, + ) -> None: if lang is None: - lang = DEFAULT_LANG() + lang = self.lang # TODO replace existing choice if already set - self.schema["choices"].append({"name": {lang: choice}, "value": value}) + self.choices.append(Choice(name=name, value=value, lang=lang).schema) self.set_max() self.set_min() + + def update(self) -> None: + """Updates the schema content based on the attributes.""" + + self.schema["@id"] = self.at_id + self.schema["@type"] = self.at_type + self.schema["@context"] = self.at_context + + for key in self.schema_order: + if key.startswith("@"): + continue + self.schema[key] = self.__getattribute__(key) + + def set_filename(self, name: str = None) -> None: + if name is None: + name = self.at_id + if name.endswith(self.ext): + name = name.replace(self.ext, "") + if name.endswith(self.suffix): + name = name.replace(self.suffix, "") + + name = name.replace(" ", "_") + + self.at_id = f"{name}{self.suffix}{self.ext}" + self.URI = os.path.join(self.output_dir, self.at_id) + self.update() + + def get_basename(self) -> str: + return Path(self.at_id).stem + + def sort(self) -> None: + self.sort_schema() + + def sort_schema(self) -> None: + reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) + self.schema = reordered_dict + + def drop_empty_values_from_schema(self) -> None: + tmp = dict(self.schema) + for key in tmp: + if self.schema[key] in [{}, [], "", None]: + self.schema.pop(key) + + def write(self, output_dir: Optional[Union[str, Path]] = None) -> None: + + self.update() + self.sort() + self.drop_empty_values_from_schema() + + if output_dir is None: + output_dir = self.output_dir + + if isinstance(output_dir, str): + output_dir = Path(output_dir) + + output_dir.mkdir(parents=True, exist_ok=True) + + with open(output_dir.joinpath(self.at_id), "w") as ff: + json.dump(self.schema, ff, sort_keys=False, indent=4) + + @classmethod + def from_data(cls, data: dict): + klass = cls() + if klass.at_type is None: + raise ValueError("SchemaBase cannot be used to instantiate class") + if klass.at_type != data["@type"]: + raise ValueError(f"Mismatch in type {data['@type']} != {klass.at_type}") + klass.schema = data + return klass + + @classmethod + def from_file(cls, filepath: Union[str, Path]): + with open(filepath) as fp: + data = json.load(fp) + if "@type" not in data: + raise ValueError("Missing @type key") + return cls.from_data(data) diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index c3f31d3..ce94fa7 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -70,9 +70,9 @@ def test_items(name, question, input_type): def test_radio(): - response_options = ResponseOption(multiple_choice=False) - response_options.add_choice("Not at all", 0, "en") - response_options.add_choice("Several days", 1, "en") + response_options = ResponseOption(multipleChoice=False) + response_options.add_choice(name="Not at all", value=0) + response_options.add_choice(name="Several days", value=1) item = Item(name="radio", question="question for radio item", output_dir=item_dir) item.set_input_type_as_radio(response_options) @@ -88,7 +88,8 @@ def test_radio(): item.update() item.set_pref_label("radio multiple") item.set_question("question for radio item with multiple responses") - response_options.set_multiple_choice(True) + response_options.multipleChoice = True + # response_options.update() item.set_input_type_as_radio(response_options) item.write() @@ -100,10 +101,10 @@ def test_radio(): def test_select(): - response_options = ResponseOption(multiple_choice=False) - response_options.add_choice("Response option 1", 0) - response_options.add_choice("Response option 2", 1) - response_options.add_choice("Response option 3", 2) + response_options = ResponseOption(multipleChoice=False) + response_options.add_choice(name="Response option 1", value=0) + response_options.add_choice(name="Response option 2", value=1) + response_options.add_choice(name="Response option 3", value=2) item = Item(name="select", question="question for select item", output_dir=item_dir) item.set_input_type_as_select(response_options) @@ -119,7 +120,8 @@ def test_select(): item.update() item.set_pref_label("select multiple") item.set_question("question for select item with multiple responses") - response_options.set_multiple_choice(True) + response_options.multipleChoice = True + # response_options.update() item.set_input_type_as_select(response_options) item.write() @@ -132,11 +134,11 @@ def test_select(): def test_slider(): response_options = ResponseOption() - response_options.add_choice("not at all", 0) - response_options.add_choice("a bit", 1) - response_options.add_choice("so so", 2) - response_options.add_choice("a lot", 3) - response_options.add_choice("very much", 4) + response_options.add_choice(name="not at all", value=0) + response_options.add_choice(name="a bit", value=1) + response_options.add_choice(name="so so", value=2) + response_options.add_choice(name="a lot", value=3) + response_options.add_choice(name="very much", value=4) item = Item(name="slider", question="question for slider item", output_dir=item_dir) item.set_input_type_as_slider(response_options) diff --git a/reproschema/models/tests/test_response_options.py b/reproschema/models/tests/test_response_options.py index 6b94c63..b99611a 100644 --- a/reproschema/models/tests/test_response_options.py +++ b/reproschema/models/tests/test_response_options.py @@ -2,22 +2,33 @@ from utils import load_jsons from utils import output_dir -from reproschema.models.item import ResponseOption +from reproschema.models.response_options import Choice +from reproschema.models.response_options import ResponseOption + response_options_dir = output_dir("response_options") +def test_choice(): + choice = Choice(name="Not at all", value=1) + assert choice.schema == { + "name": {"en": "Not at all"}, + "value": 1, + } + + def test_example(): response_options = ResponseOption(output_dir=response_options_dir) - response_options.set_defaults() response_options.set_filename("example") - response_options.set_type("integer") - response_options.unset("multipleChoice") - response_options.add_choice("Not at all", 0) + response_options.set_valueType("integer") + + response_options.add_choice(name="Not at all", value=0) for i in range(1, 5): - response_options.add_choice("", i) - response_options.add_choice("Completely", 6) + response_options.add_choice(value=i) + response_options.add_choice(name="Completely", value=6) + + print(response_options.schema) response_options.write() content, expected = load_jsons(response_options_dir, response_options) diff --git a/reproschema/models/utils.py b/reproschema/models/utils.py index 5287870..b5a892a 100644 --- a/reproschema/models/utils.py +++ b/reproschema/models/utils.py @@ -1,19 +1,16 @@ -import json +from collections import OrderedDict +from typing import Dict +from typing import List -from . import Activity -from . import Item -from . import Protocol - -def load_schema(filepath): - with open(filepath) as fp: - data = json.load(fp) - if "@type" not in data: - raise ValueError("Missing @type key") - schema_type = data["@type"] - if schema_type == "reproschema:Protocol": - return Protocol.from_data(data) - if schema_type == "reproschema:Activity": - return Activity.from_data(data) - if schema_type == "reproschema:Item": - return Item.from_data(data) +def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> OrderedDict: + """ + reorders dictionary according to ``key_list`` + removing any key with no associated value + or that is not in the key list + """ + return OrderedDict( + (k, old_dict[k]) + for k in key_list + if (k in old_dict and old_dict[k] not in ["", [], None]) + ) From ccb0df89ac90679c593267a77fe23b66c2472401 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 12:31:03 +0200 Subject: [PATCH 53/69] refactor refactor --- reproschema/models/item.py | 108 +++++++++---------------- reproschema/models/response_options.py | 14 ---- reproschema/models/tests/test_item.py | 2 - 3 files changed, 36 insertions(+), 88 deletions(-) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 47b45e8..770f3b7 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -113,106 +113,69 @@ def set_input_type(self) -> None: "integer", "float", "date", - "time", + "year", "timeRange", "selectLanguage", "selectCountry", "selectState", "email", "pid", + "select", + "radio", + "slider", ) - if self.inputType in SUPPORTED_TYPES: - self.response_options.unset( - ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] + if self.inputType not in SUPPORTED_TYPES: + raise ValueError( + f""" + Input_type {self.inputType} not supported. + Supported input_types are: {SUPPORTED_TYPES} + """ ) self.ui.inputType = self.inputType if self.inputType != "integer" else "number" + if not self.inputType or self.inputType in [ + "text", + "multitext", + "selectLanguage", + "email", + "pid", + "selectLanguage", + "selectCountry", + "selectState", + ]: + self.response_options.set_valueType("string") + if not self.inputType or self.inputType in ["select", "radio", "slider"]: return - if self.inputType == "text": - self.response_options.set_valueType("string") - self.response_options.maxLength = 300 - self.response_options.update() - - elif self.inputType == "multitext": - self.response_options.set_valueType("string") + if self.inputType in ["text", "multitext"]: self.response_options.maxLength = 300 self.response_options.update() - elif self.inputType == "integer": - self.response_options.set_valueType("integer") - - elif self.inputType == "float": - self.response_options.set_valueType("float") + elif self.inputType in ["integer", "float", "date"]: + self.response_options.set_valueType(self.inputType) elif self.inputType == "year": self.response_options.set_valueType("date") - self.response_options.unset( - ["maxLength", "choices", "maxValue", "multipleChoice", "minValue"] - ) - - elif self.inputType == "date": - self.response_options.set_valueType("date") elif self.inputType == "timeRange": self.response_options.set_valueType("datetime") elif self.inputType == "selectLanguage": - self.set_input_type_as_language() + URL = "https://raw.githubusercontent.com/ReproNim/reproschema-library/" + self.response_options.multipleChoice = True + self.response_options.choices = f"{URL}master/resources/languages.json" elif self.inputType == "selectCountry": - self.set_input_type_as_country() + URL = "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json" + self.response_options.maxLength = 50 + self.response_options.choices = URL elif self.inputType == "selectState": - self.set_input_type_as_state() - - elif self.inputType == "email": - self.response_options.set_valueType("string") - - elif self.inputType == "pid": - self.response_options.set_valueType("string") - - else: - - raise ValueError( - f""" - Input_type {self.inputType} not supported. - Supported input_types are: {SUPPORTED_TYPES} - """ - ) - - """ - input types with preset response choices - """ - - def set_input_type_as_language(self) -> None: - URL = self.set_input_from_preset( - "https://raw.githubusercontent.com/ReproNim/reproschema-library/", - ) - self.response_options.multipleChoice = True - self.response_options.use_preset(f"{URL}master/resources/languages.json") - - def set_input_type_as_country(self) -> None: - URL = self.set_input_from_preset( - "https://raw.githubusercontent.com/samayo/country-json/master/src/country-by-name.json", - ) - self.response_options.maxLength = 50 - self.response_options.use_preset(URL) - - def set_input_type_as_state(self) -> None: - URL = self.set_input_from_preset( - "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json", - ) - self.response_options.use_preset(URL) - - def set_input_from_preset(self, arg0) -> str: - result = arg0 - self.response_options.set_valueType("string") - - return result + URL = "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json" + self.response_options.choices = URL """ input types with 'different response choices' @@ -229,10 +192,11 @@ def set_input_type_as_select(self, response_options: ResponseOption) -> None: def set_input_type_as_slider(self, response_options: ResponseOption) -> None: response_options.multipleChoice = False - response_options.update() self.set_input_type_rasesli("slider", response_options) - def set_input_type_rasesli(self, arg0, response_options: ResponseOption) -> None: + def set_input_type_rasesli( + self, arg0: str, response_options: ResponseOption + ) -> None: self.ui.inputType = arg0 response_options.set_valueType("integer") self.response_options = response_options diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index e37ccd5..83a3d79 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -243,12 +243,6 @@ def set_defaults(self) -> None: self.schema["@type"] = self.at_type self.set_filename() - def unset(self, keys) -> None: - if type(keys) == str: - keys = [keys] - for i in keys: - self.schema.pop(i, None) - def set_valueType(self, value: str = None) -> None: if value is not None: self.valueType = f"xsd:{value}" @@ -270,14 +264,6 @@ def set_max(self, value: int = None) -> None: self.maxValue = max(self.values_all_options()) self.update() - def use_preset(self, URI) -> None: - """ - In case the list response options are read from another file - like for languages, country, state... - """ - self.choices = URI - self.update() - def add_choice( self, name: Optional[str] = None, diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index ce94fa7..ab9ddf1 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -89,7 +89,6 @@ def test_radio(): item.set_pref_label("radio multiple") item.set_question("question for radio item with multiple responses") response_options.multipleChoice = True - # response_options.update() item.set_input_type_as_radio(response_options) item.write() @@ -121,7 +120,6 @@ def test_select(): item.set_pref_label("select multiple") item.set_question("question for select item with multiple responses") response_options.multipleChoice = True - # response_options.update() item.set_input_type_as_select(response_options) item.write() From dc2d69faa281aa0581603d77bb6693de03699aa9 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 12:50:29 +0200 Subject: [PATCH 54/69] refactor --- reproschema/models/base.py | 17 ++--------------- reproschema/models/item.py | 2 +- reproschema/models/ui.py | 19 +++---------------- 3 files changed, 6 insertions(+), 32 deletions(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index fea6e4b..f4cbce1 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -15,6 +15,7 @@ from attrs.validators import optional from .ui import UI +from .utils import reorder_dict_skip_missing def DEFAULT_LANG() -> str: @@ -162,7 +163,6 @@ def _default_context(self) -> str: """ Field only """ - # inputType # additionalNotesObj inputType: str = field( factory=(str), @@ -386,7 +386,7 @@ def sort(self) -> None: self.update_ui() def sort_schema(self) -> None: - reordered_dict = self.reorder_dict_skip_missing(self.schema, self.schema_order) + reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) self.schema = reordered_dict def write(self, output_dir: Optional[Union[str, Path]] = None) -> None: @@ -426,16 +426,3 @@ def from_file(cls, filepath: Union[str, Path]): if "@type" not in data: raise ValueError("Missing @type key") return cls.from_data(data) - - @staticmethod - def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> OrderedDict: - """ - reorders dictionary according to ``key_list`` - removing any key with no associated value - or that is not in the key list - """ - return OrderedDict( - (k, old_dict[k]) - for k in key_list - if (k in old_dict and old_dict[k] not in ["", [], None]) - ) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 770f3b7..5f7e64a 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -208,7 +208,7 @@ def set_input_type_rasesli( def set_response_options(self) -> None: """ Passes the content of the response options to the schema of the item. - To be done before writing the item + Also removes some "unnecessary" fields. """ self.response_options.update() self.response_options.sort() diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index cc9920b..d5d3194 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -11,6 +11,8 @@ from attrs.validators import instance_of from attrs.validators import optional +from .utils import reorder_dict_skip_missing + @define( kw_only=True, @@ -179,21 +181,6 @@ def update(self) -> None: def sort(self) -> Dict[str, Any]: if self.schema is None: return - reordered_dict = self.reorder_dict_skip_missing(self.schema, self.schema_order) + reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) self.schema = reordered_dict return reordered_dict - - @staticmethod - def reorder_dict_skip_missing( - old_dict: Dict[Any, Any], key_list: List[Any] - ) -> Dict: - """ - reorders dictionary according to ``key_list`` - removing any key with no associated value - or that is not in the key list - """ - return OrderedDict( - (k, old_dict[k]) - for k in key_list - if (k in old_dict and old_dict[k] not in [[], "", None]) - ) From cab0f749431ab9f87059338bfade883f970c015c Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 13:37:01 +0200 Subject: [PATCH 55/69] add additional property class --- reproschema/models/base.py | 3 +- reproschema/models/response_options.py | 16 +-- reproschema/models/ui.py | 139 ++++++++++++++++++++++--- 3 files changed, 133 insertions(+), 25 deletions(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index f4cbce1..2431e80 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -1,6 +1,5 @@ import json import os -from collections import OrderedDict from pathlib import Path from typing import Dict from typing import List @@ -221,7 +220,7 @@ def _default_ui(self) -> UI: converter=default_if_none(default=[]), # type: ignore validator=optional(instance_of(list)), ) - output_dir = field( + output_dir: Optional[Union[str, Path]] = field( default=None, converter=default_if_none(default=Path.cwd()), # type: ignore validator=optional(instance_of((str, Path))), diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index 83a3d79..c1268ed 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -142,40 +142,40 @@ def _default_context(self) -> str: validator=optional(instance_of((str, list))), ) - multipleChoice: bool = field( + multipleChoice: Optional[bool] = field( default=None, validator=optional(instance_of(bool)), ) - minValue: int = field( + minValue: Optional[int] = field( default=None, validator=optional(instance_of(int)), ) - maxValue: int = field( + maxValue: Optional[int] = field( default=None, validator=optional(instance_of(int)), ) - unitOptions: str = field( + unitOptions: Optional[int] = field( default=None, converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) - unitCode: str = field( + unitCode: Optional[str] = field( default=None, converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) - datumType: str = field( + datumType: Optional[str] = field( default=None, converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) - maxLength: int = field( + maxLength: Optional[int] = field( default=None, validator=optional(instance_of(int)), ) @@ -211,7 +211,7 @@ def _default_context(self) -> str: ), # type: ignore validator=optional(instance_of(list)), ) - output_dir = field( + output_dir: Optional[Union[str, Path]] = field( default=None, converter=default_if_none(default=Path.cwd()), # type: ignore validator=optional(instance_of((str, Path))), diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index d5d3194..411677c 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -1,8 +1,9 @@ -from collections import OrderedDict +import re from typing import Any from typing import Dict from typing import List from typing import Optional +from typing import Union from attrs import define from attrs import field @@ -13,12 +14,16 @@ from .utils import reorder_dict_skip_missing +# from .protocol import Protocol +# from .activity import Activity + @define( kw_only=True, ) class UI: + #: this is more to help set up things on the UI side than purely schema related SUPPORTED_INPUT_TYPES = ( "text", "multitext", @@ -133,23 +138,22 @@ def __attrs_post_init__(self) -> None: self.update() - def append(self, obj=None, variableName: Optional[str] = None) -> None: - obj_properties = { - "variableName": variableName, - "isAbout": obj.URI, - "isVis": obj.visible, - } - if obj.prefLabel not in [None, "", {}]: - obj_properties["prefLabel"] = obj.prefLabel - - if obj.required is not None: - obj_properties["requiredValue"] = obj.required + def append(self, obj, variableName: Optional[str] = None) -> None: - if obj.skippable is True: - obj_properties["allow"] = ["reproschema:Skipped"] + this_property = AddtionalPropertity( + variableName=variableName, + isAbout=obj.URI, + prefLabel=obj.prefLabel, + isVis=obj.visible, + requiredValue=obj.required, + skippable=obj.skippable, + ) + this_property.update() + this_property.sort() + this_property.drop_empty_values_from_schema() self.order.append(obj.URI) - self.addProperties.append(obj_properties) + self.addProperties.append(this_property.schema) self.update() def update(self) -> None: @@ -184,3 +188,108 @@ def sort(self) -> Dict[str, Any]: reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) self.schema = reordered_dict return reordered_dict + + +@define( + kw_only=True, +) +class AddtionalPropertity: + + variableName: Optional[str] = field( + default=None, + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + isAbout: Optional[str] = field( + default=None, + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + prefLabel: Optional[Union[str, Dict[str, str]]] = field( + default=None, + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of((str, dict))), + ) + isVis: Optional[bool] = field( + default=None, + validator=optional(instance_of(bool)), + ) + requiredValue: Optional[bool] = field( + default=None, + validator=optional(instance_of(bool)), + ) + allow: Optional[List[str]] = field( + factory=list, + converter=default_if_none(default=[]), # type: ignore + validator=optional(instance_of(list)), + ) + limit: Optional[str] = field( + default=None, + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + maxRetakes: Optional[str] = field( + default=None, + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + randomMaxDelay: Optional[str] = field( + default=None, + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + schedule: Optional[str] = field( + default=None, + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + + schema_order: Optional[List[str]] = field( + default=None, + converter=default_if_none( + default=[ + "variableName", + "isAbout", + "prefLabel", + "isVis", + "requiredValue", + "allow", + "limit", + "maxRetakes", + "randomMaxDelay", + "schedule", + ] + ), # type: ignore + validator=optional(instance_of(list)), + ) + + skippable: Optional[bool] = field( + factory=(bool), + converter=default_if_none(default=True), # type: ignore + validator=optional(instance_of(bool)), + ) + + schema: Optional[Dict[str, Any]] = field( + factory=(dict), + validator=optional(instance_of(dict)), + ) + + def update(self) -> None: + """Updates the schema content based on the attributes.""" + if self.skippable is True: + self.allow = ["reproschema:Skipped"] + for key in self.schema_order: + self.schema[key] = self.__getattribute__(key) + + def drop_empty_values_from_schema(self) -> None: + tmp = dict(self.schema) + for key in tmp: + if self.schema[key] in [{}, [], "", None]: + self.schema.pop(key) + + def sort(self) -> Dict[str, Any]: + if self.schema is None: + return + reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) + self.schema = reordered_dict + return reordered_dict From a9838f0c80b82eb39274dc70ab9b131b9c42b8b5 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 15:53:50 +0200 Subject: [PATCH 56/69] refactor refactor refactor refactor refactor refactor refactor refactor refactor refactor --- reproschema/models/base.py | 24 +----- reproschema/models/item.py | 36 ++------ reproschema/models/response_options.py | 110 +++++++------------------ reproschema/models/tests/test_item.py | 31 +++++-- reproschema/models/ui.py | 100 +++++----------------- reproschema/models/utils.py | 44 ++++++++++ 6 files changed, 130 insertions(+), 215 deletions(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 2431e80..0690a72 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -15,6 +15,7 @@ from .ui import UI from .utils import reorder_dict_skip_missing +from .utils import SchemaUtils def DEFAULT_LANG() -> str: @@ -42,7 +43,7 @@ def COMMON_SCHEMA_ORDER() -> list: @define(kw_only=True) -class SchemaBase: +class SchemaBase(SchemaUtils): """ Schema based attributes: REQUIRED @@ -215,11 +216,6 @@ def _default_ui(self) -> UI: converter=default_if_none(default=DEFAULT_LANG()), # type: ignore validator=optional(instance_of(str)), ) - schema_order: Optional[list] = field( - factory=(list), - converter=default_if_none(default=[]), # type: ignore - validator=optional(instance_of(list)), - ) output_dir: Optional[Union[str, Path]] = field( default=None, converter=default_if_none(default=Path.cwd()), # type: ignore @@ -241,13 +237,6 @@ def _default_ui(self) -> UI: validator=optional(instance_of((str, Path))), ) - #: contains the content that is read from or dumped in JSON - schema: Optional[dict] = field( - factory=(dict), - converter=default_if_none(default={}), # type: ignore - validator=optional(instance_of(dict)), - ) - def __attrs_post_init__(self) -> None: if self.description == "": @@ -384,18 +373,11 @@ def sort(self) -> None: self.sort_schema() self.update_ui() - def sort_schema(self) -> None: - reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) - self.schema = reordered_dict - def write(self, output_dir: Optional[Union[str, Path]] = None) -> None: self.sort() - tmp = dict(self.schema) - for key in tmp: - if self.schema[key] in [{}, [], ""]: - self.schema.pop(key) + self.drop_empty_values_from_schema() if output_dir is None: output_dir = self.output_dir diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 5f7e64a..6efd07c 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -105,7 +105,7 @@ def set_question( # static: Static/Static.vue # StaticReadOnly: Static/Static.vue - def set_input_type(self) -> None: + def set_input_type(self, response_options: ResponseOption = None) -> None: SUPPORTED_TYPES = ( "text", @@ -147,9 +147,6 @@ def set_input_type(self) -> None: ]: self.response_options.set_valueType("string") - if not self.inputType or self.inputType in ["select", "radio", "slider"]: - return - if self.inputType in ["text", "multitext"]: self.response_options.maxLength = 300 self.response_options.update() @@ -177,29 +174,12 @@ def set_input_type(self) -> None: URL = "https://gist.githubusercontent.com/mshafrir/2646763/raw/8b0dbb93521f5d6889502305335104218454c2bf/states_hash.json" self.response_options.choices = URL - """ - input types with 'different response choices' - - Those methods require an instance of ResponseOptions as input and - it will replace the one initialized in the construction. - """ - - def set_input_type_as_radio(self, response_options: ResponseOption) -> None: - self.set_input_type_rasesli("radio", response_options) - - def set_input_type_as_select(self, response_options: ResponseOption) -> None: - self.set_input_type_rasesli("select", response_options) - - def set_input_type_as_slider(self, response_options: ResponseOption) -> None: - response_options.multipleChoice = False - self.set_input_type_rasesli("slider", response_options) - - def set_input_type_rasesli( - self, arg0: str, response_options: ResponseOption - ) -> None: - self.ui.inputType = arg0 - response_options.set_valueType("integer") - self.response_options = response_options + elif self.inputType in ["radio", "select", "slider"]: + if response_options is not None: + if self.inputType in ["slider"]: + response_options.multipleChoice = False + self.response_options = response_options + self.response_options.set_valueType("integer") """ writing, reading, sorting, unsetting @@ -211,7 +191,7 @@ def set_response_options(self) -> None: Also removes some "unnecessary" fields. """ self.response_options.update() - self.response_options.sort() + self.response_options.sort_schema() self.response_options.drop_empty_values_from_schema() self.response_options.schema.pop("@id") self.response_options.schema.pop("@type") diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index c1268ed..7cbb3c9 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -17,10 +17,11 @@ from .base import DEFAULT_LANG from .base import DEFAULT_VERSION from .utils import reorder_dict_skip_missing +from .utils import SchemaUtils @define(kw_only=True) -class Choice: +class Choice(SchemaUtils): name: Optional[Union[str, Dict[str, str]]] = field( default=None, @@ -37,56 +38,29 @@ class Choice: converter=default_if_none(default=DEFAULT_LANG()), # type: ignore validator=optional(instance_of(str)), ) - schema_order: Optional[list] = field( - default=None, - converter=default_if_none( - default=[ + + def __attrs_post_init__(self) -> None: + + if self.schema_order in [None, []]: + self.schema_order = [ "name", "value", "image", ] - ), # type: ignore - validator=optional(instance_of(list)), - ) - - #: contains the content that is read from or dumped in JSON - schema: Optional[dict] = field( - factory=(dict), - converter=default_if_none(default={}), # type: ignore - validator=optional(instance_of(dict)), - ) - - def __attrs_post_init__(self) -> None: if isinstance(self.name, str): self.name = {self.lang: self.name} self.update() - self.sort() + self.sort_schema() self.drop_empty_values_from_schema() def update(self) -> None: - """Updates the schema content based on the attributes.""" - - for key in self.schema_order: - self.schema[key] = self.__getattribute__(key) - - def sort(self) -> None: - - reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) - - self.schema = reordered_dict - - def drop_empty_values_from_schema(self) -> None: - tmp = dict(self.schema) - for key in tmp: - if self.schema[key] in [{}, [], ""]: - self.schema.pop(key) - self.schema = dict(self.schema) + super().update() @define(kw_only=True) -class ResponseOption: +class ResponseOption(SchemaUtils): SUPPORTED_VALUE_TYPES = ( "", @@ -191,26 +165,7 @@ def _default_context(self) -> str: converter=default_if_none(default=DEFAULT_LANG()), # type: ignore validator=optional(instance_of(str)), ) - schema_order: Optional[list] = field( - default=None, - converter=default_if_none( - default=[ - "@type", - "@context", - "@id", - "valueType", - "choices", - "multipleChoice", - "minValue", - "maxValue", - "unitOptions", - "unitCode", - "datumType", - "maxLength", - ] - ), # type: ignore - validator=optional(instance_of(list)), - ) + output_dir: Optional[Union[str, Path]] = field( default=None, converter=default_if_none(default=Path.cwd()), # type: ignore @@ -232,12 +187,23 @@ def _default_context(self) -> str: validator=optional(instance_of((str, Path))), ) - #: contains the content that is read from or dumped in JSON - schema: Optional[dict] = field( - factory=(dict), - converter=default_if_none(default={}), # type: ignore - validator=optional(instance_of(dict)), - ) + def __attrs_post_init__(self) -> None: + + if self.schema_order in [None, []]: + self.schema_order = [ + "@type", + "@context", + "@id", + "valueType", + "choices", + "multipleChoice", + "minValue", + "maxValue", + "unitOptions", + "unitCode", + "datumType", + "maxLength", + ] def set_defaults(self) -> None: self.schema["@type"] = self.at_type @@ -284,10 +250,7 @@ def update(self) -> None: self.schema["@type"] = self.at_type self.schema["@context"] = self.at_context - for key in self.schema_order: - if key.startswith("@"): - continue - self.schema[key] = self.__getattribute__(key) + super().update() def set_filename(self, name: str = None) -> None: if name is None: @@ -306,23 +269,10 @@ def set_filename(self, name: str = None) -> None: def get_basename(self) -> str: return Path(self.at_id).stem - def sort(self) -> None: - self.sort_schema() - - def sort_schema(self) -> None: - reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) - self.schema = reordered_dict - - def drop_empty_values_from_schema(self) -> None: - tmp = dict(self.schema) - for key in tmp: - if self.schema[key] in [{}, [], "", None]: - self.schema.pop(key) - def write(self, output_dir: Optional[Union[str, Path]] = None) -> None: self.update() - self.sort() + self.sort_schema() self.drop_empty_values_from_schema() if output_dir is None: diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index ab9ddf1..53369a7 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -74,8 +74,13 @@ def test_radio(): response_options.add_choice(name="Not at all", value=0) response_options.add_choice(name="Several days", value=1) - item = Item(name="radio", question="question for radio item", output_dir=item_dir) - item.set_input_type_as_radio(response_options) + item = Item( + name="radio", + input_type="radio", + question="question for radio item", + output_dir=item_dir, + ) + item.set_input_type(response_options=response_options) item.write() item_content, expected = load_jsons(item_dir, item) @@ -89,7 +94,7 @@ def test_radio(): item.set_pref_label("radio multiple") item.set_question("question for radio item with multiple responses") response_options.multipleChoice = True - item.set_input_type_as_radio(response_options) + item.set_input_type(response_options=response_options) item.write() item_content, expected = load_jsons(item_dir, item) @@ -105,8 +110,13 @@ def test_select(): response_options.add_choice(name="Response option 2", value=1) response_options.add_choice(name="Response option 3", value=2) - item = Item(name="select", question="question for select item", output_dir=item_dir) - item.set_input_type_as_select(response_options) + item = Item( + name="select", + input_type="select", + question="question for select item", + output_dir=item_dir, + ) + item.set_input_type(response_options=response_options) item.write() item_content, expected = load_jsons(item_dir, item) @@ -120,7 +130,7 @@ def test_select(): item.set_pref_label("select multiple") item.set_question("question for select item with multiple responses") response_options.multipleChoice = True - item.set_input_type_as_select(response_options) + item.set_input_type(response_options=response_options) item.write() item_content, expected = load_jsons(item_dir, item) @@ -138,8 +148,13 @@ def test_slider(): response_options.add_choice(name="a lot", value=3) response_options.add_choice(name="very much", value=4) - item = Item(name="slider", question="question for slider item", output_dir=item_dir) - item.set_input_type_as_slider(response_options) + item = Item( + name="slider", + input_type="slider", + question="question for slider item", + output_dir=item_dir, + ) + item.set_input_type(response_options) item.write() item_content, expected = load_jsons(item_dir, item) diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index 411677c..0590910 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -1,4 +1,3 @@ -import re from typing import Any from typing import Dict from typing import List @@ -12,16 +11,13 @@ from attrs.validators import instance_of from attrs.validators import optional -from .utils import reorder_dict_skip_missing - -# from .protocol import Protocol -# from .activity import Activity +from .utils import SchemaUtils @define( kw_only=True, ) -class UI: +class UI(SchemaUtils): #: this is more to help set up things on the UI side than purely schema related SUPPORTED_INPUT_TYPES = ( @@ -103,35 +99,13 @@ class UI: validator=optional(instance_of(list)), ) - schema_order: Optional[List[str]] = field( - validator=optional(instance_of(list)), - ) - - @schema_order.default - def _default_schema_order(self) -> list: - default = ["shuffle", "order", "addProperties", "allow"] - if self.at_type == "reproschema:Field": - default = ["inputType", "readonlyValue"] - return default - - schema: Optional[Dict[str, Any]] = field( - validator=optional(instance_of(dict)), - ) - - @schema.default - def _default_schema(self) -> dict: - default = { - "shuffle": [], - "order": [], - "addProperties": [], - "allow": [], - } - if self.at_type == "reproschema:Field": - default = {"inputType": [], "readonlyValue": []} - return default - def __attrs_post_init__(self) -> None: + if self.schema_order in [None, []]: + self.schema_order = ["shuffle", "order", "addProperties", "allow"] + if self.at_type == "reproschema:Field": + self.schema_order = ["inputType", "readonlyValue"] + if self.at_type == "reproschema:ResponseOption": self.AllowExport = False self.AutoAdvance = False @@ -140,7 +114,7 @@ def __attrs_post_init__(self) -> None: def append(self, obj, variableName: Optional[str] = None) -> None: - this_property = AddtionalPropertity( + this_property = AdditionalProperty( variableName=variableName, isAbout=obj.URI, prefLabel=obj.prefLabel, @@ -149,7 +123,7 @@ def append(self, obj, variableName: Optional[str] = None) -> None: skippable=obj.skippable, ) this_property.update() - this_property.sort() + this_property.sort_schema() this_property.drop_empty_values_from_schema() self.order.append(obj.URI) @@ -180,20 +154,13 @@ def update(self) -> None: self.schema["inputType"] = self.inputType - self.sort() - - def sort(self) -> Dict[str, Any]: - if self.schema is None: - return - reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) - self.schema = reordered_dict - return reordered_dict + self.sort_schema() @define( kw_only=True, ) -class AddtionalPropertity: +class AdditionalProperty(SchemaUtils): variableName: Optional[str] = field( default=None, @@ -244,10 +211,16 @@ class AddtionalPropertity: validator=optional(instance_of(str)), ) - schema_order: Optional[List[str]] = field( - default=None, - converter=default_if_none( - default=[ + skippable: Optional[bool] = field( + factory=(bool), + converter=default_if_none(default=True), # type: ignore + validator=optional(instance_of(bool)), + ) + + def __attrs_post_init__(self) -> None: + + if self.schema_order in [None, []]: + self.schema_order = [ "variableName", "isAbout", "prefLabel", @@ -259,37 +232,8 @@ class AddtionalPropertity: "randomMaxDelay", "schedule", ] - ), # type: ignore - validator=optional(instance_of(list)), - ) - - skippable: Optional[bool] = field( - factory=(bool), - converter=default_if_none(default=True), # type: ignore - validator=optional(instance_of(bool)), - ) - - schema: Optional[Dict[str, Any]] = field( - factory=(dict), - validator=optional(instance_of(dict)), - ) def update(self) -> None: - """Updates the schema content based on the attributes.""" if self.skippable is True: self.allow = ["reproschema:Skipped"] - for key in self.schema_order: - self.schema[key] = self.__getattribute__(key) - - def drop_empty_values_from_schema(self) -> None: - tmp = dict(self.schema) - for key in tmp: - if self.schema[key] in [{}, [], "", None]: - self.schema.pop(key) - - def sort(self) -> Dict[str, Any]: - if self.schema is None: - return - reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) - self.schema = reordered_dict - return reordered_dict + super().update() diff --git a/reproschema/models/utils.py b/reproschema/models/utils.py index b5a892a..bec43a0 100644 --- a/reproschema/models/utils.py +++ b/reproschema/models/utils.py @@ -1,6 +1,50 @@ from collections import OrderedDict from typing import Dict from typing import List +from typing import Optional + +from attrs import define +from attrs import field +from attrs.converters import default_if_none +from attrs.validators import instance_of +from attrs.validators import optional + + +@define(kw_only=True) +class SchemaUtils: + + schema_order: Optional[list] = field( + factory=(list), + converter=default_if_none(default=[]), # type: ignore + validator=optional(instance_of(list)), + ) + #: contains the content that is read from or dumped in JSON + schema: Optional[dict] = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of(dict)), + ) + + def sort_schema(self) -> None: + if self.schema is None or self.schema_order is None: + return + reordered_dict = reorder_dict_skip_missing(self.schema, self.schema_order) + self.schema = reordered_dict + return reordered_dict + + def drop_empty_values_from_schema(self) -> None: + tmp = dict(self.schema) + for key in tmp: + if self.schema[key] in [{}, [], "", None]: + self.schema.pop(key) + + def update(self) -> None: + """Updates the schema content based on the attributes.""" + + for key in self.schema_order: + if key.startswith("@"): + continue + self.schema[key] = self.__getattribute__(key) def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> OrderedDict: From dc1def03bafedb48343bb8fbf227462cc2a918a9 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 16:32:58 +0200 Subject: [PATCH 57/69] add limit --- reproschema/models/activity.py | 2 ++ reproschema/models/base.py | 9 ++++++++- reproschema/models/item.py | 2 ++ reproschema/models/tests/test_protocol.py | 1 + reproschema/models/ui.py | 3 ++- 5 files changed, 15 insertions(+), 2 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index 8f186e0..9989b7a 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -28,6 +28,7 @@ def __init__( visible: Optional[bool] = True, required: Optional[bool] = False, skippable: Optional[bool] = True, + limit: Optional[str] = None, ext: Optional[str] = ".jsonld", output_dir: Optional[Union[str, Path]] = Path.cwd(), lang: Optional[str] = DEFAULT_LANG(), @@ -52,6 +53,7 @@ def __init__( visible=visible, required=required, skippable=skippable, + limit=limit, suffix=suffix, ext=ext, output_dir=output_dir, diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 0690a72..cb43036 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -105,6 +105,8 @@ def _default_context(self) -> str: Protocol, Activity, Field """ # associatedMedia + # video + # audio prefLabel: dict = field( factory=(dict), converter=default_if_none(default={}), # type: ignore @@ -144,7 +146,7 @@ def _default_context(self) -> str: """ # cronTable # messages - citation: str = field( + citation: Optional[str] = field( factory=(str), converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), @@ -204,6 +206,11 @@ def _default_ui(self) -> UI: converter=default_if_none(default=True), # type: ignore validator=optional(instance_of(bool)), ) + limit: Optional[str] = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) """ Non schema based attributes: OPTIONAL diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 6efd07c..8bb6d2f 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -29,6 +29,7 @@ def __init__( required: Optional[bool] = False, skippable: Optional[bool] = True, read_only: Optional[bool] = None, + limit: Optional[str] = None, suffix: Optional[str] = "", ext: Optional[str] = ".jsonld", output_dir=Path.cwd(), @@ -55,6 +56,7 @@ def __init__( required=required, skippable=skippable, readonlyValue=read_only, + limit=limit, suffix=suffix, ext=ext, output_dir=output_dir, diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py index 4dbd502..4a6ee99 100644 --- a/reproschema/models/tests/test_protocol.py +++ b/reproschema/models/tests/test_protocol.py @@ -81,6 +81,7 @@ def test_protocol_1(): activity_1 = Activity( name="activity1", prefLabel="Screening", + limit="P1W/2020-08-01T13:00:00Z", lang="en", suffix="", output_dir=os.path.join("..", "activities"), diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index 0590910..fe17f20 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -102,7 +102,7 @@ class UI(SchemaUtils): def __attrs_post_init__(self) -> None: if self.schema_order in [None, []]: - self.schema_order = ["shuffle", "order", "addProperties", "allow"] + self.schema_order = ["shuffle", "order", "addProperties", "allow", "limit"] if self.at_type == "reproschema:Field": self.schema_order = ["inputType", "readonlyValue"] @@ -121,6 +121,7 @@ def append(self, obj, variableName: Optional[str] = None) -> None: isVis=obj.visible, requiredValue=obj.required, skippable=obj.skippable, + limit=obj.limit, ) this_property.update() this_property.sort_schema() From a724eb3d1ad256d03bf9f6d160b1a12febe95d66 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 16:40:27 +0200 Subject: [PATCH 58/69] add schedule and randomMAxDelay --- reproschema/models/activity.py | 4 ++++ reproschema/models/base.py | 10 ++++++++++ reproschema/models/item.py | 4 ++++ reproschema/models/tests/test_activity.py | 10 +++++++++- reproschema/models/tests/test_protocol.py | 2 ++ reproschema/models/ui.py | 12 +++++++++++- 6 files changed, 40 insertions(+), 2 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index 9989b7a..c244cdd 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -29,6 +29,8 @@ def __init__( required: Optional[bool] = False, skippable: Optional[bool] = True, limit: Optional[str] = None, + randomMaxDelay: Optional[str] = None, + schedule: Optional[str] = None, ext: Optional[str] = ".jsonld", output_dir: Optional[Union[str, Path]] = Path.cwd(), lang: Optional[str] = DEFAULT_LANG(), @@ -54,6 +56,8 @@ def __init__( required=required, skippable=skippable, limit=limit, + randomMaxDelay=randomMaxDelay, + schedule=schedule, suffix=suffix, ext=ext, output_dir=output_dir, diff --git a/reproschema/models/base.py b/reproschema/models/base.py index cb43036..d2c0bdc 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -211,6 +211,16 @@ def _default_ui(self) -> UI: converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) + randomMaxDelay: Optional[str] = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + schedule: Optional[str] = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) """ Non schema based attributes: OPTIONAL diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 8bb6d2f..0f8b2f9 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -30,6 +30,8 @@ def __init__( skippable: Optional[bool] = True, read_only: Optional[bool] = None, limit: Optional[str] = None, + randomMaxDelay: Optional[str] = None, + schedule: Optional[str] = None, suffix: Optional[str] = "", ext: Optional[str] = ".jsonld", output_dir=Path.cwd(), @@ -56,7 +58,9 @@ def __init__( required=required, skippable=skippable, readonlyValue=read_only, + schedule=schedule, limit=limit, + randomMaxDelay=randomMaxDelay, suffix=suffix, ext=ext, output_dir=output_dir, diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index 4c13085..396988c 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -104,7 +104,15 @@ def test_activity_1(): activity.set_compute(variable="activity1_total_score", expression="item1 + item2") - item_1 = Item(name="item1", skippable=False, required=True, output_dir="items") + item_1 = Item( + name="item1", + skippable=False, + required=True, + output_dir="items", + limit="P2D", + randomMaxDelay="PT2H", + schedule="R/2020-08-01T08:00:00Z/P1D", + ) item_1.prefLabel = {} activity.update() diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py index 4a6ee99..4b87916 100644 --- a/reproschema/models/tests/test_protocol.py +++ b/reproschema/models/tests/test_protocol.py @@ -82,6 +82,8 @@ def test_protocol_1(): name="activity1", prefLabel="Screening", limit="P1W/2020-08-01T13:00:00Z", + randomMaxDelay="PT12H", + schedule="R5/2008-01-01T13:00:00Z/P1Y2M10DT2H30M", lang="en", suffix="", output_dir=os.path.join("..", "activities"), diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index fe17f20..a23ef2d 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -102,7 +102,15 @@ class UI(SchemaUtils): def __attrs_post_init__(self) -> None: if self.schema_order in [None, []]: - self.schema_order = ["shuffle", "order", "addProperties", "allow", "limit"] + self.schema_order = [ + "shuffle", + "order", + "addProperties", + "allow", + "limit", + "randomMaxDelay", + "schedule", + ] if self.at_type == "reproschema:Field": self.schema_order = ["inputType", "readonlyValue"] @@ -122,6 +130,8 @@ def append(self, obj, variableName: Optional[str] = None) -> None: requiredValue=obj.required, skippable=obj.skippable, limit=obj.limit, + randomMaxDelay=obj.randomMaxDelay, + schedule=obj.schedule, ) this_property.update() this_property.sort_schema() From 6365fe1c6a0f68468fbb8f808a6530d5ceccde78 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 17:06:34 +0200 Subject: [PATCH 59/69] add messages --- reproschema/models/activity.py | 7 +++-- reproschema/models/base.py | 31 +++++++++++++++++++++++ reproschema/models/protocol.py | 3 +++ reproschema/models/tests/test_activity.py | 10 +++++++- reproschema/models/tests/test_protocol.py | 9 +++++++ reproschema/models/utils.py | 4 ++- 6 files changed, 58 insertions(+), 6 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index c244cdd..38035c9 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -24,6 +24,7 @@ def __init__( preamble: Optional[str] = None, citation: Optional[str] = None, image: Optional[Union[str, Dict[str, str]]] = None, + messages: Optional[Dict[str, str]] = None, suffix: Optional[str] = "_schema", visible: Optional[bool] = True, required: Optional[bool] = False, @@ -36,10 +37,7 @@ def __init__( lang: Optional[str] = DEFAULT_LANG(), ): - schema_order = COMMON_SCHEMA_ORDER() + [ - "citation", - "compute", - ] + schema_order = COMMON_SCHEMA_ORDER() + ["citation", "compute", "messages"] super().__init__( at_id=name, @@ -50,6 +48,7 @@ def __init__( description=description, preamble=preamble, citation=citation, + messages=messages, image=image, schema_order=schema_order, visible=visible, diff --git a/reproschema/models/base.py b/reproschema/models/base.py index d2c0bdc..c3c7d57 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -42,6 +42,31 @@ def COMMON_SCHEMA_ORDER() -> list: ] +@define(kw_only=True) +class Message(SchemaUtils): + + jsExpression: Optional[str] = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + message: Optional[str] = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + + def __attrs_post_init__(self) -> None: + + if self.schema_order in [None, []]: + self.schema_order = [ + "message", + "jsExpression", + ] + + self.update().sort_schema() + + @define(kw_only=True) class SchemaBase(SchemaUtils): @@ -156,6 +181,11 @@ def _default_context(self) -> str: converter=default_if_none(default=[]), # type: ignore validator=optional(instance_of(list)), ) + messages: Optional[list] = field( + factory=(list), + converter=default_if_none(default=[]), # type: ignore + validator=optional(instance_of(list)), + ) """ Activity only @@ -279,6 +309,7 @@ def update(self) -> None: "landingPage", "compute", "question", + "messages", ] for key in keys_to_update: self.schema[key] = self.__getattribute__(key) diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index e81e79f..076597d 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -25,6 +25,7 @@ def __init__( landingPage: Optional[dict] = None, citation: Optional[str] = None, image: Optional[Union[str, Dict[str, str]]] = None, + messages: Optional[Dict[str, str]] = None, suffix: Optional[str] = "_schema", ext: Optional[str] = ".jsonld", output_dir: Optional[Union[str, Path]] = Path.cwd(), @@ -35,6 +36,7 @@ def __init__( "landingPage", "citation", "compute", + "messages", ] super().__init__( @@ -47,6 +49,7 @@ def __init__( landingPage=landingPage, preamble=preamble, citation=citation, + messages=messages, image=image, schema_order=schema_order, suffix=suffix, diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index 396988c..715c7d2 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -1,11 +1,11 @@ import os -from unittest import skip from utils import clean_up from utils import load_jsons from utils import output_dir from reproschema.models.activity import Activity +from reproschema.models.base import Message from reproschema.models.item import Item activity_dir = output_dir("activities") @@ -78,11 +78,19 @@ def test_activity(): def test_activity_1(): + messages = [ + Message( + jsExpression="item1 > 1", + message="Test message: Triggered when item1 value is greater than 1", + ).schema + ] + activity = Activity( name="activity1", prefLabel="Example 1", description="Activity example 1", citation="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC1495268/", + messages=messages, output_dir=activity_dir, suffix="", ) diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py index 4b87916..2afeb65 100644 --- a/reproschema/models/tests/test_protocol.py +++ b/reproschema/models/tests/test_protocol.py @@ -2,6 +2,7 @@ from pathlib import Path from reproschema.models.activity import Activity +from reproschema.models.base import Message from reproschema.models.protocol import Protocol my_path = Path(__file__).resolve().parent @@ -62,11 +63,19 @@ def test_protocol(): def test_protocol_1(): + messages = [ + Message( + jsExpression="item1 > 0", + message="Test message: Triggered when item1 value is greater than 0", + ).schema + ] + protocol = Protocol( name="protocol1", prefLabel="Protocol1", lang="en", description="example Protocol", + messages=messages, output_dir=protocol_dir, suffix="", ) diff --git a/reproschema/models/utils.py b/reproschema/models/utils.py index bec43a0..2e5d40e 100644 --- a/reproschema/models/utils.py +++ b/reproschema/models/utils.py @@ -38,7 +38,7 @@ def drop_empty_values_from_schema(self) -> None: if self.schema[key] in [{}, [], "", None]: self.schema.pop(key) - def update(self) -> None: + def update(self): """Updates the schema content based on the attributes.""" for key in self.schema_order: @@ -46,6 +46,8 @@ def update(self) -> None: continue self.schema[key] = self.__getattribute__(key) + return self + def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> OrderedDict: """ From 5c5c8f014bffeefc1654bdca3ac5cc429280b6f8 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 17:48:01 +0200 Subject: [PATCH 60/69] add audio and video --- reproschema/models/activity.py | 4 ++ reproschema/models/base.py | 38 ++++++----- reproschema/models/item.py | 4 ++ reproschema/models/protocol.py | 4 ++ reproschema/models/response_options.py | 17 +++-- .../models/tests/data/items/item1.jsonld | 67 +++++++++++-------- reproschema/models/tests/test_item.py | 45 +++++++++++++ 7 files changed, 131 insertions(+), 48 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index 38035c9..de2c4ed 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -24,6 +24,8 @@ def __init__( preamble: Optional[str] = None, citation: Optional[str] = None, image: Optional[Union[str, Dict[str, str]]] = None, + audio: Optional[Union[str, Dict[str, str]]] = None, + video: Optional[Union[str, Dict[str, str]]] = None, messages: Optional[Dict[str, str]] = None, suffix: Optional[str] = "_schema", visible: Optional[bool] = True, @@ -50,6 +52,8 @@ def __init__( citation=citation, messages=messages, image=image, + audio=audio, + video=video, schema_order=schema_order, visible=visible, required=required, diff --git a/reproschema/models/base.py b/reproschema/models/base.py index c3c7d57..45a381a 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -14,7 +14,6 @@ from attrs.validators import optional from .ui import UI -from .utils import reorder_dict_skip_missing from .utils import SchemaUtils @@ -38,6 +37,8 @@ def COMMON_SCHEMA_ORDER() -> list: "description", "preamble", "image", + "audio", + "video", "ui", ] @@ -124,20 +125,18 @@ def _default_context(self) -> str: - about """ - # TODO - """ Protocol, Activity, Field """ # associatedMedia # video # audio - prefLabel: dict = field( + prefLabel: Optional[dict] = field( factory=(dict), converter=default_if_none(default={}), # type: ignore validator=optional(instance_of(dict)), ) - altLabel: dict = field( + altLabel: Optional[dict] = field( factory=(dict), converter=default_if_none(default={}), # type: ignore validator=optional(instance_of(dict)), @@ -152,15 +151,23 @@ def _default_context(self) -> str: default=None, validator=optional(instance_of((str, dict))), ) - - preamble: dict = field( + audio: Optional[Union[str, Dict[str, str]]] = field( + default=None, + validator=optional(instance_of((str, dict))), + ) + video: Optional[Union[str, Dict[str, str]]] = field( + default=None, + validator=optional(instance_of((str, dict))), + ) + preamble: Optional[dict] = field( factory=(dict), converter=default_if_none(default={}), # type: ignore validator=optional(instance_of(dict)), ) + # Protocol only # TODO landing_page is a dict or a list of dict? - landingPage: dict = field( + landingPage: Optional[dict] = field( factory=(dict), converter=default_if_none(default={}), # type: ignore validator=optional(instance_of(dict)), @@ -169,14 +176,13 @@ def _default_context(self) -> str: """ Protocol and Activity """ - # cronTable - # messages + # TODO cronTable citation: Optional[str] = field( factory=(str), converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) - compute: Optional[list] = field( + compute: Optional[List[Dict[str, str]]] = field( factory=(list), converter=default_if_none(default=[]), # type: ignore validator=optional(instance_of(list)), @@ -190,13 +196,13 @@ def _default_context(self) -> str: """ Activity only """ - # overrideProperties + # TODO overrideProperties """ Field only """ - # additionalNotesObj - inputType: str = field( + # TODO additionalNotesObj + inputType: Optional[str] = field( factory=(str), converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), @@ -205,7 +211,7 @@ def _default_context(self) -> str: factory=(bool), validator=optional(instance_of(bool)), ) - question: dict = field( + question: Optional[dict] = field( factory=(dict), converter=default_if_none(default={}), # type: ignore validator=optional(instance_of(dict)), @@ -305,6 +311,8 @@ def update(self) -> None: "description", "citation", "image", + "audio", + "video", "preamble", "landingPage", "compute", diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 0f8b2f9..3d80637 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -24,6 +24,8 @@ def __init__( altLabel: Optional[Dict[str, str]] = None, description: Optional[str] = "", image: Optional[Union[str, Dict[str, str]]] = None, + audio: Optional[Union[str, Dict[str, str]]] = None, + video: Optional[Union[str, Dict[str, str]]] = None, preamble: Optional[str] = None, visible: Optional[bool] = True, required: Optional[bool] = False, @@ -53,6 +55,8 @@ def __init__( description=description, preamble=preamble, image=image, + audio=audio, + video=video, schema_order=schema_order, visible=visible, required=required, diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index 076597d..60c9471 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -25,6 +25,8 @@ def __init__( landingPage: Optional[dict] = None, citation: Optional[str] = None, image: Optional[Union[str, Dict[str, str]]] = None, + audio: Optional[Union[str, Dict[str, str]]] = None, + video: Optional[Union[str, Dict[str, str]]] = None, messages: Optional[Dict[str, str]] = None, suffix: Optional[str] = "_schema", ext: Optional[str] = ".jsonld", @@ -51,6 +53,8 @@ def __init__( citation=citation, messages=messages, image=image, + audio=audio, + video=video, schema_order=schema_order, suffix=suffix, ext=ext, diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index 7cbb3c9..abc47df 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -28,7 +28,7 @@ class Choice(SchemaUtils): converter=default_if_none(default=""), validator=optional(instance_of((str, dict))), ) - value = field(default=None) + value: Any = field(default=None) image: Optional[Union[str, Dict[str, str]]] = field( default=None, validator=optional(instance_of((str, dict))) ) @@ -214,20 +214,29 @@ def set_valueType(self, value: str = None) -> None: self.valueType = f"xsd:{value}" self.update() - def values_all_options(self): + def values_all_options(self) -> List[Any]: return [i["value"] for i in self.choices if "value" in i] def set_min(self, value: int = None) -> None: if value is not None: self.minValue = value elif len(self.choices) > 1: - self.minValue = min(self.values_all_options()) + self.minValue = 0 + all_values = self.values_all_options() + if all(isinstance(x, int) for x in all_values): + self.minValue = min(all_values) def set_max(self, value: int = None) -> None: + if value is not None: self.maxValue = value + elif len(self.choices) > 1: - self.maxValue = max(self.values_all_options()) + all_values = self.values_all_options() + self.maxValue = len(all_values) - 1 + if all(isinstance(x, int) for x in all_values): + self.maxValue = max(all_values) + self.update() def add_choice( diff --git a/reproschema/models/tests/data/items/item1.jsonld b/reproschema/models/tests/data/items/item1.jsonld index 9210612..05a61dc 100644 --- a/reproschema/models/tests/data/items/item1.jsonld +++ b/reproschema/models/tests/data/items/item1.jsonld @@ -2,13 +2,15 @@ "@context": "../../../contexts/generic", "@type": "reproschema:Field", "@id": "item1.jsonld", - "prefLabel": "item1", + "prefLabel": { + "en": "item1" + }, "description": "Q1 of example 1", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", "audio": { - "@type": "AudioObject", - "contentUrl": "http://media.freesound.org/sample-file.mp4" + "@type": "AudioObject", + "contentUrl": "http://media.freesound.org/sample-file.mp4" }, "image": { "@type": "ImageObject", @@ -27,34 +29,39 @@ "maxValue": 3, "multipleChoice": false, "choices": [ - { - "name": { - "en": "Not at all", - "es": "Para nada" - }, - "value": 0 - }, - { - "name": { - "en": "Several days", - "es": "Varios días" + { + "name": { + "en": "Not at all", + "es": "Para nada" + }, + "value": 0 }, - "value": "a" - }, - { - "name": { - "en": "More than half the days", - "es": "Más de la mitad de los días" + { + "name": { + "en": "Several days", + "es": "Varios días" + }, + "value": "a" }, - "value": {"@id": "http://example.com/choice3" } - }, - { - "name": { - "en": "Nearly everyday", - "es": "Casi todos los días" + { + "name": { + "en": "More than half the days", + "es": "Más de la mitad de los días" + }, + "value": { + "@id": "http://example.com/choice3" + } }, - "value": {"@value": "choice-with-lang", "@language": "en"} - } + { + "name": { + "en": "Nearly everyday", + "es": "Casi todos los días" + }, + "value": { + "@value": "choice-with-lang", + "@language": "en" + } + } ] }, "additionalNotesObj": [ @@ -66,7 +73,9 @@ { "source": "redcap", "column": "notes", - "value": {"@id": "http://example.com/iri-example"} + "value": { + "@id": "http://example.com/iri-example" + } } ] } diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index 53369a7..ff5110e 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -1,4 +1,5 @@ import os +from asyncio import QueueEmpty from pathlib import Path import pytest @@ -163,6 +164,50 @@ def test_slider(): clean_up(item_dir, item) +def test_item1(): + + item = Item( + name="item1", + input_type="radio", + description="Q1 of example 1", + question="Little interest or pleasure in doing things", + image={ + "@type": "ImageObject", + "contentUrl": "http://example.com/sample-image.jpg", + }, + audio={ + "@type": "AudioObject", + "contentUrl": "http://media.freesound.org/sample-file.mp4", + }, + read_only=None, + output_dir=item_dir, + ) + item.at_context = "../../../contexts/generic" + item.set_question(question="Poco interés o placer en hacer cosas", lang="es") + + response_options = ResponseOption(multipleChoice=False) + response_options.add_choice(name={"en": "Not at all", "es": "Para nada"}, value=0) + response_options.add_choice( + name={"en": "Several days", "es": "Varios días"}, value="a" + ) + response_options.add_choice( + name={"en": "More than half the days", "es": "Más de la mitad de los días"}, + value={"@id": "http://example.com/choice3"}, + ) + response_options.add_choice( + name={"en": "Nearly everyday", "es": "Casi todos los días"}, + value={"@value": "choice-with-lang", "@language": "en"}, + ) + item.set_input_type(response_options=response_options) + + item.write() + + item_content, expected = load_jsons(item_dir, item) + assert item_content == expected + + clean_up(item_dir, item) + + """ Just to check that item with read only values From 743fd3432d40456f74bc11b044c518463ee3763a Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 18:05:48 +0200 Subject: [PATCH 61/69] add additionalNotesObj --- reproschema/models/base.py | 33 +++++++++++++++++++++++++++ reproschema/models/item.py | 5 ++++ reproschema/models/tests/test_item.py | 17 +++++++++++++- 3 files changed, 54 insertions(+), 1 deletion(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 45a381a..bd6a8f0 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -1,6 +1,7 @@ import json import os from pathlib import Path +from typing import Any from typing import Dict from typing import List from typing import Optional @@ -43,6 +44,32 @@ def COMMON_SCHEMA_ORDER() -> list: ] +@define(kw_only=True) +class AdditionalNoteObj(SchemaUtils): + column: Optional[str] = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + source: Optional[str] = field( + factory=(str), + converter=default_if_none(default=""), # type: ignore + validator=optional(instance_of(str)), + ) + value: Any = field(default=None) + + def __attrs_post_init__(self) -> None: + + if self.schema_order in [None, []]: + self.schema_order = [ + "column", + "source", + "value", + ] + + self.update().sort_schema() + + @define(kw_only=True) class Message(SchemaUtils): @@ -216,6 +243,11 @@ def _default_context(self) -> str: converter=default_if_none(default={}), # type: ignore validator=optional(instance_of(dict)), ) + additionalNotesObj: Optional[list] = field( + factory=(list), + converter=default_if_none(default=[]), # type: ignore + validator=optional(instance_of(list)), + ) """ UI related @@ -318,6 +350,7 @@ def update(self) -> None: "compute", "question", "messages", + "additionalNotesObj", ] for key in keys_to_update: self.schema[key] = self.__getattribute__(key) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 3d80637..4763e54 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -1,5 +1,7 @@ from pathlib import Path +from typing import Any from typing import Dict +from typing import List from typing import Optional from typing import Union @@ -27,6 +29,7 @@ def __init__( audio: Optional[Union[str, Dict[str, str]]] = None, video: Optional[Union[str, Dict[str, str]]] = None, preamble: Optional[str] = None, + additionalNotesObj: List[Dict[str, Any]] = None, visible: Optional[bool] = True, required: Optional[bool] = False, skippable: Optional[bool] = True, @@ -43,6 +46,7 @@ def __init__( schema_order = COMMON_SCHEMA_ORDER() + [ "question", "responseOptions", + "additionalNotesObj", ] super().__init__( @@ -57,6 +61,7 @@ def __init__( image=image, audio=audio, video=video, + additionalNotesObj=additionalNotesObj, schema_order=schema_order, visible=visible, required=required, diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index ff5110e..c306631 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -1,5 +1,4 @@ import os -from asyncio import QueueEmpty from pathlib import Path import pytest @@ -8,6 +7,7 @@ from utils import output_dir from utils import read_json +from reproschema.models.base import AdditionalNoteObj from reproschema.models.item import Item from reproschema.models.item import ResponseOption @@ -166,6 +166,20 @@ def test_slider(): def test_item1(): + additionalNotes = [ + AdditionalNoteObj( + column="notes", source="redcap", value="some extra note" + ).schema + ] + print(additionalNotes) + additionalNotes.append( + AdditionalNoteObj( + column="notes", + source="redcap", + value={"@id": "http://example.com/iri-example"}, + ).schema + ) + item = Item( name="item1", input_type="radio", @@ -179,6 +193,7 @@ def test_item1(): "@type": "AudioObject", "contentUrl": "http://media.freesound.org/sample-file.mp4", }, + additionalNotesObj=additionalNotes, read_only=None, output_dir=item_dir, ) From 55bf1863b5b0de35b8dc956c88ac4a03ddaaef67 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 18:51:18 +0200 Subject: [PATCH 62/69] add unitOption --- reproschema/models/base.py | 3 - reproschema/models/response_options.py | 47 ++++++++++++-- .../models/tests/data/items/item2.jsonld | 35 +++++----- reproschema/models/tests/test_item.py | 65 ++++++++++++++----- 4 files changed, 111 insertions(+), 39 deletions(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index bd6a8f0..71693a7 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -156,8 +156,6 @@ def _default_context(self) -> str: Protocol, Activity, Field """ # associatedMedia - # video - # audio prefLabel: Optional[dict] = field( factory=(dict), converter=default_if_none(default={}), # type: ignore @@ -228,7 +226,6 @@ def _default_context(self) -> str: """ Field only """ - # TODO additionalNotesObj inputType: Optional[str] = field( factory=(str), converter=default_if_none(default=""), # type: ignore diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index abc47df..e2aec4e 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -16,10 +16,49 @@ from .base import DEFAULT_LANG from .base import DEFAULT_VERSION -from .utils import reorder_dict_skip_missing from .utils import SchemaUtils +@define(kw_only=True) +class unitOption(SchemaUtils): + + value: Any = field(default=None) + prefLabel: Optional[Union[str, Dict[str, str]]] = field( + factory=(dict), + converter=default_if_none(default={}), # type: ignore + validator=optional(instance_of((dict, str))), + ) + lang: Optional[str] = field( + default=None, + converter=default_if_none(default=DEFAULT_LANG()), # type: ignore + validator=optional(instance_of(str)), + ) + + def __attrs_post_init__(self) -> None: + + if self.schema_order in [None, []]: + self.schema_order = [ + "prefLabel", + "value", + ] + if isinstance(self.prefLabel, str): + self.prefLabel = {self.lang: self.prefLabel} + + self.update() + self.sort_schema() + + def set_pref_label( + self, pref_label: Optional[str] = None, lang: Optional[str] = None + ) -> None: + if pref_label is None: + return + if lang is None: + lang = self.lang + + self.prefLabel[lang] = pref_label + self.update() + + @define(kw_only=True) class Choice(SchemaUtils): @@ -131,10 +170,10 @@ def _default_context(self) -> str: validator=optional(instance_of(int)), ) - unitOptions: Optional[int] = field( + unitOptions: Optional[list] = field( default=None, - converter=default_if_none(default=""), # type: ignore - validator=optional(instance_of(str)), + converter=default_if_none(default=[]), # type: ignore + validator=optional(instance_of(list)), ) unitCode: Optional[str] = field( diff --git a/reproschema/models/tests/data/items/item2.jsonld b/reproschema/models/tests/data/items/item2.jsonld index cf1b37a..732903d 100644 --- a/reproschema/models/tests/data/items/item2.jsonld +++ b/reproschema/models/tests/data/items/item2.jsonld @@ -2,7 +2,9 @@ "@context": "../../../contexts/generic", "@type": "reproschema:Field", "@id": "item2.jsonld", - "prefLabel": "item2", + "prefLabel": { + "en": "item2" + }, "description": "Q2 of example 1", "schemaVersion": "1.0.0-rc4", "version": "0.0.1", @@ -11,30 +13,29 @@ "es": "Fiebre actual." }, "video": { - "@type": "VideoObject", - "contentUrl": "http://media.freesound.org/data/0/previews/719__elmomo__12oclock_girona_preview.mp4" + "@type": "VideoObject", + "contentUrl": "http://media.freesound.org/data/0/previews/719__elmomo__12oclock_girona_preview.mp4" }, - "imageUrl": "http://example.com/sample-image.jpg", "ui": { "inputType": "float" }, "responseOptions": { "valueType": "xsd:float", "unitOptions": [ - { - "prefLabel": { - "en": "Fahrenheit", - "es": "Fahrenheit" - }, - "value": "°F" - }, - { - "prefLabel": { - "en": "Celsius", - "es": "Celsius" + { + "prefLabel": { + "en": "Fahrenheit", + "es": "Fahrenheit" + }, + "value": "°F" }, - "value": "°C" - } + { + "prefLabel": { + "en": "Celsius", + "es": "Celsius" + }, + "value": "°C" + } ] } } diff --git a/reproschema/models/tests/test_item.py b/reproschema/models/tests/test_item.py index c306631..02ac6a6 100644 --- a/reproschema/models/tests/test_item.py +++ b/reproschema/models/tests/test_item.py @@ -9,7 +9,8 @@ from reproschema.models.base import AdditionalNoteObj from reproschema.models.item import Item -from reproschema.models.item import ResponseOption +from reproschema.models.response_options import ResponseOption +from reproschema.models.response_options import unitOption item_dir = output_dir("items") @@ -171,7 +172,6 @@ def test_item1(): column="notes", source="redcap", value="some extra note" ).schema ] - print(additionalNotes) additionalNotes.append( AdditionalNoteObj( column="notes", @@ -180,6 +180,20 @@ def test_item1(): ).schema ) + response_options = ResponseOption(multipleChoice=False) + response_options.add_choice(name={"en": "Not at all", "es": "Para nada"}, value=0) + response_options.add_choice( + name={"en": "Several days", "es": "Varios días"}, value="a" + ) + response_options.add_choice( + name={"en": "More than half the days", "es": "Más de la mitad de los días"}, + value={"@id": "http://example.com/choice3"}, + ) + response_options.add_choice( + name={"en": "Nearly everyday", "es": "Casi todos los días"}, + value={"@value": "choice-with-lang", "@language": "en"}, + ) + item = Item( name="item1", input_type="radio", @@ -199,21 +213,42 @@ def test_item1(): ) item.at_context = "../../../contexts/generic" item.set_question(question="Poco interés o placer en hacer cosas", lang="es") + item.set_input_type(response_options=response_options) - response_options = ResponseOption(multipleChoice=False) - response_options.add_choice(name={"en": "Not at all", "es": "Para nada"}, value=0) - response_options.add_choice( - name={"en": "Several days", "es": "Varios días"}, value="a" - ) - response_options.add_choice( - name={"en": "More than half the days", "es": "Más de la mitad de los días"}, - value={"@id": "http://example.com/choice3"}, - ) - response_options.add_choice( - name={"en": "Nearly everyday", "es": "Casi todos los días"}, - value={"@value": "choice-with-lang", "@language": "en"}, + item.write() + + item_content, expected = load_jsons(item_dir, item) + assert item_content == expected + + clean_up(item_dir, item) + + +def test_item2(): + + item = Item( + name="item2", + input_type="float", + description="Q2 of example 1", + question="Current temperature.", + video={ + "@type": "VideoObject", + "contentUrl": "http://media.freesound.org/data/0/previews/719__elmomo__12oclock_girona_preview.mp4", + }, + read_only=None, + output_dir=item_dir, ) - item.set_input_type(response_options=response_options) + item.at_context = "../../../contexts/generic" + item.set_question(question="Fiebre actual.", lang="es") + + unitOption_0 = unitOption(value="°F", prefLabel="Fahrenheit") + unitOption_0.set_pref_label(pref_label="Fahrenheit", lang="es") + + unitOption_1 = unitOption(value="°C", prefLabel="Celsius") + unitOption_1.set_pref_label(pref_label="Celsius", lang="es") + + item.response_options.unitOptions = [unitOption_0.schema] + item.response_options.unitOptions.append(unitOption_1.schema) + item.set_response_options() item.write() From 7fd245026a042abb5f5ab503c86250eb5b4ad177 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 19:29:48 +0200 Subject: [PATCH 63/69] read response_options from file --- reproschema/models/response_options.py | 6 ++++++ reproschema/models/tests/test_response_options.py | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index e2aec4e..094f394 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -342,6 +342,12 @@ def from_data(cls, data: dict): if klass.at_type != data["@type"]: raise ValueError(f"Mismatch in type {data['@type']} != {klass.at_type}") klass.schema = data + """Load values into instance""" + for key in klass.schema: + if key.startswith("@"): + klass.__setattr__(f"at_{key[1:]}", klass.schema[key]) + else: + klass.__setattr__(key, klass.schema[key]) return klass @classmethod diff --git a/reproschema/models/tests/test_response_options.py b/reproschema/models/tests/test_response_options.py index b99611a..574af14 100644 --- a/reproschema/models/tests/test_response_options.py +++ b/reproschema/models/tests/test_response_options.py @@ -47,3 +47,16 @@ def test_default(): assert content == expected clean_up(response_options_dir, response_options) + + +def test_constructor_from_file(): + + response_options = ResponseOption.from_file( + response_options_dir.joinpath( + "..", "data", "response_options", "example.jsonld" + ) + ) + + assert response_options.at_id == "example.jsonld" + assert response_options.valueType == "xsd:integer" + assert response_options.choices[0]["name"] == {"en": "Not at all"} From 746d0e46603a251b907f313970b7ab5d629a2da6 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 22:32:11 +0200 Subject: [PATCH 64/69] refactor refactor refactor refactor refactor --- reproschema/models/activity.py | 3 +- reproschema/models/base.py | 78 ++++++++++---------------- reproschema/models/item.py | 4 +- reproschema/models/protocol.py | 58 ++++++++++--------- reproschema/models/response_options.py | 46 ++------------- reproschema/models/utils.py | 36 ++++++++++++ 6 files changed, 107 insertions(+), 118 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index de2c4ed..c6f7bf8 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -4,9 +4,9 @@ from typing import Union from .base import COMMON_SCHEMA_ORDER -from .base import DEFAULT_LANG from .base import SchemaBase from .item import Item +from .utils import DEFAULT_LANG class Activity(SchemaBase): @@ -66,6 +66,7 @@ def __init__( output_dir=output_dir, lang=lang, ) + super().set_defaults() self.ui.shuffle = False self.update() diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 71693a7..d76467f 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -15,13 +15,10 @@ from attrs.validators import optional from .ui import UI +from .utils import DEFAULT_LANG from .utils import SchemaUtils -def DEFAULT_LANG() -> str: - return "en" - - def DEFAULT_VERSION() -> str: return "1.0.0-rc4" @@ -156,15 +153,15 @@ def _default_context(self) -> str: Protocol, Activity, Field """ # associatedMedia - prefLabel: Optional[dict] = field( + prefLabel: Optional[Union[str, Dict[str, str]]] = field( factory=(dict), converter=default_if_none(default={}), # type: ignore - validator=optional(instance_of(dict)), + validator=optional(instance_of((str, dict))), ) - altLabel: Optional[dict] = field( + altLabel: Optional[Union[str, Dict[str, str]]] = field( factory=(dict), converter=default_if_none(default={}), # type: ignore - validator=optional(instance_of(dict)), + validator=optional(instance_of((str, dict))), ) # TODO description is language specific? description: str = field( @@ -250,13 +247,10 @@ def _default_context(self) -> str: UI related """ ui: UI = field( + default=UI(at_type="reproschema:Field"), validator=optional(instance_of(UI)), ) - @ui.default - def _default_ui(self) -> UI: - return UI(at_type=self.at_type, readonlyValue=self.readonlyValue) - visible: Optional[bool] = field( factory=(bool), converter=default_if_none(default=True), # type: ignore @@ -293,11 +287,7 @@ def _default_ui(self) -> UI: Those attributes help with file management and with printing json files with standardized key orders """ - lang: Optional[str] = field( - factory=(str), - converter=default_if_none(default=DEFAULT_LANG()), # type: ignore - validator=optional(instance_of(str)), - ) + output_dir: Optional[Union[str, Path]] = field( default=None, converter=default_if_none(default=Path.cwd()), # type: ignore @@ -319,12 +309,21 @@ def _default_ui(self) -> UI: validator=optional(instance_of((str, Path))), ) - def __attrs_post_init__(self) -> None: + def set_defaults(self): + + self.ui = UI(at_type=self.at_type, readonlyValue=self.readonlyValue) if self.description == "": self.description = self.at_id.replace("_", " ") + if self.prefLabel == "": + self.prefLabel = self.at_id.replace("_", " ") + + if isinstance(self.prefLabel, str): + self.prefLabel = {self.lang: self.prefLabel} + self.set_pref_label() + self.set_filename() def update(self) -> None: @@ -418,20 +417,23 @@ def set_alt_label( def set_pref_label( self, pref_label: Optional[str] = None, lang: Optional[str] = None ) -> None: - if pref_label is None: - if self.prefLabel == {} or self.prefLabel[DEFAULT_LANG()] in [ - "protocol", - "activity", - "item", - ]: - self.set_pref_label( - pref_label=self.at_id.replace("_", " "), lang=self.lang - ) - return - if lang is None: lang = self.lang + if pref_label is None: + if isinstance(self.prefLabel, dict) and ( + self.prefLabel == {} + or self.prefLabel[lang] + in [ + "protocol", + "activity", + "item", + ] + ): + self.set_pref_label(pref_label=self.at_id.replace("_", " "), lang=lang) + # pref_label = self.at_id.replace("_", " ") + return + self.prefLabel[lang] = pref_label self.update() @@ -475,21 +477,3 @@ def write(self, output_dir: Optional[Union[str, Path]] = None) -> None: with open(output_dir.joinpath(self.at_id), "w") as ff: json.dump(self.schema, ff, sort_keys=False, indent=4) - - @classmethod - def from_data(cls, data: dict): - klass = cls() - if klass.at_type is None: - raise ValueError("SchemaBase cannot be used to instantiate class") - if klass.at_type != data["@type"]: - raise ValueError(f"Mismatch in type {data['@type']} != {klass.at_type}") - klass.schema = data - return klass - - @classmethod - def from_file(cls, filepath: Union[str, Path]): - with open(filepath) as fp: - data = json.load(fp) - if "@type" not in data: - raise ValueError("Missing @type key") - return cls.from_data(data) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 4763e54..8a75d08 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -6,9 +6,9 @@ from typing import Union from .base import COMMON_SCHEMA_ORDER -from .base import DEFAULT_LANG from .base import SchemaBase from .response_options import ResponseOption +from .utils import DEFAULT_LANG class Item(SchemaBase): @@ -76,6 +76,8 @@ def __init__( lang=lang, ) + super().set_defaults() + self.set_question(question=question) self.response_options: ResponseOption = ResponseOption() diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index 60c9471..9c40ee7 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -1,37 +1,37 @@ -from pathlib import Path -from typing import Dict from typing import Optional -from typing import Union + +from attrs import define from .activity import Activity from .base import COMMON_SCHEMA_ORDER -from .base import DEFAULT_LANG from .base import SchemaBase +from .utils import DEFAULT_LANG +@define(kw_only=True) class Protocol(SchemaBase): """ class to deal with reproschema protocols """ + # name = field(default="protocol") + def __init__( self, name: Optional[str] = "protocol", - schemaVersion: Optional[str] = None, - prefLabel: Optional[str] = "protocol", - altLabel: Optional[Dict[str, str]] = None, - description: Optional[str] = "", - preamble: Optional[str] = None, - landingPage: Optional[dict] = None, - citation: Optional[str] = None, - image: Optional[Union[str, Dict[str, str]]] = None, - audio: Optional[Union[str, Dict[str, str]]] = None, - video: Optional[Union[str, Dict[str, str]]] = None, - messages: Optional[Dict[str, str]] = None, + # schemaVersion: Optional[str] = None, + # prefLabel: Optional[str] = "protocol", + # landingPage: Optional[dict] = None, + # citation: Optional[str] = None, + # image: Optional[Union[str, Dict[str, str]]] = None, + # audio: Optional[Union[str, Dict[str, str]]] = None, + # video: Optional[Union[str, Dict[str, str]]] = None, + # messages: Optional[Dict[str, str]] = None, suffix: Optional[str] = "_schema", ext: Optional[str] = ".jsonld", - output_dir: Optional[Union[str, Path]] = Path.cwd(), + # output_dir: Optional[Union[str, Path]] = Path.cwd(), lang: Optional[str] = DEFAULT_LANG(), + **kwargs ): schema_order = COMMON_SCHEMA_ORDER() + [ @@ -44,23 +44,25 @@ def __init__( super().__init__( at_id=name, at_type="reproschema:Protocol", - schemaVersion=schemaVersion, - prefLabel={lang: prefLabel}, - altLabel=altLabel, - description=description, - landingPage=landingPage, - preamble=preamble, - citation=citation, - messages=messages, - image=image, - audio=audio, - video=video, + # schemaVersion=schemaVersion, + # prefLabel={lang: prefLabel}, + # altLabel=altLabel, + # description=description, + # landingPage=landingPage, + # preamble=preamble, + # citation=citation, + # messages=messages, + # image=image, + # audio=audio, + # video=video, schema_order=schema_order, suffix=suffix, ext=ext, - output_dir=output_dir, + # output_dir=output_dir, lang=lang, + **kwargs ) + super().set_defaults() self.ui.shuffle = False self.update() diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index 094f394..18f4c0f 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -14,7 +14,6 @@ from attrs.validators import instance_of from attrs.validators import optional -from .base import DEFAULT_LANG from .base import DEFAULT_VERSION from .utils import SchemaUtils @@ -28,11 +27,6 @@ class unitOption(SchemaUtils): converter=default_if_none(default={}), # type: ignore validator=optional(instance_of((dict, str))), ) - lang: Optional[str] = field( - default=None, - converter=default_if_none(default=DEFAULT_LANG()), # type: ignore - validator=optional(instance_of(str)), - ) def __attrs_post_init__(self) -> None: @@ -72,12 +66,6 @@ class Choice(SchemaUtils): default=None, validator=optional(instance_of((str, dict))) ) - lang: Optional[str] = field( - default=None, - converter=default_if_none(default=DEFAULT_LANG()), # type: ignore - validator=optional(instance_of(str)), - ) - def __attrs_post_init__(self) -> None: if self.schema_order in [None, []]: @@ -199,11 +187,11 @@ def _default_context(self) -> str: Those attributes help with file management and with printing json files with standardized key orders """ - lang: Optional[str] = field( - default=None, - converter=default_if_none(default=DEFAULT_LANG()), # type: ignore - validator=optional(instance_of(str)), - ) + # lang: Optional[str] = field( + # default=None, + # converter=default_if_none(default=DEFAULT_LANG()), # type: ignore + # validator=optional(instance_of(str)), + # ) output_dir: Optional[Union[str, Path]] = field( default=None, @@ -333,27 +321,3 @@ def write(self, output_dir: Optional[Union[str, Path]] = None) -> None: with open(output_dir.joinpath(self.at_id), "w") as ff: json.dump(self.schema, ff, sort_keys=False, indent=4) - - @classmethod - def from_data(cls, data: dict): - klass = cls() - if klass.at_type is None: - raise ValueError("SchemaBase cannot be used to instantiate class") - if klass.at_type != data["@type"]: - raise ValueError(f"Mismatch in type {data['@type']} != {klass.at_type}") - klass.schema = data - """Load values into instance""" - for key in klass.schema: - if key.startswith("@"): - klass.__setattr__(f"at_{key[1:]}", klass.schema[key]) - else: - klass.__setattr__(key, klass.schema[key]) - return klass - - @classmethod - def from_file(cls, filepath: Union[str, Path]): - with open(filepath) as fp: - data = json.load(fp) - if "@type" not in data: - raise ValueError("Missing @type key") - return cls.from_data(data) diff --git a/reproschema/models/utils.py b/reproschema/models/utils.py index 2e5d40e..4cf1243 100644 --- a/reproschema/models/utils.py +++ b/reproschema/models/utils.py @@ -1,7 +1,10 @@ +import json from collections import OrderedDict +from pathlib import Path from typing import Dict from typing import List from typing import Optional +from typing import Union from attrs import define from attrs import field @@ -10,6 +13,10 @@ from attrs.validators import optional +def DEFAULT_LANG() -> str: + return "en" + + @define(kw_only=True) class SchemaUtils: @@ -24,6 +31,11 @@ class SchemaUtils: converter=default_if_none(default={}), # type: ignore validator=optional(instance_of(dict)), ) + lang: Optional[str] = field( + default=None, + converter=default_if_none(default=DEFAULT_LANG()), # type: ignore + validator=optional(instance_of(str)), + ) def sort_schema(self) -> None: if self.schema is None or self.schema_order is None: @@ -48,6 +60,30 @@ def update(self): return self + @classmethod + def from_data(cls, data: dict): + klass = cls() + if klass.at_type is None: + raise ValueError("Base class cannot be used to instantiate") + if klass.at_type != data["@type"]: + raise ValueError(f"Mismatch in type {data['@type']} != {klass.at_type}") + klass.schema = data + """Load values into instance""" + for key in klass.schema: + if key.startswith("@"): + klass.__setattr__(f"at_{key[1:]}", klass.schema[key]) + else: + klass.__setattr__(key, klass.schema[key]) + return klass + + @classmethod + def from_file(cls, filepath: Union[str, Path]): + with open(filepath) as fp: + data = json.load(fp) + if "@type" not in data: + raise ValueError("Missing @type key") + return cls.from_data(data) + def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> OrderedDict: """ From 1c1b333c238bf9e0700a7cadcde6756028061e7d Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Mon, 15 Aug 2022 22:51:43 +0200 Subject: [PATCH 65/69] undo --- reproschema/models/activity.py | 4 +-- reproschema/models/protocol.py | 50 ++++++++++++++++++---------------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/reproschema/models/activity.py b/reproschema/models/activity.py index c6f7bf8..aa43f0f 100644 --- a/reproschema/models/activity.py +++ b/reproschema/models/activity.py @@ -18,8 +18,8 @@ def __init__( self, name: Optional[str] = "activity", schemaVersion: Optional[str] = None, - prefLabel: Optional[str] = "activity", - altLabel: Optional[Dict[str, str]] = None, + prefLabel: Optional[Union[str, Dict[str, str]]] = "activity", + altLabel: Optional[Union[str, Dict[str, str]]] = None, description: Optional[str] = "", preamble: Optional[str] = None, citation: Optional[str] = None, diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index 9c40ee7..881c38c 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -1,4 +1,7 @@ +from pathlib import Path +from typing import Dict from typing import Optional +from typing import Union from attrs import define @@ -14,22 +17,23 @@ class Protocol(SchemaBase): class to deal with reproschema protocols """ - # name = field(default="protocol") - def __init__( self, name: Optional[str] = "protocol", - # schemaVersion: Optional[str] = None, - # prefLabel: Optional[str] = "protocol", - # landingPage: Optional[dict] = None, - # citation: Optional[str] = None, - # image: Optional[Union[str, Dict[str, str]]] = None, - # audio: Optional[Union[str, Dict[str, str]]] = None, - # video: Optional[Union[str, Dict[str, str]]] = None, - # messages: Optional[Dict[str, str]] = None, + schemaVersion: Optional[str] = None, + prefLabel: Optional[str] = "protocol", + altLabel: Optional[Union[str, Dict[str, str]]] = None, + description: Optional[str] = "", + landingPage: Optional[dict] = None, + preamble: Optional[str] = None, + citation: Optional[str] = None, + image: Optional[Union[str, Dict[str, str]]] = None, + audio: Optional[Union[str, Dict[str, str]]] = None, + video: Optional[Union[str, Dict[str, str]]] = None, + messages: Optional[Dict[str, str]] = None, suffix: Optional[str] = "_schema", ext: Optional[str] = ".jsonld", - # output_dir: Optional[Union[str, Path]] = Path.cwd(), + output_dir: Optional[Union[str, Path]] = Path.cwd(), lang: Optional[str] = DEFAULT_LANG(), **kwargs ): @@ -44,21 +48,21 @@ def __init__( super().__init__( at_id=name, at_type="reproschema:Protocol", - # schemaVersion=schemaVersion, - # prefLabel={lang: prefLabel}, - # altLabel=altLabel, - # description=description, - # landingPage=landingPage, - # preamble=preamble, - # citation=citation, - # messages=messages, - # image=image, - # audio=audio, - # video=video, + schemaVersion=schemaVersion, + prefLabel={lang: prefLabel}, + altLabel=altLabel, + description=description, + landingPage=landingPage, + preamble=preamble, + citation=citation, + messages=messages, + image=image, + audio=audio, + video=video, schema_order=schema_order, suffix=suffix, ext=ext, - # output_dir=output_dir, + output_dir=output_dir, lang=lang, **kwargs ) From 61310c85b4e04ed59d6e611ac988135761410731 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Wed, 17 Aug 2022 10:12:42 +0200 Subject: [PATCH 66/69] update doc update doc --- .vscode/settings.json | 3 +- Makefile | 38 +++++++++++++ docs/Makefile | 20 +++++++ docs/make.bat | 35 ++++++++++++ docs/source/architecture.md | 20 +++++++ docs/source/conf.py | 55 ++++++++++++++++++ docs/source/index.rst | 20 +++++++ docs/source/modules.rst | 7 +++ docs/source/reproschema.models.rst | 69 +++++++++++++++++++++++ docs/source/reproschema.rst | 53 +++++++++++++++++ reproschema/models/base.py | 13 ++++- reproschema/models/item.py | 28 ++++++--- reproschema/models/response_options.py | 78 +++++++++++++++++++------- reproschema/models/ui.py | 21 +++++++ reproschema/models/utils.py | 22 ++++++-- setup.cfg | 2 + 16 files changed, 449 insertions(+), 35 deletions(-) create mode 100644 Makefile create mode 100644 docs/Makefile create mode 100644 docs/make.bat create mode 100644 docs/source/architecture.md create mode 100644 docs/source/conf.py create mode 100644 docs/source/index.rst create mode 100644 docs/source/modules.rst create mode 100644 docs/source/reproschema.models.rst create mode 100644 docs/source/reproschema.rst diff --git a/.vscode/settings.json b/.vscode/settings.json index 4bc4068..b337018 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,6 @@ "cSpell.words": [ "reproschema" ], - "autoDocstring.docstringFormat": "numpy" + "autoDocstring.docstringFormat": "sphinx", + "esbonio.sphinx.confDir": "" } diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..88e128b --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +.DEFAULT_GOAL := help + +define BROWSER_PYSCRIPT +import os, webbrowser, sys + +from urllib.request import pathname2url + +webbrowser.open("file://" + pathname2url(os.path.abspath(sys.argv[1]))) +endef +export BROWSER_PYSCRIPT + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef +export PRINT_HELP_PYSCRIPT + +BROWSER := python -c "$$BROWSER_PYSCRIPT" + +# Put it first so that "make" without argument is like "make help". +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +## DOC +.PHONY: docs + +docs: ## generate Sphinx HTML documentation, including API docs + rm -f docs/source/reproschema.rst + rm -f docs/source/modules.rst + sphinx-apidoc -o docs/source reproschema + $(MAKE) -C docs clean + $(MAKE) -C docs html + $(BROWSER) docs/build/html/index.html diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d0c3cbf --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..dc1312a --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/architecture.md b/docs/source/architecture.md new file mode 100644 index 0000000..61d5d72 --- /dev/null +++ b/docs/source/architecture.md @@ -0,0 +1,20 @@ +# architecture + + + +```{mermaid} + classDiagram + SchemaUtils <|-- UI + SchemaBase *-- UI + SchemaBase <|-- Protocol + SchemaBase <|-- Activity + SchemaUtils <|-- Message + SchemaBase <|-- Item + Item *-- ResponseOption + SchemaUtils <|-- ResponseOption + SchemaUtils <|-- AdditionalNoteObj + SchemaUtils <|-- unitOption + SchemaUtils <|-- Choice + SchemaUtils <|-- AdditionalProperty + SchemaUtils <|-- SchemaBase +``` diff --git a/docs/source/conf.py b/docs/source/conf.py new file mode 100644 index 0000000..001dd5a --- /dev/null +++ b/docs/source/conf.py @@ -0,0 +1,55 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html +# -- Path setup -------------------------------------------------------------- +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys + +sys.path.insert(0, os.path.abspath("../..")) + + +# -- Project information ----------------------------------------------------- + +project = "reproschema-py" +copyright = "2022, Repronim developers" +author = "Repronim developers" + +# The full version, including alpha/beta/rc tags +release = "0.6.2" + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = ["sphinx.ext.autodoc", "myst_parser", "sphinxcontrib.mermaid"] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = [] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = "sphinx_rtd_theme" + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ["_static"] diff --git a/docs/source/index.rst b/docs/source/index.rst new file mode 100644 index 0000000..60fbd63 --- /dev/null +++ b/docs/source/index.rst @@ -0,0 +1,20 @@ +.. reproschema-py documentation master file, created by + sphinx-quickstart on Mon Aug 15 22:56:47 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to reproschema-py's documentation! +========================================== + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + architecture + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/source/modules.rst b/docs/source/modules.rst new file mode 100644 index 0000000..83ba206 --- /dev/null +++ b/docs/source/modules.rst @@ -0,0 +1,7 @@ +reproschema +=========== + +.. toctree:: + :maxdepth: 4 + + reproschema diff --git a/docs/source/reproschema.models.rst b/docs/source/reproschema.models.rst new file mode 100644 index 0000000..936288f --- /dev/null +++ b/docs/source/reproschema.models.rst @@ -0,0 +1,69 @@ +reproschema.models package +========================== + +Submodules +---------- + +reproschema.models.activity module +---------------------------------- + +.. automodule:: reproschema.models.activity + :members: + :undoc-members: + :show-inheritance: + +reproschema.models.base module +------------------------------ + +.. automodule:: reproschema.models.base + :members: + :undoc-members: + :show-inheritance: + +reproschema.models.item module +------------------------------ + +.. automodule:: reproschema.models.item + :members: + :undoc-members: + :show-inheritance: + +reproschema.models.protocol module +---------------------------------- + +.. automodule:: reproschema.models.protocol + :members: + :undoc-members: + :show-inheritance: + +reproschema.models.response\_options module +------------------------------------------- + +.. automodule:: reproschema.models.response_options + :members: + :undoc-members: + :show-inheritance: + +reproschema.models.ui module +---------------------------- + +.. automodule:: reproschema.models.ui + :members: + :undoc-members: + :show-inheritance: + +reproschema.models.utils module +------------------------------- + +.. automodule:: reproschema.models.utils + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: reproschema.models + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/reproschema.rst b/docs/source/reproschema.rst new file mode 100644 index 0000000..6932c41 --- /dev/null +++ b/docs/source/reproschema.rst @@ -0,0 +1,53 @@ +reproschema package +=================== + +Subpackages +----------- + +.. toctree:: + :maxdepth: 4 + + reproschema.models + +Submodules +---------- + +reproschema.cli module +---------------------- + +.. automodule:: reproschema.cli + :members: + :undoc-members: + :show-inheritance: + +reproschema.jsonldutils module +------------------------------ + +.. automodule:: reproschema.jsonldutils + :members: + :undoc-members: + :show-inheritance: + +reproschema.utils module +------------------------ + +.. automodule:: reproschema.utils + :members: + :undoc-members: + :show-inheritance: + +reproschema.validate module +--------------------------- + +.. automodule:: reproschema.validate + :members: + :undoc-members: + :show-inheritance: + +Module contents +--------------- + +.. automodule:: reproschema + :members: + :undoc-members: + :show-inheritance: diff --git a/reproschema/models/base.py b/reproschema/models/base.py index d76467f..9297e36 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -15,7 +15,6 @@ from attrs.validators import optional from .ui import UI -from .utils import DEFAULT_LANG from .utils import SchemaUtils @@ -43,16 +42,25 @@ def COMMON_SCHEMA_ORDER() -> list: @define(kw_only=True) class AdditionalNoteObj(SchemaUtils): + """A set of objects to define notes in a field. + + For example, most Redcap and NDA data dictionaries have notes + for each item which needs to be captured in reproschema. + """ + + #: An element to define the column name where the note was taken from column: Optional[str] = field( factory=(str), converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) + #: An element to define the source (eg. RedCap, NDA) where the note was taken from. source: Optional[str] = field( factory=(str), converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) + #: The value for each option in choices or in additionalNotesObj value: Any = field(default=None) def __attrs_post_init__(self) -> None: @@ -69,12 +77,15 @@ def __attrs_post_init__(self) -> None: @define(kw_only=True) class Message(SchemaUtils): + """An object to define messages in an activity or protocol.""" + #: A JavaScript expression to compute a score from other variables. jsExpression: Optional[str] = field( factory=(str), converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) + #: The message to be conditionally displayed for an item. message: Optional[str] = field( factory=(str), converter=default_if_none(default=""), # type: ignore diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 8a75d08..f79cc41 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -90,6 +90,13 @@ def __init__( def set_question( self, question: Optional[Union[str, dict]] = None, lang: Optional[str] = None ) -> None: + """_summary_ + + :param question: _description_, defaults to None + :type question: Optional[Union[str, dict]], optional + :param lang: _description_, defaults to None + :type lang: Optional[str], optional + """ if question is None: question = self.question @@ -107,9 +114,6 @@ def set_question( self.update() - """ - CREATE DIFFERENT ITEMS - """ # TODO: items not yet covered # audioCheck: AudioCheck/AudioCheck.vue # audioRecord: WebAudioRecord/Audio.vue @@ -122,7 +126,13 @@ def set_question( # static: Static/Static.vue # StaticReadOnly: Static/Static.vue - def set_input_type(self, response_options: ResponseOption = None) -> None: + def set_input_type(self, response_options: Optional[ResponseOption] = None) -> None: + """Set the input type of the item in the UI and the ResponseOptions objects. + + :param response_options: _description_, defaults to None + :type response_options: Optional[ResponseOption], optional + :raises ValueError: When the input type is not one of the supported values. + """ SUPPORTED_TYPES = ( "text", @@ -192,6 +202,7 @@ def set_input_type(self, response_options: ResponseOption = None) -> None: self.response_options.choices = URL elif self.inputType in ["radio", "select", "slider"]: + # TODO make it more general to be able to pass response_options for all input types if response_options is not None: if self.inputType in ["slider"]: response_options.multipleChoice = False @@ -203,8 +214,8 @@ def set_input_type(self, response_options: ResponseOption = None) -> None: """ def set_response_options(self) -> None: - """ - Passes the content of the response options to the schema of the item. + """Pass the content of the response options object to the schema of the item. + Also removes some "unnecessary" fields. """ self.response_options.update() @@ -216,8 +227,9 @@ def set_response_options(self) -> None: self.schema["responseOptions"] = self.response_options.schema def unset(self, keys) -> None: - """ - Mostly used to remove some empty keys from the schema. Rarely used. + """Remove empty keys from the schema. + + Rarely used. """ for i in keys: self.schema.pop(i, None) diff --git a/reproschema/models/response_options.py b/reproschema/models/response_options.py index 18f4c0f..84604b8 100644 --- a/reproschema/models/response_options.py +++ b/reproschema/models/response_options.py @@ -20,8 +20,14 @@ @define(kw_only=True) class unitOption(SchemaUtils): + """ + An object to represent a human displayable name, + alongside the more formal value for units. + """ + #: The value for each option in choices or in additionalNotesObj value: Any = field(default=None) + #: The preferred label. prefLabel: Optional[Union[str, Dict[str, str]]] = field( factory=(dict), converter=default_if_none(default={}), # type: ignore @@ -55,13 +61,17 @@ def set_pref_label( @define(kw_only=True) class Choice(SchemaUtils): + """An object to describe a response option.""" + #: The name of the item. name: Optional[Union[str, Dict[str, str]]] = field( default=None, converter=default_if_none(default=""), validator=optional(instance_of((str, dict))), ) + #: The value for each option in choices or in additionalNotesObj value: Any = field(default=None) + #: An image of the item. This can be a URL or a fully described ImageObject. image: Optional[Union[str, Dict[str, str]]] = field( default=None, validator=optional(instance_of((str, dict))) ) @@ -82,9 +92,6 @@ def __attrs_post_init__(self) -> None: self.sort_schema() self.drop_empty_values_from_schema() - def update(self) -> None: - super().update() - @define(kw_only=True) class ResponseOption(SchemaUtils): @@ -131,67 +138,67 @@ def _default_context(self) -> str: VERSION = self.schemaVersion or DEFAULT_VERSION() return URL + VERSION + "/contexts/generic" + #: The type of the response of an item. For example, string, integer, etc. valueType: str = field( factory=(str), converter=default_if_none(default=""), # type: ignore validator=optional(in_(SUPPORTED_VALUE_TYPES)), ) - + #: List the available options for response of the Field item. choices: Optional[Union[str, List[Choice]]] = field( factory=(list), converter=default_if_none(default=[]), # type: ignore validator=optional(instance_of((str, list))), ) - + #: Indicates if response for the Field item has one or more answer. multipleChoice: Optional[bool] = field( default=None, validator=optional(instance_of(bool)), ) - + #: The lower value of some characteristic or property. minValue: Optional[int] = field( default=None, validator=optional(instance_of(int)), ) - + #: The upper value of some characteristic or property. maxValue: Optional[int] = field( default=None, validator=optional(instance_of(int)), ) - + #: A list to represent a human displayable name alongside the more formal value for units. unitOptions: Optional[list] = field( default=None, converter=default_if_none(default=[]), # type: ignore validator=optional(instance_of(list)), ) - + #: The unit of measurement given using the UN/CEFACT Common Code (3 characters) or a URL. + # Other codes than the UN/CEFACT Common Code may be used with a prefix followed by a colon. unitCode: Optional[str] = field( default=None, converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) - + #: Indicates what type of datum the response is + # (e.g. range,count,scalar etc.) for the Field item. datumType: Optional[str] = field( default=None, converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) - + # Technically not in the schema, but useful for the UI maxLength: Optional[int] = field( default=None, validator=optional(instance_of(int)), ) """ + Non schema based attributes: OPTIONAL Those attributes help with file management and with printing json files with standardized key orders + """ - # lang: Optional[str] = field( - # default=None, - # converter=default_if_none(default=DEFAULT_LANG()), # type: ignore - # validator=optional(instance_of(str)), - # ) output_dir: Optional[Union[str, Path]] = field( default=None, @@ -232,19 +239,30 @@ def __attrs_post_init__(self) -> None: "maxLength", ] + """ + SETTERS: + """ + def set_defaults(self) -> None: self.schema["@type"] = self.at_type self.set_filename() def set_valueType(self, value: str = None) -> None: + """_summary_ + + :param value: _description_, defaults to None + :type value: str, optional + """ if value is not None: self.valueType = f"xsd:{value}" self.update() - def values_all_options(self) -> List[Any]: - return [i["value"] for i in self.choices if "value" in i] - def set_min(self, value: int = None) -> None: + """_summary_ + + :param value: _description_, defaults to None + :type value: int, optional + """ if value is not None: self.minValue = value elif len(self.choices) > 1: @@ -254,7 +272,11 @@ def set_min(self, value: int = None) -> None: self.minValue = min(all_values) def set_max(self, value: int = None) -> None: + """_summary_ + :param value: _description_, defaults to None + :type value: int, optional + """ if value is not None: self.maxValue = value @@ -266,12 +288,24 @@ def set_max(self, value: int = None) -> None: self.update() + def values_all_options(self) -> List[Any]: + return [i["value"] for i in self.choices if "value" in i] + def add_choice( self, name: Optional[str] = None, value: Any = None, lang: Optional[str] = None, ) -> None: + """Add a response choice. + + :param name: _description_, defaults to None + :type name: Optional[str], optional + :param value: _description_, defaults to None + :type value: Any, optional + :param lang: _description_, defaults to None + :type lang: Optional[str], optional + """ if lang is None: lang = self.lang # TODO replace existing choice if already set @@ -279,9 +313,11 @@ def add_choice( self.set_max() self.set_min() - def update(self) -> None: - """Updates the schema content based on the attributes.""" + """ + MISC + """ + def update(self) -> None: self.schema["@id"] = self.at_id self.schema["@type"] = self.at_type self.schema["@context"] = self.at_context diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index a23ef2d..5144ff7 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -121,6 +121,13 @@ def __attrs_post_init__(self) -> None: self.update() def append(self, obj, variableName: Optional[str] = None) -> None: + """Append Field or Activity to the UI schema. + + :param obj: _description_ + :type obj: _type_ + :param variableName: _description_, defaults to None + :type variableName: Optional[str], optional + """ this_property = AdditionalProperty( variableName=variableName, @@ -172,50 +179,64 @@ def update(self) -> None: kw_only=True, ) class AdditionalProperty(SchemaUtils): + """An object to describe the various properties added to assessments and fields.""" + #: The name used to represent an item. variableName: Optional[str] = field( default=None, converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) + #: A pointer to the node describing the item. isAbout: Optional[str] = field( default=None, converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) + #: The preferred label. prefLabel: Optional[Union[str, Dict[str, str]]] = field( default=None, converter=default_if_none(default={}), # type: ignore validator=optional(instance_of((str, dict))), ) + #: An element to describe (by boolean or conditional statement) + # visibility conditions of items in an assessment. isVis: Optional[bool] = field( default=None, validator=optional(instance_of(bool)), ) + #: Whether the property must be filled in to complete the action. requiredValue: Optional[bool] = field( default=None, validator=optional(instance_of(bool)), ) + #: List of items indicating properties allowed. allow: Optional[List[str]] = field( factory=list, converter=default_if_none(default=[]), # type: ignore validator=optional(instance_of(list)), ) + #: An element to limit the duration (uses ISO 8601) + # this activity is allowed to be completed by once activity is available. limit: Optional[str] = field( default=None, converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) + #: Defines number of times the item is allowed to be redone. maxRetakes: Optional[str] = field( default=None, converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) + #: Present activity/item within some random offset of activity available time up + # to the maximum specified by this ISO 8601 duration randomMaxDelay: Optional[str] = field( default=None, converter=default_if_none(default=""), # type: ignore validator=optional(instance_of(str)), ) + #: An element to set make activity available/repeat info using ISO 8601 repeating interval format. schedule: Optional[str] = field( default=None, converter=default_if_none(default=""), # type: ignore diff --git a/reproschema/models/utils.py b/reproschema/models/utils.py index 4cf1243..ba2b248 100644 --- a/reproschema/models/utils.py +++ b/reproschema/models/utils.py @@ -20,6 +20,7 @@ def DEFAULT_LANG() -> str: @define(kw_only=True) class SchemaUtils: + #: specifies the order of keys in the output file schema_order: Optional[list] = field( factory=(list), converter=default_if_none(default=[]), # type: ignore @@ -31,6 +32,7 @@ class SchemaUtils: converter=default_if_none(default={}), # type: ignore validator=optional(instance_of(dict)), ) + #: default language for the schema lang: Optional[str] = field( default=None, converter=default_if_none(default=DEFAULT_LANG()), # type: ignore @@ -45,6 +47,7 @@ def sort_schema(self) -> None: return reordered_dict def drop_empty_values_from_schema(self) -> None: + """Remove any empty keys from the schema to avoid cluttering output files.""" tmp = dict(self.schema) for key in tmp: if self.schema[key] in [{}, [], "", None]: @@ -86,11 +89,22 @@ def from_file(cls, filepath: Union[str, Path]): def reorder_dict_skip_missing(old_dict: Dict, key_list: List) -> OrderedDict: + """Reorders dictionary according to ``key_list``. + + Removing any key with no associated value, or that is not in ``key_list`` + + This is useful to ensure that order of keys in output files is the same across files. + + :param old_dict: _description_ + :type old_dict: Dict + + :param key_list: _description_ + :type key_list: List + + :return: _description_ + :rtype: OrderedDict """ - reorders dictionary according to ``key_list`` - removing any key with no associated value - or that is not in the key list - """ + return OrderedDict( (k, old_dict[k]) for k in key_list diff --git a/setup.cfg b/setup.cfg index 5d1061d..c8a2980 100644 --- a/setup.cfg +++ b/setup.cfg @@ -56,6 +56,8 @@ doc = sphinxcontrib-apidoc ~= 0.3.0 sphinxcontrib-napoleon sphinxcontrib-versioning + sphinxcontrib-mermaid + myst-parser docs = %(doc)s test = From 4d509642d130588285c45ef2a43fc1c99d752d67 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 11 May 2024 13:16:31 -0400 Subject: [PATCH 67/69] adapt for tests --- reproschema/models/base.py | 5 ++--- reproschema/models/item.py | 2 +- reproschema/models/protocol.py | 1 + reproschema/models/ui.py | 5 +---- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/reproschema/models/base.py b/reproschema/models/base.py index 9297e36..cab8ff9 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -261,11 +261,10 @@ def _default_context(self) -> str: default=UI(at_type="reproschema:Field"), validator=optional(instance_of(UI)), ) - - visible: Optional[bool] = field( + visible: bool | str | None = field( factory=(bool), converter=default_if_none(default=True), # type: ignore - validator=optional(instance_of(bool)), + validator=optional(instance_of((bool, str))), ) required: Optional[bool] = field( factory=(bool), diff --git a/reproschema/models/item.py b/reproschema/models/item.py index f79cc41..ecdc4b7 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -30,7 +30,7 @@ def __init__( video: Optional[Union[str, Dict[str, str]]] = None, preamble: Optional[str] = None, additionalNotesObj: List[Dict[str, Any]] = None, - visible: Optional[bool] = True, + visible: bool | str = True, required: Optional[bool] = False, skippable: Optional[bool] = True, read_only: Optional[bool] = None, diff --git a/reproschema/models/protocol.py b/reproschema/models/protocol.py index 881c38c..30c97cc 100644 --- a/reproschema/models/protocol.py +++ b/reproschema/models/protocol.py @@ -68,6 +68,7 @@ def __init__( ) super().set_defaults() self.ui.shuffle = False + self.ui.update() self.update() def append_activity(self, activity: Activity): diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index 5144ff7..134a924 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -201,10 +201,7 @@ class AdditionalProperty(SchemaUtils): ) #: An element to describe (by boolean or conditional statement) # visibility conditions of items in an assessment. - isVis: Optional[bool] = field( - default=None, - validator=optional(instance_of(bool)), - ) + isVis: Optional[Union[str, bool]] = field(default=None, validator=optional(instance_of((bool, str)))) #: Whether the property must be filled in to complete the action. requiredValue: Optional[bool] = field( default=None, From 70bd3afa3b230341a551e6daa193cde372769877 Mon Sep 17 00:00:00 2001 From: Remi Gau Date: Sat, 11 May 2024 13:17:07 -0400 Subject: [PATCH 68/69] lint --- .pre-commit-config.yaml | 6 +++--- reproschema/models/base.py | 1 - reproschema/models/tests/test_activity.py | 1 - reproschema/models/tests/test_protocol.py | 1 - reproschema/models/ui.py | 4 +++- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 12b8abe..7e70bcc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.3.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -10,11 +10,11 @@ repos: - id: check-added-large-files - repo: https://github.com/psf/black - rev: 22.6.0 + rev: 24.4.2 hooks: - id: black - repo: https://github.com/asottile/reorder_python_imports - rev: "v3.8.2" + rev: "v3.12.0" hooks: - id: reorder-python-imports diff --git a/reproschema/models/base.py b/reproschema/models/base.py index cab8ff9..2a2116d 100644 --- a/reproschema/models/base.py +++ b/reproschema/models/base.py @@ -105,7 +105,6 @@ def __attrs_post_init__(self) -> None: @define(kw_only=True) class SchemaBase(SchemaUtils): - """ Schema based attributes: REQUIRED """ diff --git a/reproschema/models/tests/test_activity.py b/reproschema/models/tests/test_activity.py index 715c7d2..6628b08 100644 --- a/reproschema/models/tests/test_activity.py +++ b/reproschema/models/tests/test_activity.py @@ -12,7 +12,6 @@ def test_default(): - """ FYI: The default activity does not conform to the schema so `reproschema validate` will complain if you run it on this diff --git a/reproschema/models/tests/test_protocol.py b/reproschema/models/tests/test_protocol.py index 2afeb65..40a69d9 100644 --- a/reproschema/models/tests/test_protocol.py +++ b/reproschema/models/tests/test_protocol.py @@ -13,7 +13,6 @@ def test_default(): - """ FYI: The default protocol does not conform to the schema so `reproschema validate` will complain if you run it in this diff --git a/reproschema/models/ui.py b/reproschema/models/ui.py index 134a924..aba666c 100644 --- a/reproschema/models/ui.py +++ b/reproschema/models/ui.py @@ -201,7 +201,9 @@ class AdditionalProperty(SchemaUtils): ) #: An element to describe (by boolean or conditional statement) # visibility conditions of items in an assessment. - isVis: Optional[Union[str, bool]] = field(default=None, validator=optional(instance_of((bool, str)))) + isVis: Optional[Union[str, bool]] = field( + default=None, validator=optional(instance_of((bool, str))) + ) #: Whether the property must be filled in to complete the action. requiredValue: Optional[bool] = field( default=None, From c75504a5f1246f11f73f8eeabdd4eb0330f5bd18 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sat, 11 May 2024 17:19:33 +0000 Subject: [PATCH 69/69] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- reproschema/models/item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reproschema/models/item.py b/reproschema/models/item.py index 6b5ba04..ecdc4b7 100644 --- a/reproschema/models/item.py +++ b/reproschema/models/item.py @@ -238,4 +238,4 @@ def write(self, output_dir=None) -> None: if output_dir is None: output_dir = self.output_dir self.set_response_options() - super().write(output_dir) \ No newline at end of file + super().write(output_dir)