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.
Files changed (80) hide show
  1. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/.github/workflows/release.yml +8 -8
  2. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/.gitignore +3 -0
  3. {girder_jsonforms-2.0.7/girder_jsonforms.egg-info → girder_jsonforms-2.1.2}/PKG-INFO +9 -2
  4. girder_jsonforms-2.1.2/doc/file-uploads.md +315 -0
  5. girder_jsonforms-2.1.2/doc/preload.md +69 -0
  6. girder_jsonforms-2.1.2/girder_jsonforms/__init__.py +612 -0
  7. girder_jsonforms-2.1.2/girder_jsonforms/asgi.py +52 -0
  8. girder_jsonforms-2.1.2/girder_jsonforms/lib/announcement.py +132 -0
  9. girder_jsonforms-2.1.2/girder_jsonforms/lib/events.py +165 -0
  10. girder_jsonforms-2.1.2/girder_jsonforms/lib/igsn_client.py +219 -0
  11. girder_jsonforms-2.1.2/girder_jsonforms/lib/igsn_vocab.py +79 -0
  12. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/lib/jq.py +77 -1
  13. girder_jsonforms-2.1.2/girder_jsonforms/lib/locks.py +44 -0
  14. girder_jsonforms-2.1.2/girder_jsonforms/lib/mail.py +317 -0
  15. girder_jsonforms-2.1.2/girder_jsonforms/lib/metadata_dates.py +96 -0
  16. girder_jsonforms-2.1.2/girder_jsonforms/lib/orcid.py +85 -0
  17. girder_jsonforms-2.1.2/girder_jsonforms/lib/project_helpers.py +130 -0
  18. girder_jsonforms-2.1.2/girder_jsonforms/lib/project_tasks.py +28 -0
  19. girder_jsonforms-2.1.2/girder_jsonforms/lib/response_cache.py +146 -0
  20. girder_jsonforms-2.1.2/girder_jsonforms/lib/timing.py +271 -0
  21. girder_jsonforms-2.1.2/girder_jsonforms/mail_assets/caimee-shield-80.png +0 -0
  22. girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/_projectsFooter.mako +9 -0
  23. girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/_projectsFooter.txt.mako +5 -0
  24. girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/_projectsHeader.mako +34 -0
  25. girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/projectDecision.mako +36 -0
  26. girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/projectDecision.txt.mako +18 -0
  27. girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/projectSubmitted.mako +23 -0
  28. girder_jsonforms-2.1.2/girder_jsonforms/mail_templates/projectSubmitted.txt.mako +13 -0
  29. girder_jsonforms-2.1.2/girder_jsonforms/models/deposition.py +1066 -0
  30. girder_jsonforms-2.1.2/girder_jsonforms/models/entry.py +474 -0
  31. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/models/form.py +127 -37
  32. girder_jsonforms-2.1.2/girder_jsonforms/models/project.py +462 -0
  33. girder_jsonforms-2.1.2/girder_jsonforms/rest/aimdl.py +863 -0
  34. girder_jsonforms-2.1.2/girder_jsonforms/rest/deposition.py +671 -0
  35. girder_jsonforms-2.1.2/girder_jsonforms/rest/entry.py +280 -0
  36. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/rest/form.py +119 -8
  37. girder_jsonforms-2.1.2/girder_jsonforms/rest/project.py +172 -0
  38. girder_jsonforms-2.1.2/girder_jsonforms/schemas/__init__.py +0 -0
  39. girder_jsonforms-2.1.2/girder_jsonforms/schemas/datacite-v4.5.json +628 -0
  40. girder_jsonforms-2.1.2/girder_jsonforms/scripts/__init__.py +0 -0
  41. girder_jsonforms-2.1.2/girder_jsonforms/scripts/migrate_metadata_dates.py +140 -0
  42. girder_jsonforms-2.1.2/girder_jsonforms/scripts/preview_mail.py +396 -0
  43. girder_jsonforms-2.1.2/girder_jsonforms/settings.py +503 -0
  44. girder_jsonforms-2.1.2/girder_jsonforms/web_client/dist/girder-plugin-jsonforms.umd.cjs +57 -0
  45. girder_jsonforms-2.1.2/girder_jsonforms/web_client/dist/style.css +2 -0
  46. girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/__init__.py +26 -0
  47. girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/amdee.py +266 -0
  48. girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/folder_ops.py +198 -0
  49. girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/handle_enumsource.py +105 -0
  50. girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/htmax.py +58 -0
  51. girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/igsn_registry.py +174 -0
  52. girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/orcid.py +218 -0
  53. girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/projects.py +53 -0
  54. girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/pull_related_ids.py +11 -0
  55. girder_jsonforms-2.1.2/girder_jsonforms/worker_plugin/relate_entry_to_igsn.py +48 -0
  56. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2/girder_jsonforms.egg-info}/PKG-INFO +9 -2
  57. girder_jsonforms-2.1.2/girder_jsonforms.egg-info/SOURCES.txt +68 -0
  58. girder_jsonforms-2.1.2/girder_jsonforms.egg-info/entry_points.txt +9 -0
  59. girder_jsonforms-2.1.2/girder_jsonforms.egg-info/requires.txt +11 -0
  60. girder_jsonforms-2.1.2/setup.py +67 -0
  61. girder_jsonforms-2.0.7/girder_jsonforms/__init__.py +0 -90
  62. girder_jsonforms-2.0.7/girder_jsonforms/models/entry.py +0 -245
  63. girder_jsonforms-2.0.7/girder_jsonforms/rest/entry.py +0 -127
  64. girder_jsonforms-2.0.7/girder_jsonforms/settings.py +0 -26
  65. girder_jsonforms-2.0.7/girder_jsonforms/web_client/dist/girder-plugin-jsonforms.umd.cjs +0 -88
  66. girder_jsonforms-2.0.7/girder_jsonforms/web_client/dist/style.css +0 -1
  67. girder_jsonforms-2.0.7/girder_jsonforms.egg-info/SOURCES.txt +0 -25
  68. girder_jsonforms-2.0.7/girder_jsonforms.egg-info/entry_points.txt +0 -2
  69. girder_jsonforms-2.0.7/girder_jsonforms.egg-info/requires.txt +0 -4
  70. girder_jsonforms-2.0.7/setup.py +0 -35
  71. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/LICENSE +0 -0
  72. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/README.md +0 -0
  73. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/lib/__init__.py +0 -0
  74. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/lib/google_drive.py +0 -0
  75. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/models/__init__.py +0 -0
  76. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms/rest/__init__.py +0 -0
  77. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms.egg-info/dependency_links.txt +0 -0
  78. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms.egg-info/not-zip-safe +0 -0
  79. {girder_jsonforms-2.0.7 → girder_jsonforms-2.1.2}/girder_jsonforms.egg-info/top_level.txt +0 -0
  80. {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@v4
11
+ - uses: actions/checkout@v7
12
12
  - name: Set up Python
13
- uses: actions/setup-python@v5
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@v4
17
+ uses: actions/setup-node@v7
18
18
  with:
19
- node-version: "18"
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@v4
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@v4
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@v4
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.0.0
88
+ uses: sigstore/gh-action-sigstore-python@v3.5.0
89
89
  with:
90
90
  inputs: >-
91
91
  ./dist/*.tar.gz
@@ -131,3 +131,6 @@ dmypy.json
131
131
  .pyre/
132
132
 
133
133
  node_modules/
134
+
135
+ # Local email template previews
136
+ mail-preview/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: girder-jsonforms
3
- Version: 2.0.7
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.0a5.dev0
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.