Skip to content
Snippets Groups Projects
Verified Commit edc0986f authored by Daniel Hornung's avatar Daniel Hornung
Browse files

ENH: jsex: multiple choice option for "do_not_create" list props.

parent fb1f78d8
No related branches found
No related tags found
2 merge requests!89ENH: JsonSchemaExporter accepts do_not_create parameter.,!85more jsonschema export
Pipeline #43515 failed
......@@ -25,7 +25,7 @@ The scope of this json schema is the automatic generation of user interfaces.
"""
from collections import OrderedDict
from typing import Any, Dict, Iterable, List, Optional, Union
from typing import Any, Dict, Iterable, List, Optional, Tuple, Union
import linkahead as db
from linkahead.common.datatype import get_list_datatype, is_list_datatype
......@@ -43,6 +43,7 @@ class JsonSchemaExporter:
do_not_create: List[str] = None,
do_not_retrieve: List[str] = None,
no_remote: bool = False,
multiple_choice: List[str] = None,
):
"""Set up a JsonSchemaExporter, which can then be applied on RecordTypes.
......@@ -75,6 +76,10 @@ class JsonSchemaExporter:
parameter, the behavior is undefined.
no_remote : bool
If True, do not attempt to connect to a LinkAhead server at all. Default is False.
multiple_choice : list[str], optional
A list of reference Property names which shall be denoted as multiple choice properties.
This means that each option in this property may be selected at most once. This is not
implemented yet if the Property is not in ``do_not_create`` as well.
"""
if not additional_options_for_text_props:
additional_options_for_text_props = {}
......@@ -82,6 +87,8 @@ class JsonSchemaExporter:
do_not_create = []
if not do_not_retrieve:
do_not_retrieve = []
if not multiple_choice:
multiple_choice = []
self._additional_properties = additional_properties
self._name_property_for_new_records = name_property_for_new_records
......@@ -91,6 +98,8 @@ class JsonSchemaExporter:
self._do_not_create = do_not_create
self._do_not_retrieve = do_not_retrieve
self._no_remote = no_remote
self._multiple_choice = multiple_choice
self._rjsf_uischema: dict = {}
@staticmethod
def _make_required_list(rt: db.RecordType):
......@@ -106,15 +115,15 @@ class JsonSchemaExporter:
return required_list
def _make_segment_from_prop(self, prop: db.Property):
"""Return the JSON Schema segment for the given property
def _make_segment_from_prop(self, prop: db.Property) -> Tuple[OrderedDict, dict]:
"""Return the JSON Schema and ui schema segments for the given property.
Parameters
----------
prop : db.Property
The property to be transformed.
"""
ui_schema: dict = {}
if prop.datatype == db.TEXT or prop.datatype == db.DATETIME:
text_format = None
text_pattern = None
......@@ -131,7 +140,7 @@ class JsonSchemaExporter:
# options list.
text_format = ["date", "date-time"]
return self._make_text_property(prop.description, text_format, text_pattern)
return self._make_text_property(prop.description, text_format, text_pattern), ui_schema
json_prop = OrderedDict()
if prop.description:
......@@ -154,13 +163,21 @@ class JsonSchemaExporter:
json_prop["type"] = "array"
list_element_prop = db.Property(
name=prop.name, datatype=get_list_datatype(prop.datatype, strict=True))
json_prop["items"] = self._make_segment_from_prop(list_element_prop)
json_prop["items"], inner_ui_schema = self._make_segment_from_prop(list_element_prop)
if prop.name in self._multiple_choice and prop.name in self._do_not_create:
json_prop["uniqueItems"] = True
ui_schema["ui:widget"] = "checkboxes"
ui_schema["ui:options"] = {"inline": True}
if inner_ui_schema:
ui_schema["items"] = inner_ui_schema
elif prop.is_reference():
if prop.datatype == db.REFERENCE:
# No Record creation since no RT is specified and we don't know what
# schema to use, so only enum of all Records and all Files.
values = self._retrieve_enum_values("RECORD") + self._retrieve_enum_values("FILE")
json_prop["enum"] = values
if prop.name in self._multiple_choice:
json_prop["uniqueItems"] = True
elif prop.datatype == db.FILE:
json_prop["type"] = "string"
json_prop["format"] = "data-url"
......@@ -181,7 +198,9 @@ class JsonSchemaExporter:
else:
rt = db.execute_query(f"FIND RECORDTYPE WITH name='{prop_name}'",
unique=True)
subschema = self._make_segment_from_recordtype(rt)
subschema, ui_schema = self._make_segment_from_recordtype(rt)
# if inner_ui_schema:
# ui_schema = inner_ui_schema
if values:
subschema["title"] = "Create new"
json_prop["oneOf"] = [
......@@ -198,7 +217,7 @@ class JsonSchemaExporter:
raise ValueError(
f"Unknown or no property datatype. Property {prop.name} with type {prop.datatype}")
return json_prop
return json_prop, ui_schema
@staticmethod
def _make_text_property(description="", text_format=None, text_pattern=None):
......@@ -245,16 +264,20 @@ class JsonSchemaExporter:
return vals
def _make_segment_from_recordtype(self, rt: db.RecordType):
"""Return a Json schema segment for the given RecordType.
def _make_segment_from_recordtype(self, rt: db.RecordType) -> Tuple[OrderedDict, dict]:
"""Return Json schema and uischema segments for the given RecordType.
"""
schema: dict[str, Any] = {
schema: OrderedDict[str, Any] = OrderedDict({
"type": "object"
}
})
ui_schema = {}
schema["required"] = self._make_required_list(rt)
schema["additionalProperties"] = self._additional_properties
if rt.name:
schema["title"] = rt.name
props = OrderedDict()
if self._name_property_for_new_records:
props["name"] = self._make_text_property("The name of the Record to be created")
......@@ -269,13 +292,15 @@ class JsonSchemaExporter:
"Creating a schema for multi-properties is not specified. "
f"Property {prop.name} occurs more than once."
)
props[prop.name] = self._make_segment_from_prop(prop)
props[prop.name], inner_ui_schema = self._make_segment_from_prop(prop)
if inner_ui_schema:
ui_schema[prop.name] = inner_ui_schema
schema["properties"] = props
return schema
return schema, ui_schema
def recordtype_to_json_schema(self, rt: db.RecordType):
def recordtype_to_json_schema(self, rt: db.RecordType, rjsf_uischema: Optional[dict] = None):
"""Create a jsonschema from a given RecordType that can be used, e.g., to
validate a json specifying a record of the given type.
......@@ -283,6 +308,9 @@ class JsonSchemaExporter:
----------
rt : RecordType
The RecordType from which a json schema will be created.
rjsf_uischema : dict, optional
If given, uiSchema definitions for react-jsonschema-forms will be appended to this dict
at ``[rt.name]``.
Returns
-------
......@@ -292,10 +320,13 @@ class JsonSchemaExporter:
if rt is None:
raise ValueError(
"recordtype_to_json_schema(...) cannot be called with a `None` RecordType.")
schema = self._make_segment_from_recordtype(rt)
schema["$schema"] = "https://json-schema.org/draft/2019-09/schema"
if rt.name:
schema["title"] = rt.name
if rjsf_uischema is None:
rjsf_uischema = {}
schema, inner_uischema = self._make_segment_from_recordtype(rt)
if inner_uischema:
rjsf_uischema[rt.name] = inner_uischema
schema["$schema"] = "https://json-schema.org/draft/2020-12/schema"
if rt.description:
schema["description"] = rt.description
......@@ -310,6 +341,8 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T
do_not_create: List[str] = None,
do_not_retrieve: List[str] = None,
no_remote: bool = False,
multiple_choice: List[str] = None,
rjsf_uischema: Optional[dict] = None,
):
"""Create a jsonschema from a given RecordType that can be used, e.g., to
validate a json specifying a record of the given type.
......@@ -338,16 +371,22 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T
description of the corresponding schema entry. If set to false, an
additional `unit` key is added to the schema itself which is purely
annotational and ignored, e.g., in validation. Default is True.
do_not_create : list[str]
do_not_create : list[str], optional
A list of RedcordType names, for which there should be no option
to create them. Instead, only the choice of existing elements should
be given.
do_not_retrieve : list[str]
do_not_retrieve : list[str], optional
A list of RedcordType names, for which no Records shall be retrieved. Instead, only an
object description should be given. If this list overlaps with the `do_not_create`
parameter, the behavior is undefined.
no_remote : bool
no_remote : bool, optional
If True, do not attempt to connect to a LinkAhead server at all. Default is False.
multiple_choice : list[str], optional
A list of reference Property names which shall be denoted as multiple choice properties.
This means that each option in this property may be selected at most once. This is not
implemented yet if the Property is not in ``do_not_create`` as well.
rjsf_uischema : dict, optional
If given, uiSchema definitions for react-jsonschema-forms will be appended to this dict.
Returns
-------
......@@ -365,8 +404,9 @@ def recordtype_to_json_schema(rt: db.RecordType, additional_properties: bool = T
do_not_create=do_not_create,
do_not_retrieve=do_not_retrieve,
no_remote=no_remote,
multiple_choice=multiple_choice,
)
return exporter.recordtype_to_json_schema(rt)
return exporter.recordtype_to_json_schema(rt, rjsf_uischema=rjsf_uischema)
def make_array(schema: dict) -> dict:
......@@ -397,7 +437,7 @@ out : dict
result = {
"type": "array",
"items": schema,
"$schema": "https://json-schema.org/draft/2019-09/schema",
"$schema": "https://json-schema.org/draft/2020-12/schema",
}
return result
......
......@@ -81,6 +81,19 @@ def _mock_execute_query(query_string, unique=False, **kwargs):
return referencing_type_records # wrong types, but who cares for the test?
elif query_string == "FIND RECORDTYPE WITH name='RT1'" and unique is True:
return RT1
elif query_string == "FIND RECORDTYPE WITH name='RT21'" and unique is True:
model_str = """
RT1:
RT21:
obligatory_properties:
RT1:
datatype: LIST<RT1>
RT3:
obligatory_properties:
RT21:
"""
model = parse_model_from_string(model_str)
return model.get_deep("RT21")
elif query_string == "SELECT name, id FROM RECORD":
return all_records
elif query_string == "SELECT name, id FROM FILE":
......@@ -602,6 +615,7 @@ RT2:
"some_date"
],
"additionalProperties": true,
"title": "RT1",
"properties": {
"some_date": {
"description": "Just some date",
......@@ -617,8 +631,7 @@ RT2:
]
}
},
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "RT1",
"$schema": "https://json-schema.org/draft/2020-12/schema",
"description": "some description"
}"""
# Second test: with reference
......@@ -630,6 +643,7 @@ RT2:
"RT1"
],
"additionalProperties": true,
"title": "RT2",
"properties": {
"RT1": {
"description": "some description",
......@@ -647,6 +661,7 @@ RT2:
"some_date"
],
"additionalProperties": true,
"title": "Create new",
"properties": {
"some_date": {
"description": "Just some date",
......@@ -661,14 +676,12 @@ RT2:
}
]
}
},
"title": "Create new"
}
}
]
}
},
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "RT2"
"$schema": "https://json-schema.org/draft/2020-12/schema"
}"""
# Third test: Reference prop shall be only existing references, no option to create new ones.
......@@ -679,6 +692,7 @@ RT2:
"RT1"
],
"additionalProperties": true,
"title": "RT2",
"properties": {
"RT1": {
"description": "some description",
......@@ -688,8 +702,7 @@ RT2:
]
}
},
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "RT2"
"$schema": "https://json-schema.org/draft/2020-12/schema"
}"""
......@@ -820,6 +833,53 @@ RT3:
assert (schema_noexist_noremote["properties"]["NoRecords"].get("properties")
== OrderedDict([('some_text', {'type': 'string'})]))
schema_noexist_noretrieve = rtjs(model.get_deep("RT2"), do_not_retrieve=["RT1"])
uischema = {}
schema_noexist_noretrieve = rtjs(model.get_deep("RT2"), do_not_retrieve=["RT1"],
rjsf_uischema=uischema)
assert schema_noexist_noretrieve["properties"]["RT1"].get("type") == "object"
assert "some_date" in schema_noexist_noretrieve["properties"]["RT1"].get("properties")
assert not uischema
@patch("linkahead.execute_query", new=Mock(side_effect=_mock_execute_query))
def test_multiple_choice():
"""Multiple choice is mostyly a matter of UI."""
model_str = """
RT1:
RT21:
obligatory_properties:
RT1:
datatype: LIST<RT1>
RT3:
obligatory_properties:
RT21:
RT4:
obligatory_properties:
RT21:
datatype: LIST<RT21>
"""
model = parse_model_from_string(model_str)
# generate a multiple choice, in first level
uischema = {}
schema = rtjs(model.get_deep("RT21"), additional_properties=False, do_not_create=["RT1"],
multiple_choice=["RT1"], rjsf_uischema=uischema)
assert schema["properties"]["RT1"]["uniqueItems"] is True
assert (str(uischema["RT21"]) ==
"{'RT1': {'ui:widget': 'checkboxes', 'ui:options': {'inline': True}}}")
# second level
uischema = {}
schema = rtjs(model.get_deep("RT3"), additional_properties=False, do_not_create=["RT1"],
multiple_choice=["RT1"], rjsf_uischema=uischema)
assert schema["properties"]["RT21"]["properties"]["RT1"]["uniqueItems"] is True
assert (str(uischema["RT3"]) ==
"{'RT21': {'RT1': {'ui:widget': 'checkboxes', 'ui:options': {'inline': True}}}}")
# second level with lists
uischema = {}
schema = rtjs(model.get_deep("RT4"), additional_properties=False, do_not_create=["RT1"],
multiple_choice=["RT1"], rjsf_uischema=uischema)
assert schema["properties"]["RT21"]["items"]["properties"]["RT1"]["uniqueItems"] is True
assert (str(uischema["RT4"]) ==
"{'RT21': {'items': {'RT1': {'ui:widget': 'checkboxes', "
"'ui:options': {'inline': True}}}}}")
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment