girder-jsonforms 2.0.7__tar.gz → 2.1.2__tar.gz
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-2.0.7 → girder_jsonforms-2.1.2}/.github/workflows/release.yml +8 -8
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/.gitignore +3 -0
- {girder_jsonforms-2.0.7/girder_jsonforms.egg-info → girder_jsonforms-2.1.2}/PKG-INFO +9 -2
- girder_jsonforms-2.1.2/doc/file-uploads.md +315 -0
- girder_jsonforms-2.1.2/doc/preload.md +69 -0
- girder_jsonforms-2.1.2/girder_jsonforms/__init__.py +612 -0
- girder_jsonforms-2.1.2/girder_jsonforms/asgi.py +52 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/announcement.py +132 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/events.py +165 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/igsn_client.py +219 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/igsn_vocab.py +79 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/lib/jq.py +77 -1
- girder_jsonforms-2.1.2/girder_jsonforms/lib/locks.py +44 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/mail.py +317 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/metadata_dates.py +96 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/orcid.py +85 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/project_helpers.py +130 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/project_tasks.py +28 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/response_cache.py +146 -0
- girder_jsonforms-2.1.2/girder_jsonforms/lib/timing.py +271 -0
- girder_jsonforms-2.1.2/girder_jsonforms/mail_assets/caimee-shield-80.png +0 -0
- girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/_projectsFooter.mako +9 -0
- girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/_projectsFooter.txt.mako +5 -0
- girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/_projectsHeader.mako +34 -0
- girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/projectDecision.mako +36 -0
- girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/projectDecision.txt.mako +18 -0
- girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/projectSubmitted.mako +23 -0
- girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/projectSubmitted.txt.mako +13 -0
- girder_jsonforms-2.1.2/girder_jsonforms/models/deposition.py +1066 -0
- girder_jsonforms-2.1.2/girder_jsonforms/models/entry.py +474 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/models/form.py +127 -37
- girder_jsonforms-2.1.2/girder_jsonforms/models/project.py +462 -0
- girder_jsonforms-2.1.2/girder_jsonforms/rest/aimdl.py +863 -0
- girder_jsonforms-2.1.2/girder_jsonforms/rest/deposition.py +671 -0
- girder_jsonforms-2.1.2/girder_jsonforms/rest/entry.py +280 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/rest/form.py +119 -8
- girder_jsonforms-2.1.2/girder_jsonforms/rest/project.py +172 -0
- girder_jsonforms-2.1.2/girder_jsonforms/schemas/__init__.py +0 -0
- girder_jsonforms-2.1.2/girder_jsonforms/schemas/datacite-v4.5.json +628 -0
- girder_jsonforms-2.1.2/girder_jsonforms/scripts/__init__.py +0 -0
- girder_jsonforms-2.1.2/girder_jsonforms/scripts/migrate_metadata_dates.py +140 -0
- girder_jsonforms-2.1.2/girder_jsonforms/scripts/preview_mail.py +396 -0
- girder_jsonforms-2.1.2/girder_jsonforms/settings.py +503 -0
- girder_jsonforms-2.1.2/girder_jsonforms/web_client/dist/girder-plugin-jsonforms.umd.cjs +57 -0
- girder_jsonforms-2.1.2/girder_jsonforms/web_client/dist/style.css +2 -0
- girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/__init__.py +26 -0
- girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/amdee.py +266 -0
- girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/folder_ops.py +198 -0
- girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/handle_enumsource.py +105 -0
- girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/htmax.py +58 -0
- girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/igsn_registry.py +174 -0
- girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/orcid.py +218 -0
- girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/projects.py +53 -0
- girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/pull_related_ids.py +11 -0
- girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/relate_entry_to_igsn.py +48 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2/girder_jsonforms.egg-info}/PKG-INFO +9 -2
- girder_jsonforms-2.1.2/girder_jsonforms.egg-info/SOURCES.txt +68 -0
- girder_jsonforms-2.1.2/girder_jsonforms.egg-info/entry_points.txt +9 -0
- girder_jsonforms-2.1.2/girder_jsonforms.egg-info/requires.txt +11 -0
- girder_jsonforms-2.1.2/setup.py +67 -0
- girder_jsonforms-2.0.7/girder_jsonforms/__init__.py +0 -90
- girder_jsonforms-2.0.7/girder_jsonforms/models/entry.py +0 -245
- girder_jsonforms-2.0.7/girder_jsonforms/rest/entry.py +0 -127
- girder_jsonforms-2.0.7/girder_jsonforms/settings.py +0 -26
- girder_jsonforms-2.0.7/girder_jsonforms/web_client/dist/girder-plugin-jsonforms.umd.cjs +0 -88
- girder_jsonforms-2.0.7/girder_jsonforms/web_client/dist/style.css +0 -1
- girder_jsonforms-2.0.7/girder_jsonforms.egg-info/SOURCES.txt +0 -25
- girder_jsonforms-2.0.7/girder_jsonforms.egg-info/entry_points.txt +0 -2
- girder_jsonforms-2.0.7/girder_jsonforms.egg-info/requires.txt +0 -4
- girder_jsonforms-2.0.7/setup.py +0 -35
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/LICENSE +0 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/README.md +0 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/lib/__init__.py +0 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/lib/google_drive.py +0 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/models/__init__.py +0 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/rest/__init__.py +0 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms.egg-info/dependency_links.txt +0 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms.egg-info/not-zip-safe +0 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms.egg-info/top_level.txt +0 -0
- {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/setup.cfg +0 -0
|
@@ -8,15 +8,15 @@ jobs:
|
|
|
8
8
|
runs-on: ubuntu-latest
|
|
9
9
|
|
|
10
10
|
steps:
|
|
11
|
-
- uses: actions/checkout@
|
|
11
|
+
- uses: actions/checkout@v7
|
|
12
12
|
- name: Set up Python
|
|
13
|
-
uses: actions/setup-python@
|
|
13
|
+
uses: actions/setup-python@v7
|
|
14
14
|
with:
|
|
15
15
|
python-version: "3.x"
|
|
16
16
|
- name: Set up Node.js
|
|
17
|
-
uses: actions/setup-node@
|
|
17
|
+
uses: actions/setup-node@v7
|
|
18
18
|
with:
|
|
19
|
-
node-version:
|
|
19
|
+
node-version: 24
|
|
20
20
|
- name: Install pypa/build
|
|
21
21
|
run: >-
|
|
22
22
|
python3 -m
|
|
@@ -39,7 +39,7 @@ jobs:
|
|
|
39
39
|
- name: Check the package manifest
|
|
40
40
|
run: check-manifest -vvv
|
|
41
41
|
- name: Store the distribution packages
|
|
42
|
-
uses: actions/upload-artifact@
|
|
42
|
+
uses: actions/upload-artifact@v7
|
|
43
43
|
with:
|
|
44
44
|
name: python-package-distributions
|
|
45
45
|
path: dist/
|
|
@@ -59,7 +59,7 @@ jobs:
|
|
|
59
59
|
|
|
60
60
|
steps:
|
|
61
61
|
- name: Download all the dists
|
|
62
|
-
uses: actions/download-artifact@
|
|
62
|
+
uses: actions/download-artifact@v7
|
|
63
63
|
with:
|
|
64
64
|
name: python-package-distributions
|
|
65
65
|
path: dist/
|
|
@@ -80,12 +80,12 @@ jobs:
|
|
|
80
80
|
|
|
81
81
|
steps:
|
|
82
82
|
- name: Download all the dists
|
|
83
|
-
uses: actions/download-artifact@
|
|
83
|
+
uses: actions/download-artifact@v7
|
|
84
84
|
with:
|
|
85
85
|
name: python-package-distributions
|
|
86
86
|
path: dist/
|
|
87
87
|
- name: Sign the dists with Sigstore
|
|
88
|
-
uses: sigstore/gh-action-sigstore-python@v3.
|
|
88
|
+
uses: sigstore/gh-action-sigstore-python@v3.5.0
|
|
89
89
|
with:
|
|
90
90
|
inputs: >-
|
|
91
91
|
./dist/*.tar.gz
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: girder-jsonforms
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.2
|
|
4
4
|
Summary: Girder plugin adding forms based on JSON-editor
|
|
5
5
|
License: BSD-3-Clause
|
|
6
6
|
Classifier: Development Status :: 4 - Beta
|
|
@@ -12,10 +12,17 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Requires-Python: >=3.10
|
|
13
13
|
Description-Content-Type: text/markdown
|
|
14
14
|
License-File: LICENSE
|
|
15
|
-
Requires-Dist: girder>=5.0.
|
|
15
|
+
Requires-Dist: girder>=5.0.18
|
|
16
|
+
Requires-Dist: girder-worker>=5.0.18
|
|
17
|
+
Requires-Dist: girder-wholetale>=2.0.13
|
|
18
|
+
Requires-Dist: girder-jobs>=5.0.18
|
|
19
|
+
Requires-Dist: girder-sample-tracker>=2.0.0
|
|
20
|
+
Requires-Dist: girder-oauth>=5.0.18
|
|
16
21
|
Requires-Dist: google-api-python-client
|
|
17
22
|
Requires-Dist: google-auth-oauthlib
|
|
23
|
+
Requires-Dist: jsondiff
|
|
18
24
|
Requires-Dist: pandas
|
|
25
|
+
Requires-Dist: openpyxl
|
|
19
26
|
Dynamic: classifier
|
|
20
27
|
Dynamic: description
|
|
21
28
|
Dynamic: description-content-type
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# Technical Documentation: File Uploads in JSON-Schema Forms
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## 1. Introduction
|
|
6
|
+
|
|
7
|
+
This document describes how files attached to a form entry are uploaded, staged,
|
|
8
|
+
annotated, and finally moved into their permanent location. File handling in
|
|
9
|
+
`girder_jsonforms` spans the web client (json-editor + Girder's `UploadWidget`),
|
|
10
|
+
a Girder upload event handler, and the `FormEntry` model. The most important
|
|
11
|
+
property of the design — and the one most easily gotten wrong — is *when* a
|
|
12
|
+
file's destination path is decided: it is **evaluated from the submitted form
|
|
13
|
+
data at submission time**, not frozen when the bytes are uploaded.
|
|
14
|
+
|
|
15
|
+
The relevant source files are:
|
|
16
|
+
|
|
17
|
+
* `girder_jsonforms/web_client/views/EditFormView.js` — the upload UI and submit.
|
|
18
|
+
* `girder_jsonforms/__init__.py` (`annotate_upload`) — the `data.process` handler.
|
|
19
|
+
* `girder_jsonforms/rest/entry.py` — the `POST /entry` / `PUT /entry/:id` routes.
|
|
20
|
+
* `girder_jsonforms/models/entry.py` (`handle_source`, `_get_meta`,
|
|
21
|
+
`_collect_target_paths`) — the move into the destination.
|
|
22
|
+
|
|
23
|
+
## 2. The `files` schema definition
|
|
24
|
+
|
|
25
|
+
A form declares a file-upload control as an object, conventionally a reusable
|
|
26
|
+
`#/definitions/files`, with three properties:
|
|
27
|
+
|
|
28
|
+
```json
|
|
29
|
+
{
|
|
30
|
+
"targetPath": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"title": "Target Path",
|
|
33
|
+
"watch": { "hid": "root.heat_treatment_id" },
|
|
34
|
+
"options": { "hidden": true },
|
|
35
|
+
"template": "{{hid}}"
|
|
36
|
+
},
|
|
37
|
+
"file": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"title": "Upload files"
|
|
40
|
+
},
|
|
41
|
+
"button": {
|
|
42
|
+
"title": "Browse",
|
|
43
|
+
"format": "button",
|
|
44
|
+
"options": { "button": { "action": "button1CB", "uploadFor": "file" } }
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### 2.1. `targetPath`
|
|
50
|
+
|
|
51
|
+
A hidden, template-driven string that names the **sub-folder (relative to the
|
|
52
|
+
entry's destination folder) into which the uploaded files should ultimately be
|
|
53
|
+
placed**. It is normally computed by a json-editor `watch` + `template` chain
|
|
54
|
+
from other fields — e.g. `{{hid}}` bound to `root.heat_treatment_id`, which is
|
|
55
|
+
itself a template over the rest of the form. Because it is template-driven,
|
|
56
|
+
`this.form.getValue()` always reflects the *current* value: if the field it
|
|
57
|
+
depends on changes (a new heat-treatment stage is added, say), `targetPath`
|
|
58
|
+
updates with it.
|
|
59
|
+
|
|
60
|
+
### 2.2. `file`
|
|
61
|
+
|
|
62
|
+
A string holding a **comma-separated list of Girder *file* ids** that have been
|
|
63
|
+
uploaded for this control. The client appends ids to it as uploads complete (see
|
|
64
|
+
§3.3). This is the field that links a submitted form to the physical files that
|
|
65
|
+
were staged for it.
|
|
66
|
+
|
|
67
|
+
### 2.3. `button`
|
|
68
|
+
|
|
69
|
+
A json-editor `format: button` control whose `action` names a callback
|
|
70
|
+
registered in `JSONEditor.defaults.callbacks.button` (see §3.1). Clicking it
|
|
71
|
+
opens the upload dialog for the sibling `file` field.
|
|
72
|
+
|
|
73
|
+
## 3. The client upload flow
|
|
74
|
+
|
|
75
|
+
### 3.1. Button callbacks
|
|
76
|
+
|
|
77
|
+
`EditFormView.initialize` registers the button callbacks
|
|
78
|
+
(`EditFormView.js:189`). Each derives the sibling `file` field path from the
|
|
79
|
+
button's own path and opens the upload dialog:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
'button1CB': function (jseditor) {
|
|
83
|
+
const field = jseditor.options.path.replace(/\.button(?!.*\.button)/, '.file');
|
|
84
|
+
this.uploadDialog(jseditor, field, false, true); // files, multi-file
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`button2CB` is the same but for directory uploads; `buttonSample` is the
|
|
89
|
+
single-file variant.
|
|
90
|
+
|
|
91
|
+
### 3.2. The staging (temp) folder
|
|
92
|
+
|
|
93
|
+
At `initialize` time the view creates a **temporary folder** under the current
|
|
94
|
+
user (`EditFormView.js:175`), named `_temp_<formId>_<random>`, and immediately
|
|
95
|
+
schedules it for deletion in one hour:
|
|
96
|
+
|
|
97
|
+
```js
|
|
98
|
+
this.tempFolder.save().done(() => {
|
|
99
|
+
this.tempFolder.addMetadata('formId', this.model.id);
|
|
100
|
+
restRequest({ method: 'DELETE',
|
|
101
|
+
url: `folder/${this.tempFolder.id}?progress=false&countdown=3600` });
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Every file the user uploads while filling the form lands here first. The
|
|
106
|
+
countdown delete guarantees abandoned drafts are cleaned up even if the form is
|
|
107
|
+
never submitted.
|
|
108
|
+
|
|
109
|
+
### 3.3. `uploadDialog` and the upload reference
|
|
110
|
+
|
|
111
|
+
`uploadDialog` (`EditFormView.js:235`) builds a **reference** object and hands
|
|
112
|
+
it to Girder's `UploadWidget`, uploading into `this.tempFolder`:
|
|
113
|
+
|
|
114
|
+
```js
|
|
115
|
+
var reference = {
|
|
116
|
+
[this.uniqueField]: value[this.uniqueField],
|
|
117
|
+
annotate: true,
|
|
118
|
+
formField: field
|
|
119
|
+
};
|
|
120
|
+
if (value.targetPath) { reference.targetPath = value.targetPath; }
|
|
121
|
+
if (this.model.get('gdriveFolderId')) { reference.gdriveFolderId = ...; }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The reference is serialized into `otherParams.reference` and travels with the
|
|
125
|
+
upload to the server. Key fields:
|
|
126
|
+
|
|
127
|
+
| Field | Meaning |
|
|
128
|
+
|---------------|----------------------------------------------------------------|
|
|
129
|
+
| `annotate` | Tells the server to copy the reference into item metadata. |
|
|
130
|
+
| `formField` | The dotted path of the `file` field (used by batch numbering). |
|
|
131
|
+
| `targetPath` | The value of `targetPath` **at upload time** (see §6 caveat). |
|
|
132
|
+
| `<uniqueField>` | The entry's unique id (e.g. `sampleId`). |
|
|
133
|
+
| `gdriveFolderId` | Optional Google Drive target. |
|
|
134
|
+
|
|
135
|
+
On `g:uploadFinished` the returned file ids are appended to the `file` field via
|
|
136
|
+
`setField` (`EditFormView.js:271`), so the form value now records which files
|
|
137
|
+
belong to this control.
|
|
138
|
+
|
|
139
|
+
## 4. Server-side annotation on upload
|
|
140
|
+
|
|
141
|
+
Uploading a file triggers Girder's `data.process` event, bound to
|
|
142
|
+
`annotate_uploads` → `annotate_upload` (`__init__.py:47`, `__init__.py:59`).
|
|
143
|
+
When the reference has `annotate: true`, the handler writes the **entire
|
|
144
|
+
reference into the uploaded item's metadata**:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
if reference.get("annotate"):
|
|
148
|
+
parent = Item().load(info["file"]["itemId"], level=AccessType.WRITE, user=...)
|
|
149
|
+
reference.pop("file", None)
|
|
150
|
+
reference.pop("annotate", None)
|
|
151
|
+
Item().setMetadata(parent, reference)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
So immediately after upload, each staged item carries `formField`, the unique
|
|
155
|
+
field, and the upload-time `targetPath` in its `meta`. This metadata is a
|
|
156
|
+
best-effort snapshot; it is **not** the authoritative source for the final
|
|
157
|
+
destination (see §5.2 and §6).
|
|
158
|
+
|
|
159
|
+
## 5. Submission
|
|
160
|
+
|
|
161
|
+
### 5.1. The submit request
|
|
162
|
+
|
|
163
|
+
On form submit (`EditFormView.js:73`) the view validates, then posts the form
|
|
164
|
+
value along with the staging and destination folder ids:
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
var params = { formId, data: JSON.stringify(this.form.getValue()) };
|
|
168
|
+
if (this.tempFolder) { params.sourceId = this.tempFolder.id; }
|
|
169
|
+
if (this.destFolder) { params.destinationId = this.destFolder.id; }
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`POST /entry` (create) or `PUT /entry/:id` (update) receive these as `source`
|
|
173
|
+
and `destination` model params (`rest/entry.py:213`, `rest/entry.py:154`) and
|
|
174
|
+
call `FormEntry.create_entry` / `update_entry`, which delegate the file move to
|
|
175
|
+
`handle_source` (`models/entry.py`).
|
|
176
|
+
|
|
177
|
+
### 5.2. `handle_source`: moving staged content into place
|
|
178
|
+
|
|
179
|
+
`handle_source` (`models/entry.py`) walks the staging (source) folder and moves
|
|
180
|
+
each child item and child folder into a destination derived from its
|
|
181
|
+
`targetPath`:
|
|
182
|
+
|
|
183
|
+
1. Build the authoritative `targetPath` map from the submitted data (§6).
|
|
184
|
+
2. For each child folder / item:
|
|
185
|
+
* Resolve its `targetPath` (form data first, baked metadata as fallback).
|
|
186
|
+
* Compute the destination folder with `get_destination_folder`, which
|
|
187
|
+
creates the `targetPath` sub-folders under the destination on demand
|
|
188
|
+
(`reuseExisting=True`).
|
|
189
|
+
* Rename to avoid collisions (`unique`) and move it there.
|
|
190
|
+
* Record the moved id on the entry (`entry["files"]` / `entry["folders"]`).
|
|
191
|
+
3. Remove the now-empty source folder.
|
|
192
|
+
|
|
193
|
+
`get_destination_folder(path, root, user)` (`models/entry.py:360`) splits
|
|
194
|
+
`path` on the OS separator and creates each segment as a nested folder, so a
|
|
195
|
+
`targetPath` of `a/b/c` yields `<destination>/a/b/c`.
|
|
196
|
+
|
|
197
|
+
## 6. When `targetPath` is evaluated (the important part)
|
|
198
|
+
|
|
199
|
+
The destination for each file is resolved from the **submitted form data**, not
|
|
200
|
+
from the metadata frozen onto the item at upload time. This matters because
|
|
201
|
+
`targetPath` is template-driven: its value can legitimately change between the
|
|
202
|
+
moment a file is uploaded and the moment the form is submitted (e.g. adding a
|
|
203
|
+
heat-treatment stage changes `heat_treatment_id`, and therefore every
|
|
204
|
+
`targetPath` bound to it). The intended and implemented behavior is: **all files
|
|
205
|
+
are placed according to the final form state.**
|
|
206
|
+
|
|
207
|
+
### 6.1. Building the map
|
|
208
|
+
|
|
209
|
+
`_collect_target_paths(data)` (`models/entry.py`) walks the entire submitted
|
|
210
|
+
data tree and, for every object that has a non-empty `file` string, maps each
|
|
211
|
+
file id in that string to the object's `targetPath`:
|
|
212
|
+
|
|
213
|
+
```python
|
|
214
|
+
{ "<fileId>": "<targetPath>", ... }
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Because it walks the whole tree, it captures every `files` control wherever it
|
|
218
|
+
appears — top-level, inside `stress_relief`, inside each `heat_treatment[]`
|
|
219
|
+
entry, etc.
|
|
220
|
+
|
|
221
|
+
### 6.2. Resolving a staged item / folder
|
|
222
|
+
|
|
223
|
+
* `_item_target_path(item, target_paths)` matches a staged item to the map by
|
|
224
|
+
the ids of the files it contains (`Item().childFiles`).
|
|
225
|
+
* `_folder_target_path(folder, creator, target_paths)` matches a staged
|
|
226
|
+
directory by any file found under it (searched recursively).
|
|
227
|
+
|
|
228
|
+
Both return a sentinel (`_UNSET`) when nothing matches, in which case
|
|
229
|
+
`_get_meta` falls back to the item's baked `targetPath` metadata (§4),
|
|
230
|
+
preserving behavior for any form that does not carry `targetPath` in its data.
|
|
231
|
+
|
|
232
|
+
### 6.3. Applying the resolved path
|
|
233
|
+
|
|
234
|
+
`_get_meta(entry, child_meta, override_path=_UNSET)` (`models/entry.py`) uses
|
|
235
|
+
the override when present, and also writes it back into the item's `meta` so the
|
|
236
|
+
stored `targetPath` matches where the file was actually moved:
|
|
237
|
+
|
|
238
|
+
```python
|
|
239
|
+
if override_path is _UNSET:
|
|
240
|
+
path = child_meta.get("targetPath") # fallback: baked metadata
|
|
241
|
+
else:
|
|
242
|
+
path = override_path # authoritative: submitted form data
|
|
243
|
+
meta["targetPath"] = path
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
> **Rationale.** The upload-time `targetPath` written in §4 is a snapshot that
|
|
247
|
+
> can become stale. Resolving from the submitted data at move time guarantees a
|
|
248
|
+
> file lands in the folder implied by the *final* form, regardless of how the
|
|
249
|
+
> form changed after the file was uploaded. The upload reference still carries
|
|
250
|
+
> `targetPath` (as a harmless fallback), but form data always wins when present.
|
|
251
|
+
|
|
252
|
+
## 7. Batch numbering (IGSN sub-samples)
|
|
253
|
+
|
|
254
|
+
When `data["igsn"]["batch"]["method"] == "from_array"`, `_get_meta` derives a
|
|
255
|
+
per-file numeric suffix from the item's `formField` metadata and appends it to
|
|
256
|
+
the resolved `targetPath` (and to the assigned IGSN):
|
|
257
|
+
|
|
258
|
+
```python
|
|
259
|
+
number = str(int(re.search(r"\d+", child_meta.pop("formField")).group()) + 1)
|
|
260
|
+
meta["igsn"] = f"{entry['data']['assignedIGSN']}-{number}" # if assignedIGSN present
|
|
261
|
+
path = os.path.join(path, number) if path else number
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
This is why `formField` is still carried in the upload reference and item
|
|
265
|
+
metadata even though `targetPath` itself is now resolved from form data: batch
|
|
266
|
+
numbering keys off `formField`, and the number is layered on top of the resolved
|
|
267
|
+
base path.
|
|
268
|
+
|
|
269
|
+
## 8. Serialization and Google Drive
|
|
270
|
+
|
|
271
|
+
If the form has `serialize: true`, `handle_serialization` (`models/entry.py:284`)
|
|
272
|
+
dumps the entry itself as a JSON file into each resolved destination folder,
|
|
273
|
+
reusing the same `targetPath` / `get_destination_folder` machinery. When a
|
|
274
|
+
`gdriveFolderId` is configured, both moved files and the serialized entry are
|
|
275
|
+
pushed to Google Drive via the `gdrive.upload` event, using
|
|
276
|
+
`os.path.join(path, file_name)` as the Drive-side path — again built from the
|
|
277
|
+
resolved `path`.
|
|
278
|
+
|
|
279
|
+
## 9. Lifecycle summary
|
|
280
|
+
|
|
281
|
+
```
|
|
282
|
+
┌──────────────┐ upload (reference: annotate, formField, targetPath@now)
|
|
283
|
+
│ user clicks │ ───────────────────────────────────────────────► temp folder
|
|
284
|
+
│ "Browse" │ (auto-delete 1h)
|
|
285
|
+
└──────────────┘ │
|
|
286
|
+
│ file ids appended to the `file` field │ data.process
|
|
287
|
+
▼ ▼
|
|
288
|
+
┌──────────────┐ annotate_upload writes the
|
|
289
|
+
│ user edits │ targetPath field re-evaluates reference into item metadata
|
|
290
|
+
│ form; watch │ via watch/template (may change) (snapshot, fallback)
|
|
291
|
+
│ updates │
|
|
292
|
+
└──────────────┘
|
|
293
|
+
│ submit: POST /entry { data, sourceId=temp, destinationId=dest }
|
|
294
|
+
▼
|
|
295
|
+
┌───────────────────────── handle_source ─────────────────────────┐
|
|
296
|
+
│ _collect_target_paths(data) → { fileId: targetPath } │ ← authoritative
|
|
297
|
+
│ for each staged item/folder: │
|
|
298
|
+
│ path = form-data targetPath (fallback: baked metadata) │
|
|
299
|
+
│ dest = get_destination_folder(path, destination) # creates │
|
|
300
|
+
│ unique(...) ; move(...) │
|
|
301
|
+
│ remove(temp) ; save(entry) │
|
|
302
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## 10. Testing
|
|
306
|
+
|
|
307
|
+
`girder_jsonforms/tests/test_entry.py` covers this flow:
|
|
308
|
+
|
|
309
|
+
* `test_create_entry_with_folders` — create with `sourceId`/`destinationId`.
|
|
310
|
+
* `test_handle_source_uses_submitted_target_path` — regression test proving that
|
|
311
|
+
a staged item whose *metadata* carries a stale `targetPath` is moved to the
|
|
312
|
+
path in the *submitted data* instead, and that its stored metadata is
|
|
313
|
+
corrected.
|
|
314
|
+
|
|
315
|
+
The suite requires a running MongoDB and Redis (see `CLAUDE.md`).
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Technical Documentation: Preload Field in JSON Schema
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## 1. Introduction
|
|
6
|
+
|
|
7
|
+
This document describes the `preload` field within the root of a JSON schema, its specified format, and its resulting impact on the materialization of a form, specifically regarding the "dependencies" key.
|
|
8
|
+
|
|
9
|
+
## 2. Preload Field Definition
|
|
10
|
+
|
|
11
|
+
The `preload` field is an optional field located at the root level of a JSON schema. Its primary purpose is to define a mechanism for pre-populating data within a form's materialized dependencies.
|
|
12
|
+
|
|
13
|
+
## 3. Preload Field Format
|
|
14
|
+
|
|
15
|
+
The `preload` field adheres to a specific string format:
|
|
16
|
+
|
|
17
|
+
`girder.formId:<formId>:<field1>:<field2>...`
|
|
18
|
+
|
|
19
|
+
### 3.1. Format Components:
|
|
20
|
+
|
|
21
|
+
* **`girder.formId`**: A literal string prefix indicating the type of preload operation, specifically targeting a Girder form.
|
|
22
|
+
* **`<formId>`**: A unique identifier corresponding to the target form. This ID links the preload configuration to a specific form definition.
|
|
23
|
+
* **`<field1>:<field2>...`**: A colon-separated list of field names. These field names correspond to specific fields within the form identified by `<formId>` whose values are intended to be preloaded.
|
|
24
|
+
|
|
25
|
+
## 4. Impact on Form Materialization: "dependencies" Key
|
|
26
|
+
|
|
27
|
+
When a form is materialized, the presence and proper configuration of the `preload` field result in the addition or modification of a "dependencies" key within the form's materialized structure.
|
|
28
|
+
|
|
29
|
+
### 4.1. "dependencies" Key Structure:
|
|
30
|
+
|
|
31
|
+
The "dependencies" key will contain a map where:
|
|
32
|
+
|
|
33
|
+
* **`<entryId>`**: Represents a unique identifier for a data entry or record that is being preloaded. The specific generation or source of this `entryId` is context-dependent but typically relates to an existing data record that the form is intended to interact with.
|
|
34
|
+
* **`{data.<field1>: value1, data.<field2>: value2, ...}`**: This is an object that maps the specified preload fields to their corresponding values for the given `<entryId>`.
|
|
35
|
+
* **`data.<fieldN>`**: Represents the full path to the field within the preloaded data structure. The `data.` prefix indicates that these fields are part of the core data payload for the entry.
|
|
36
|
+
* **`valueN`**: The actual value to be preloaded for `data.<fieldN>`. These values are typically retrieved from an external data source based on the `<entryId>` and the specified field names.
|
|
37
|
+
|
|
38
|
+
## 5. Example
|
|
39
|
+
|
|
40
|
+
Consider the following `preload` field in a JSON schema:
|
|
41
|
+
|
|
42
|
+
```json
|
|
43
|
+
{
|
|
44
|
+
"preload": "girder.formId:myAwesomeForm:firstName:lastName:email"
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Upon materialization of the myAwesomeForm, and assuming a preloaded entry with entryId "user123", the "dependencies" key within the form's materialized structure might appear as follows:
|
|
49
|
+
|
|
50
|
+
```json
|
|
51
|
+
{
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"user123": {
|
|
54
|
+
"data.firstName": "John",
|
|
55
|
+
"data.lastName": "Doe",
|
|
56
|
+
"data.email": "john.doe@example.com"
|
|
57
|
+
},
|
|
58
|
+
"user124": {
|
|
59
|
+
"data.firstName": "Jane",
|
|
60
|
+
"data.lastName": "Smith",
|
|
61
|
+
"data.email": "jane.smith@example.com"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## 6. Conclusion
|
|
68
|
+
|
|
69
|
+
The `preload` field in a JSON schema serves as a powerful tool for pre-populating form data, enhancing user experience by reducing manual input requirements. By adhering to the specified format and understanding its impact on the "dependencies" key, developers can effectively implement this feature in their applications.
|