girder-jsonforms 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- girder_jsonforms/__init__.py +83 -0
- girder_jsonforms/lib/__init__.py +0 -0
- girder_jsonforms/lib/google_drive.py +157 -0
- girder_jsonforms/lib/jq.py +134 -0
- girder_jsonforms/models/__init__.py +0 -0
- girder_jsonforms/models/entry.py +245 -0
- girder_jsonforms/models/form.py +287 -0
- girder_jsonforms/rest/__init__.py +0 -0
- girder_jsonforms/rest/entry.py +126 -0
- girder_jsonforms/rest/form.py +362 -0
- girder_jsonforms/web_client/dist/girder-plugin-jsonforms.umd.cjs +88 -0
- girder_jsonforms/web_client/dist/style.css +1 -0
- girder_jsonforms-2.0.0.dist-info/LICENSE +27 -0
- girder_jsonforms-2.0.0.dist-info/METADATA +33 -0
- girder_jsonforms-2.0.0.dist-info/RECORD +18 -0
- girder_jsonforms-2.0.0.dist-info/WHEEL +5 -0
- girder_jsonforms-2.0.0.dist-info/entry_points.txt +2 -0
- girder_jsonforms-2.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import io
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
import jsonschema
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
import requests
|
|
9
|
+
from girder.constants import AccessType
|
|
10
|
+
from girder.models.model_base import AccessControlledModel
|
|
11
|
+
|
|
12
|
+
from ..lib.jq import (
|
|
13
|
+
convert_to_jq_notation,
|
|
14
|
+
find_key_paths,
|
|
15
|
+
get_value,
|
|
16
|
+
parse_jq_notation,
|
|
17
|
+
set_value,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Form(AccessControlledModel):
|
|
22
|
+
def initialize(self):
|
|
23
|
+
self.name = "form"
|
|
24
|
+
self.ensureIndices(["name"])
|
|
25
|
+
|
|
26
|
+
self.exposeFields(
|
|
27
|
+
level=AccessType.READ,
|
|
28
|
+
fields=(
|
|
29
|
+
"_id",
|
|
30
|
+
"name",
|
|
31
|
+
"description",
|
|
32
|
+
"entryFileName",
|
|
33
|
+
"schema",
|
|
34
|
+
"created",
|
|
35
|
+
"updated",
|
|
36
|
+
"gdriveFolderId",
|
|
37
|
+
"folderId",
|
|
38
|
+
"serialize",
|
|
39
|
+
"pathTemplate",
|
|
40
|
+
"uniqueField",
|
|
41
|
+
),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def validate(self, doc):
|
|
45
|
+
return doc
|
|
46
|
+
|
|
47
|
+
def create(
|
|
48
|
+
self,
|
|
49
|
+
name,
|
|
50
|
+
description,
|
|
51
|
+
schema,
|
|
52
|
+
creator,
|
|
53
|
+
folder=None,
|
|
54
|
+
pathTemplate=None,
|
|
55
|
+
entryFileName=None,
|
|
56
|
+
gdriveFolderId=None,
|
|
57
|
+
serialize=False,
|
|
58
|
+
uniqueField=None,
|
|
59
|
+
):
|
|
60
|
+
now = datetime.datetime.utcnow()
|
|
61
|
+
|
|
62
|
+
form = {
|
|
63
|
+
"name": name,
|
|
64
|
+
"description": description,
|
|
65
|
+
"schema": schema,
|
|
66
|
+
"folderId": None,
|
|
67
|
+
"gdriveFolderId": gdriveFolderId,
|
|
68
|
+
"pathTemplate": pathTemplate,
|
|
69
|
+
"entryFileName": entryFileName or "entry.json",
|
|
70
|
+
"serialize": serialize,
|
|
71
|
+
"created": now,
|
|
72
|
+
"updated": now,
|
|
73
|
+
"uniqueField": uniqueField or "sampleId",
|
|
74
|
+
}
|
|
75
|
+
if folder:
|
|
76
|
+
form["folderId"] = folder["_id"]
|
|
77
|
+
|
|
78
|
+
return self.save(form)
|
|
79
|
+
|
|
80
|
+
def update(
|
|
81
|
+
self,
|
|
82
|
+
form,
|
|
83
|
+
name,
|
|
84
|
+
description,
|
|
85
|
+
schema,
|
|
86
|
+
folder=None,
|
|
87
|
+
pathTemplate=None,
|
|
88
|
+
entryFileName=None,
|
|
89
|
+
gdriveFolderId=None,
|
|
90
|
+
serialize=None,
|
|
91
|
+
uniqueField=None,
|
|
92
|
+
):
|
|
93
|
+
now = datetime.datetime.utcnow()
|
|
94
|
+
|
|
95
|
+
form["name"] = name
|
|
96
|
+
form["description"] = description
|
|
97
|
+
form["schema"] = schema
|
|
98
|
+
form["updated"] = now
|
|
99
|
+
form["pathTemplate"] = pathTemplate
|
|
100
|
+
|
|
101
|
+
if folder:
|
|
102
|
+
form["folderId"] = folder["_id"]
|
|
103
|
+
else:
|
|
104
|
+
form["folderId"] = None
|
|
105
|
+
|
|
106
|
+
if entryFileName:
|
|
107
|
+
form["entryFileName"] = entryFileName
|
|
108
|
+
|
|
109
|
+
if gdriveFolderId:
|
|
110
|
+
form["gdriveFolderId"] = gdriveFolderId
|
|
111
|
+
|
|
112
|
+
if serialize is not None:
|
|
113
|
+
form["serialize"] = serialize
|
|
114
|
+
|
|
115
|
+
if uniqueField:
|
|
116
|
+
form["uniqueField"] = uniqueField
|
|
117
|
+
|
|
118
|
+
return self.save(form)
|
|
119
|
+
|
|
120
|
+
def materialize(self, form, user):
|
|
121
|
+
from .entry import FormEntry
|
|
122
|
+
|
|
123
|
+
if form["schema"].startswith("http"):
|
|
124
|
+
form["schema"] = self._loadRemoteSchema(form["schema"])
|
|
125
|
+
else:
|
|
126
|
+
form["schema"] = json.loads(form["schema"])
|
|
127
|
+
|
|
128
|
+
for keyPath in find_key_paths(form["schema"], "enumSource"):
|
|
129
|
+
value = get_value(form["schema"], keyPath)
|
|
130
|
+
if isinstance(value, str) and value.startswith("girder.formId:"):
|
|
131
|
+
formId = value.split(":")[1]
|
|
132
|
+
source_form = self.load(
|
|
133
|
+
formId, level=AccessType.READ, user=user, exc=True
|
|
134
|
+
)
|
|
135
|
+
enum_source = {
|
|
136
|
+
"source": [],
|
|
137
|
+
"title": "{{item.title}}",
|
|
138
|
+
"value": "{{item.value}}",
|
|
139
|
+
}
|
|
140
|
+
for entry in (
|
|
141
|
+
FormEntry()
|
|
142
|
+
.find(
|
|
143
|
+
{"formId": source_form["_id"]},
|
|
144
|
+
fields={"_id": 1, source_form["uniqueField"]: 1, "data": 1},
|
|
145
|
+
)
|
|
146
|
+
.sort([(source_form["uniqueField"], 1)])
|
|
147
|
+
):
|
|
148
|
+
enum_source["source"].append(
|
|
149
|
+
{
|
|
150
|
+
"value": str(entry["_id"]),
|
|
151
|
+
"title": entry["data"][source_form["uniqueField"]],
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
set_value(form["schema"], keyPath, [enum_source])
|
|
155
|
+
|
|
156
|
+
return form
|
|
157
|
+
|
|
158
|
+
def export(self, form, export_format):
|
|
159
|
+
from .entry import FormEntry
|
|
160
|
+
|
|
161
|
+
entries = [
|
|
162
|
+
convert_to_jq_notation(entry)
|
|
163
|
+
for entry in FormEntry()
|
|
164
|
+
.find(
|
|
165
|
+
{"formId": form["_id"]},
|
|
166
|
+
fields={"_id": 1, "data": 1},
|
|
167
|
+
)
|
|
168
|
+
.sort([("created", 1)])
|
|
169
|
+
]
|
|
170
|
+
buffer = io.BytesIO()
|
|
171
|
+
if export_format == "csv":
|
|
172
|
+
pd.DataFrame(entries).to_csv(buffer, index=False)
|
|
173
|
+
elif export_format == "xlsx":
|
|
174
|
+
pd.DataFrame(entries).to_excel(buffer, index=False)
|
|
175
|
+
return buffer
|
|
176
|
+
|
|
177
|
+
@staticmethod
|
|
178
|
+
def resolve_ref(ref, definitions):
|
|
179
|
+
ref_path = ref.lstrip("#/").split("/")
|
|
180
|
+
ref_value = definitions
|
|
181
|
+
for part in ref_path[1:]:
|
|
182
|
+
ref_value = ref_value.get(part, {})
|
|
183
|
+
return ref_value
|
|
184
|
+
|
|
185
|
+
def get_data_types(self, form):
|
|
186
|
+
schema = json.loads(form["schema"])
|
|
187
|
+
types = self.infer_column_types(
|
|
188
|
+
schema, definitions=schema.get("definitions", {})
|
|
189
|
+
)
|
|
190
|
+
return {"data." + key: value for key, value in types.items()}
|
|
191
|
+
|
|
192
|
+
def schema_type_to_pandas(self, typename):
|
|
193
|
+
if isinstance(typename, list):
|
|
194
|
+
for t in typename:
|
|
195
|
+
if t == "null":
|
|
196
|
+
continue
|
|
197
|
+
return self.schema_type_to_pandas(t)
|
|
198
|
+
if typename == "string":
|
|
199
|
+
return "object"
|
|
200
|
+
if typename == "integer":
|
|
201
|
+
return "int64"
|
|
202
|
+
if typename == "number":
|
|
203
|
+
return "float64"
|
|
204
|
+
if typename == "boolean":
|
|
205
|
+
return "bool"
|
|
206
|
+
|
|
207
|
+
def infer_column_types(self, schema, definitions=None, parent_key=""):
|
|
208
|
+
properties = {}
|
|
209
|
+
definitions = definitions or {}
|
|
210
|
+
|
|
211
|
+
if "properties" in schema:
|
|
212
|
+
for key, value in schema["properties"].items():
|
|
213
|
+
full_key = f"{parent_key}.{key}" if parent_key else key
|
|
214
|
+
|
|
215
|
+
if "$ref" in value:
|
|
216
|
+
value = self.resolve_ref(value["$ref"], definitions)
|
|
217
|
+
|
|
218
|
+
if (
|
|
219
|
+
"type" in value
|
|
220
|
+
and value["type"] != "object"
|
|
221
|
+
and value["type"] != "array"
|
|
222
|
+
):
|
|
223
|
+
properties[full_key] = self.schema_type_to_pandas(value["type"])
|
|
224
|
+
|
|
225
|
+
if value.get("type", "object") == "object":
|
|
226
|
+
properties.update(
|
|
227
|
+
self.infer_column_types(
|
|
228
|
+
value, definitions=definitions or None, parent_key=full_key
|
|
229
|
+
)
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
if value.get("type") == "array" and "items" in value:
|
|
233
|
+
properties[full_key] = "array"
|
|
234
|
+
properties.update(
|
|
235
|
+
self.infer_column_types(
|
|
236
|
+
value["items"],
|
|
237
|
+
definitions=definitions or None,
|
|
238
|
+
parent_key=full_key + "[]",
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
return properties
|
|
243
|
+
|
|
244
|
+
def import_entries(self, form, file_obj, file_type, dry_run=True):
|
|
245
|
+
from .entry import FormEntry
|
|
246
|
+
|
|
247
|
+
schema = json.loads(form["schema"])
|
|
248
|
+
io_buffer = io.BytesIO(file_obj.read())
|
|
249
|
+
if file_type == "csv":
|
|
250
|
+
entries = pd.read_csv(
|
|
251
|
+
io_buffer, na_values=["None", "nan"], dtype=self.get_data_types(form)
|
|
252
|
+
)
|
|
253
|
+
elif file_type == "xlsx":
|
|
254
|
+
entries = pd.read_excel(io_buffer)
|
|
255
|
+
|
|
256
|
+
for col in entries.columns:
|
|
257
|
+
if entries[col].dtype == "object":
|
|
258
|
+
entries[col] = entries[col].replace(np.nan, "")
|
|
259
|
+
|
|
260
|
+
parsed_entries = [
|
|
261
|
+
parse_jq_notation(entry) for entry in entries.to_dict(orient="records")
|
|
262
|
+
]
|
|
263
|
+
failed = new = updated = 0
|
|
264
|
+
for row in parsed_entries:
|
|
265
|
+
entry = row["data"]
|
|
266
|
+
try:
|
|
267
|
+
jsonschema.Draft7Validator(schema).validate(entry)
|
|
268
|
+
except jsonschema.ValidationError as e:
|
|
269
|
+
print(str(e))
|
|
270
|
+
failed += 1
|
|
271
|
+
continue
|
|
272
|
+
try:
|
|
273
|
+
unique_id = entry[form["uniqueField"]]
|
|
274
|
+
except KeyError:
|
|
275
|
+
failed += 1
|
|
276
|
+
continue
|
|
277
|
+
if existing_entry := FormEntry().findOne(
|
|
278
|
+
{"formId": form["_id"], f"data.{form['uniqueField']}": unique_id}
|
|
279
|
+
):
|
|
280
|
+
updated += 1
|
|
281
|
+
print("Updating entry", existing_entry["_id"])
|
|
282
|
+
else:
|
|
283
|
+
new += 1
|
|
284
|
+
return json.dumps({"new": new, "updated": updated, "failed": failed})
|
|
285
|
+
|
|
286
|
+
def _loadRemoteSchema(self, url):
|
|
287
|
+
return requests.get(url).json()
|
|
File without changes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from girder.api import access
|
|
2
|
+
from girder.api.describe import Description, autoDescribeRoute
|
|
3
|
+
from girder.api.rest import Resource, filtermodel
|
|
4
|
+
from girder.constants import AccessType, TokenScope
|
|
5
|
+
from girder.models.folder import Folder
|
|
6
|
+
|
|
7
|
+
from ..models.form import Form as FormModel
|
|
8
|
+
from ..models.entry import FormEntry as FormEntryModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class FormEntry(Resource):
|
|
12
|
+
def __init__(self):
|
|
13
|
+
super(FormEntry, self).__init__()
|
|
14
|
+
self.resourceName = "entry"
|
|
15
|
+
self.route("GET", (), self.listFormEntry)
|
|
16
|
+
self.route("GET", ("search",), self.searchFormEntry)
|
|
17
|
+
self.route("GET", (":id",), self.getFormEntry)
|
|
18
|
+
self.route("POST", (), self.createFormEntry)
|
|
19
|
+
self.route("DELETE", (":id",), self.deleteFormEntry)
|
|
20
|
+
|
|
21
|
+
@access.public
|
|
22
|
+
@autoDescribeRoute(
|
|
23
|
+
Description("List all entries")
|
|
24
|
+
.modelParam(
|
|
25
|
+
"formId",
|
|
26
|
+
"The ID of the form",
|
|
27
|
+
model=FormModel,
|
|
28
|
+
level=AccessType.READ,
|
|
29
|
+
required=False,
|
|
30
|
+
)
|
|
31
|
+
.pagingParams(defaultSort="created")
|
|
32
|
+
)
|
|
33
|
+
def listFormEntry(self, form, limit, offset, sort):
|
|
34
|
+
q = {}
|
|
35
|
+
if form:
|
|
36
|
+
q = {"formId": form["_id"]}
|
|
37
|
+
|
|
38
|
+
cursor = FormEntryModel().findWithPermissions(
|
|
39
|
+
q,
|
|
40
|
+
sort=sort,
|
|
41
|
+
user=self.getCurrentUser(),
|
|
42
|
+
level=AccessType.READ,
|
|
43
|
+
limit=limit,
|
|
44
|
+
offset=offset,
|
|
45
|
+
)
|
|
46
|
+
return list(cursor)
|
|
47
|
+
|
|
48
|
+
@access.public
|
|
49
|
+
@autoDescribeRoute(
|
|
50
|
+
Description("Search entries")
|
|
51
|
+
.param("query", "Regex for Sample Id", dataType="string", required=True)
|
|
52
|
+
.pagingParams(defaultSort="data.sampleId")
|
|
53
|
+
)
|
|
54
|
+
def searchFormEntry(self, query, limit, offset, sort):
|
|
55
|
+
print(query)
|
|
56
|
+
q = {"data.sampleId": {"$regex": query}}
|
|
57
|
+
cursor = FormEntryModel().findWithPermissions(
|
|
58
|
+
q,
|
|
59
|
+
user=self.getCurrentUser(),
|
|
60
|
+
level=AccessType.READ,
|
|
61
|
+
limit=limit,
|
|
62
|
+
offset=offset,
|
|
63
|
+
sort=sort,
|
|
64
|
+
)
|
|
65
|
+
return list(cursor)
|
|
66
|
+
|
|
67
|
+
@access.public
|
|
68
|
+
@autoDescribeRoute(
|
|
69
|
+
Description("Get an entry by ID").modelParam(
|
|
70
|
+
"id", "The ID of the form", model=FormEntryModel, level=AccessType.READ
|
|
71
|
+
)
|
|
72
|
+
)
|
|
73
|
+
@filtermodel(model=FormEntryModel, plugin="jsonforms")
|
|
74
|
+
def getFormEntry(self, entry):
|
|
75
|
+
return entry
|
|
76
|
+
|
|
77
|
+
@access.user(scope=TokenScope.DATA_WRITE)
|
|
78
|
+
@autoDescribeRoute(
|
|
79
|
+
Description("Create a new entry")
|
|
80
|
+
.modelParam(
|
|
81
|
+
"formId",
|
|
82
|
+
"The ID of the form",
|
|
83
|
+
model=FormModel,
|
|
84
|
+
level=AccessType.READ,
|
|
85
|
+
destName="form",
|
|
86
|
+
paramType="query",
|
|
87
|
+
required=True,
|
|
88
|
+
)
|
|
89
|
+
.jsonParam("data", "The data of the entry", required=True)
|
|
90
|
+
.modelParam(
|
|
91
|
+
"sourceId",
|
|
92
|
+
"The folder ID of uploaded data",
|
|
93
|
+
required=False,
|
|
94
|
+
model=Folder,
|
|
95
|
+
paramType="query",
|
|
96
|
+
destName="source",
|
|
97
|
+
level=AccessType.WRITE,
|
|
98
|
+
)
|
|
99
|
+
.modelParam(
|
|
100
|
+
"destinationId",
|
|
101
|
+
"The folder ID of destination",
|
|
102
|
+
required=True,
|
|
103
|
+
model=Folder,
|
|
104
|
+
paramType="query",
|
|
105
|
+
destName="destination",
|
|
106
|
+
level=AccessType.WRITE,
|
|
107
|
+
)
|
|
108
|
+
)
|
|
109
|
+
@filtermodel(model=FormEntryModel, plugin="jsonforms")
|
|
110
|
+
def createFormEntry(self, form, data, source, destination):
|
|
111
|
+
return FormEntryModel().create(
|
|
112
|
+
form,
|
|
113
|
+
data,
|
|
114
|
+
source,
|
|
115
|
+
destination,
|
|
116
|
+
self.getCurrentUser(),
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
@access.user(scope=TokenScope.DATA_WRITE)
|
|
120
|
+
@autoDescribeRoute(
|
|
121
|
+
Description("Delete an entry").modelParam(
|
|
122
|
+
"id", "The ID of the entry", model=FormEntryModel, level=AccessType.WRITE
|
|
123
|
+
)
|
|
124
|
+
)
|
|
125
|
+
def deleteFormEntry(self, entry):
|
|
126
|
+
FormEntryModel().remove(entry)
|