labimotion 2.4.0.rc3 → 2.4.0.rc4
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/lib/labimotion/apis/generic_dataset_api.rb +41 -3
- data/lib/labimotion/apis/generic_element_api.rb +60 -8
- data/lib/labimotion/apis/generic_klass_api.rb +50 -6
- data/lib/labimotion/apis/klass_share_api.rb +646 -0
- data/lib/labimotion/apis/labimotion_api.rb +1 -0
- data/lib/labimotion/apis/labimotion_doi_api.rb +24 -10
- data/lib/labimotion/apis/labimotion_template_browse_api.rb +13 -1
- data/lib/labimotion/apis/segment_api.rb +61 -10
- data/lib/labimotion/constants.rb +14 -0
- data/lib/labimotion/entities/application_entity.rb +8 -0
- data/lib/labimotion/entities/generic_klass_entity.rb +125 -0
- data/lib/labimotion/entities/klass_share_entity.rb +48 -0
- data/lib/labimotion/helpers/dataset_helpers.rb +123 -1
- data/lib/labimotion/helpers/element_helpers.rb +206 -4
- data/lib/labimotion/helpers/generic_helpers.rb +276 -4
- data/lib/labimotion/helpers/param_helpers.rb +68 -0
- data/lib/labimotion/helpers/segment_helpers.rb +87 -4
- data/lib/labimotion/libs/ai_egress_guard.rb +84 -0
- data/lib/labimotion/libs/ai_template.rb +1482 -0
- data/lib/labimotion/libs/data/datacite/labimotion_template.html.erb +67 -0
- data/lib/labimotion/libs/file_extractor.rb +210 -0
- data/lib/labimotion/libs/owner_resolver.rb +50 -0
- data/lib/labimotion/libs/ownership_audit.rb +73 -0
- data/lib/labimotion/libs/share_notifier.rb +114 -0
- data/lib/labimotion/libs/share_resolver.rb +369 -0
- data/lib/labimotion/models/concerns/template_doi.rb +133 -0
- data/lib/labimotion/models/dataset_klass.rb +1 -1
- data/lib/labimotion/models/element_klass.rb +1 -1
- data/lib/labimotion/models/klass_share.rb +126 -0
- data/lib/labimotion/models/segment_klass.rb +1 -1
- data/lib/labimotion/usecases/build_template_doi_xml.rb +69 -23
- data/lib/labimotion/usecases/release_template_doi.rb +42 -17
- data/lib/labimotion/usecases/template_doi_helpers.rb +28 -6
- data/lib/labimotion/usecases/update_template_publication_metadata.rb +71 -2
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +11 -0
- metadata +28 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1babeffe4e43eb088d5ab781101aa48574f67aab9abf0d32df4d883742e63788
|
|
4
|
+
data.tar.gz: 8c063d368a6af316e5397804951da314d8fc65035a852f56b066097091b0b2b3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5d92879758e133fbf40fd433882967c8731ebf18e1e2586d8026a65b2d085359a530333cd740d00b7b0359981f155c034eee5687ffe326bb842c2dbc1ee52ad8
|
|
7
|
+
data.tar.gz: 3db447e733afbe176b1d7adb80078ef85899b473336e882d41bb9f7da4b66b51ca91d1f4084d30a0f644401e95c4312669bd1f9a6d7bc861af42dc8ad89d1abd
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
* Features and enhancements:
|
|
5
|
+
* Share notifications now fire from the gem itself ([chem-generic-ui#1040](https://github.com/LabIMotion/chem-generic-ui/issues/1040), labimotion#255): a successful grant or level change (`POST /generic_klass/shares`), an ownership transfer (`POST /generic_klass/transfer_ownership`) and the ask that creates an access-request row (`POST /generic_klass/request_access`, `created: true` only — repeat asks are not repeat pings) each send the toast the host's `Chemotion::TemplateShareAPI` used to send, through the new `Labimotion::ShareNotifier`. Recipients are Persons only, never the actor; the channel is looked up by the literal subject `'Template Share Notification'` (the host ELN's channel migration stays required for delivery); and the notifier rescues internally — an undeliverable notification is a notification not sent, never a failed share. A host without Message/Channel machinery (or without the channel row) silently sends nothing. The level-change route (`PUT /shares/:id`) stays silent, matching the host layer it replaces. Until the matching ELN change removes `TemplateShareAPI` and its caller, grants on the integration branches notify twice — accepted, dev-only.
|
|
6
|
+
* Per-template ownership and sharing, backend ([chem-generic-ui#1001](https://github.com/LabIMotion/chem-generic-ui/issues/1001) Phase 2): `klass_shares` table (viewer/editor/owner, one owner per template enforced by a partial unique index), `authorize_klass!` per-template gate replacing the designer-wide check on template mutation endpoints (owner-less templates keep the legacy behaviour permanently), share management endpoints under `/generic_klass/shares` plus `/generic_klass/transfer_ownership`, and capability booleans (`can_write` … `can_manage`) on the Designer list payloads. `download_klass` is now gated (`:read`) and no longer writes an identifier during a GET. Requires host ELN twin migrations `create_klass_shares` and `seed_klass_share_owners` (the latter seeds owner rows from `created_by` in raw SQL, idempotent via `ON CONFLICT DO NOTHING`); deploy order is migrate → **restart** (the stale schema cache otherwise silently drops the running app's writes to the new table). Ships as one upgrade with the matching chem-generic-ui and ELN changes — see the design doc's ADR 0001.
|
|
7
|
+
* `created_by` and `created_at` are now `NOT NULL` on `element_klasses` and `segment_klasses` ([chem-generic-ui#1018](https://github.com/LabIMotion/chem-generic-ui/issues/1018)); `dataset_klasses` already declared both. Two new migrations, back-dated ahead of `create_klass_shares`: `backfill_klass_created_columns` fills a NULL `created_by` with the first system administrator (falling back to the lowest-id user of any type, and raising if the `users` table is empty) and a NULL `created_at` with `COALESCE(updated_at, NOW())`; `tighten_klass_created_columns` then applies the constraints — data before constraint. The back-dating is what lets `seed_klass_share_owners` stay unchanged: with `created_by` already filled in, its `WHERE created_by IS NOT NULL` filter excludes nothing and every template gets an owner row instead of staying owner-less on the permanent legacy gate. The backfill re-runs that owner seed itself (`ON CONFLICT DO NOTHING`) so a database on which the seed had already run picks up the repaired rows. Requires host ELN twin migrations for both, back-dated the same way; deploy order is migrate → **restart** (the stale schema cache otherwise silently drops the running app's writes).
|
|
8
|
+
* Any individual account may own a template, and an owner who cannot be notified is now reported as such ([chem-generic-ui#1019](https://github.com/LabIMotion/chem-generic-ui/issues/1019)). `Labimotion::KlassShare` validates `shared_with_id` against an allowlist (`Person`, `Admin`) instead of `User.persons`, so the raw-SQL `seed_klass_share_owners` migration and the runtime `KlassShare.seed_owner!` reach the same verdict for an admin-created template: the migration used to write the owner row while the runtime path refused it and left the template owner-less on the permanent legacy gate, where any designer of the family may edit and delete it. `backfill_klass_created_columns` fills a NULL `created_by` with the first system administrator, so those templates exist in numbers. `Group` and `DeviceDeprecated` stay excluded — a group is not an individual, and ELN reserves the type for device management — and grants and ownership transfers through the API stay Person-only, since an admin owner is a state to clean up rather than one to create. `GenericKlassEntity#owner_state` gains a fourth value, `unnotifiable` (after `deleted` and `deactivated`), for an owner whose `type` is not `Person`: such a template read `active` and appeared in no banner, while a designer refused on it was told to ask an owner that ELN's `channel_type` 8 fan-out cannot reach, so `request_access` answered 422 and nothing said a transfer was needed.
|
|
9
|
+
* An access request is now stored as a `klass_shares` row rather than sent as a message ([chem-generic-ui#1023](https://github.com/LabIMotion/chem-generic-ui/issues/1023)). `Labimotion::KlassShare::LEVELS` gains `requested: 0` — additive, never a renumber, since the one-owner-per-klass partial index is `WHERE permission_level = 30` — so a level-0 row records the ask while granting nothing (`0 >= 10` is false, so every existing threshold refuses it unchanged). The last hop of the request loop used to land on the Designer, which polls no message list, so an owner working there never saw the request; as a row it survives acknowledgement, and the unique `(klass_type, klass_id, shared_with_id)` index dedupes repeat asks. New `POST /generic_klass/request_access?klass=&id=`, answering `{ mc: 'ss00', created: <Boolean> }`: self only, level 0 only, idempotent — no row creates one and answers `created: true` (the caller fires the host ping only then, so ten asks are not ten pings), an existing level-0 row answers `created: false` silently, and a caller who already holds viewer/editor/owner gets 409 `sc13`. A lost race on the unique index answers `created: false` rather than 500 — the row existing is the goal state. Gated on the legacy family designer right, not `authorize_klass!(:read)`: `:read` demands level ≥ 10 and a requester has none, so the share gate would refuse every caller the route exists for. Granting and rejecting need no new endpoint — `POST /generic_klass/shares` lifts the level-0 row to viewer/editor in place, and `DELETE /generic_klass/shares/:id` is the rejection (a hard delete, so "no" does not stick and the requester may ask again — accepted, since a tombstone would contradict `KlassShare` deliberately not being `acts_as_paranoid`). Every share route's body gains a `requests` key carrying the level-0 rows, which `shares` no longer contains: someone *waiting* for access listed under "People with access" says the opposite of what they are. `shares` keeps its contents and owner-first ordering for everything ≥ 10. Both lists go to anyone who may `:read` the template, so viewers and editors see others' pending requests — intended transparency. Klass list payloads gain `pending_request_count` (Integer, 0 when absent), derived from the share rows `ShareResolver.context_for` already loads — no extra query — and scoped to the requesting user's own owner rows, since a naive count would ship "somebody asked" to every editor and viewer in the payload. Exposed under the same `share_context` guard as the capability booleans, so exports, Hub submissions and un-migrated hosts are unaffected. No migration: `permission_level` already accepts 0. Note that `current_user_permission` now has a fourth answer, `'requested'` — a client must compare the level, not test the string for truthiness.
|
|
10
|
+
* The Designer's share poller now asks the gem instead of the host ([chem-generic-ui#1023](https://github.com/LabIMotion/chem-generic-ui/issues/1023)). New `GET /generic_klass/share_activity`, answering `{ mc: 'ss00', count: <Integer>, latest: <ISO8601 String|null> }`: how many `klass_shares` rows involve the caller — every row on the templates they own, plus the rows granting them access on other people's — and the newest `updated_at` among them. The pair is an opaque change token, and the client refreshes its grid when **either** value differs in **either** direction. Deliberately not a "newer than" cursor: a revocation is a hard delete (`KlassShare` is not `acts_as_paranoid`), so `latest` moves *backwards* while `count` drops — precisely the event a timestamp comparison would stop detecting. The library used to poll the host's `/api/v1/messages/list?is_ack=0` for this: its only reach outside gem-owned API surface, a route no embedder is obliged to mount, carrying every kind of notification with no way for the library to name the host's channels and filter it (an unrelated spectra message refreshed the Designer grid), and acknowledging message rows as a side effect of the read. Authentication only and no further gate — there is no single klass for `authorize_klass!` to take and no family for `authenticate_admin!`, and the response is two aggregates over the caller's *own* rows, naming no template, user or level and disclosing nothing about anybody else's sharing, while a gate would refuse the grantees the route exists for. Aggregates only, three indexed queries however large the account (the owned keys, then `COUNT` and `MAX` over one OR-ed scope), because an open Designer polls it about once a minute. The administrator is an implicit owner everywhere but holds rows only where they really own something, so their token moves for their own templates alone. No migration: `klass_shares` already carries `t.timestamps` and an index on `shared_with_id`.
|
|
11
|
+
* The share poller can now refresh individual template rows instead of reloading the whole Designer grid ([chem-generic-ui#1023](https://github.com/LabIMotion/chem-generic-ui/issues/1023)). Reloading the grid re-seeds the open Work Area and destroys unsaved template edits, and it fired even for the user's own grant or revoke; two reads replace it. First, `GET /generic_klass/share_activity` gains `digests`: `{ "ElementKlass:12": "<row count>|<newest updated_at, ISO8601 with milliseconds>", … }`, one entry per klass the caller is involved in — the same involvement `count` and `latest` already aggregate over, split by key, each value that klass's own count and `MAX(updated_at)`. The client diffs the map and re-reads only the keys whose digest changed, appeared or vanished. `count` and `latest` stay, unchanged, as the cheap early-out. The same inequality-in-either-direction rule applies and matters more here: a revocation is a hard delete, so a key's digest can move *backwards* or the key can drop out of the map entirely, and a "newer than" comparison would see neither. The digest string is opaque — compare it, never parse it. No extra query: the map is a `GROUP BY (klass_type, klass_id)` over the scope that was already being aggregated, and the pair is then derived from the groups rather than queried again (three indexed queries before and after), so `count` is the sum of the digests' counts by construction. Second, new `GET /generic_klass/rows?keys[]=ElementKlass:12&keys[]=SegmentKlass:3`, answering `{ mc: 'ss00', data: [<grid-shaped entity>, …], keys: [<the key of each entry>, …] }`. Each entry is exactly what the Designer list endpoints send for that record — same entity, `with_ownership: true`, preloaded `owners:` and a `share_context:` resolved over precisely the requested records — so a row can be dropped into a grid unmodified and `owner`, `can_write` … `can_manage`, `current_user_permission` and `pending_request_count` all keep meaning what they meant in the list. `GET /generic_klass/fetch` could not serve this: it represents with neither `share_context` nor `with_ownership`, and the library reads an absent capability field as permissive (that is how an un-upgraded gem stays usable), so a row patched from it would quietly turn a viewer's template editable. `keys` runs parallel to `data` because the klass entities carry no type field — an id alone cannot distinguish `ElementKlass:9` from `DatasetKlass:9` — and it is also how a client learns which requested keys were skipped. Batches are capped at 50 keys, counted before de-duplication, and refused with 400 `se00` rather than truncated, so a short answer never reads as "those templates were deleted"; malformed keys are refused the same way rather than guessed at; a key whose record no longer exists is skipped, since a template deleted between the poll and the read is an ordinary race. Grouped by klass type, so the cost is one query per type present in the batch, not one per key. Ungated, deliberately and like the list endpoints it mirrors — those list every template to every designer and let the *actions* gate — and what constrains the caller is inside the payload: the capability booleans are resolved for that user, and every mutating route still re-checks with `authorize_klass!`. No migration.
|
|
12
|
+
* The Designer now has a share inbox, derived entirely from `klass_shares` ([chem-generic-ui#1028](https://github.com/LabIMotion/chem-generic-ui/issues/1028)). An owner working in the Designer had no list of what was waiting for them, and a requester was never told they had been granted access: the request loop's last hop landed on a host message that only `NoticeButton` polls, mounted in the mydb topbar outside the Designer's React roots. New `GET /generic_klass/share_inbox`, answering `{ mc: 'ss00', requests: [{ share_id, klass_type, klass_id, klass_label, user: { id, name, name_abbreviation }, requested_at }, …], grants: [{ share_id, klass_type, klass_id, klass_label, permission, user: { … }, changed_at }, …] }`. **Both queues are queries over `klass_shares`, not a message log** — a second store would immediately have its own ack state, its own retention question, and the ability to disagree with the rows. `requests` is the owner's queue: the `permission_level = 0` rows on the templates the caller holds the level-30 row on, oldest ask first, `user` the requester and `requested_at` their `created_at`. No column gates it and none should — a grant promotes the row off level 0 and a reject hard-deletes it, so it empties itself, and it is the same predicate behind the grid's `ShareBtn` badge, so the two cannot disagree. `grants` is the requester's queue: `shared_with_id = caller AND permission_level >= 10 AND created_by <> shared_with_id AND (acked_at IS NULL OR acked_at < updated_at)`, newest change first, `user` the granter (`created_by`) and `changed_at` the `updated_at` that `acked_at` is compared against. `created_by <> shared_with_id` is load-bearing and looks removable: every owner row is a self-grant written by `KlassShare.seed_owner!`, so without it every designer would be told "you were granted owner on…" for every template they ever created; a real grant re-stamps `created_by` to the granter and so does a transfer, so both still announce. Entries whose template can no longer be resolved are dropped rather than sent with a null label — an entry exists to be clicked through to a grid row — and the share row survives, so a restore brings the entry back untouched. Labels and user names are resolved in bulk (one query per klass type present, one for the users), so the whole route is at most seven indexed queries however large the account; it is polled on the same tick as `share_activity`. Authentication only, no `authorize_klass!` and no family gate, for the same reason as `share_activity`: every row returned either names the caller or sits on a template they own, and a gate would refuse the grantees the route exists for.
|
|
13
|
+
* New `POST /generic_klass/shares/:share_id/ack`, answering `{ mc: 'ss00' }` ([chem-generic-ui#1028](https://github.com/LabIMotion/chem-generic-ui/issues/1028)). Self only — a row whose `shared_with_id` is not the caller is refused 404, the same answer an unknown id gets, so a row that does not name you is indistinguishable from one that does not exist; holding `:manage` on the template does not make somebody else's grant yours to acknowledge. Idempotent. Written with `update_column`, deliberately: `updated_at` is the other half of the unacknowledged comparison, so an ack that bumped it would leave the row announcing itself for ever, and it would also move `share_activity`'s change token and make every other Designer session re-read the grid row. `acked_at` is a nullable datetime carried by `create_klass_shares` itself rather than a separate ALTER migration — one file per table while nothing is released — so a database that already ran that version will never receive the column and must be dropped and recreated rather than migrated again. Nullable because every row standing at upgrade time is unacknowledged by definition and a backfill would silently swallow it. The ELN twin of `create_klass_shares` carries the same change, byte-identical. Deploy order is migrate → **restart** (the stale schema cache otherwise silently drops the running app's writes to the new column). The library never reads ELN's notifications and never acks them: `NoticeButton`'s read marker and `acked_at` are deliberately independent, answering different questions in different places.
|
|
5
14
|
* Added per-user, per-klass toolbar configuration (Overview/Arrange visibility) via `POST /user_klass_settings/save` (upsert) and `GET /user_klass_settings/fetch` ([chem-generic-ui#905](https://github.com/LabIMotion/chem-generic-ui/issues/905)). Each user stores their own `settings` (`{ toolbar: { overview, arrange } }`) per element/segment/dataset klass, keyed by the stable klass identifier so it applies to every instance of that klass across template versions. Requires host ELN twin migration `create_user_klass_settings`.
|
|
6
15
|
* Designer can enable/disable common layer functionalities (record time, add reaction, add/remove layer) per template; template-scoped and independent of template versions ([chem-generic-ui#904](https://github.com/LabIMotion/chem-generic-ui/issues/904)). Saved segments expose the effective klass `settings` so records honor the toggles after a reload. Requires host ELN twin migration `add_settings_to_klasses`.
|
|
7
16
|
* Support linking CellLine elements in the generic Link Element via the `Labimotion::CellLine` wrapper ([chem-generic-ui#906](https://github.com/LabIMotion/chem-generic-ui/issues/906)). Requires a host ELN `element_klasses` row for `cell_line` (migration) — see PR notes.
|
|
@@ -7,6 +7,7 @@ module Labimotion
|
|
|
7
7
|
|
|
8
8
|
helpers Labimotion::GenericHelpers
|
|
9
9
|
helpers Labimotion::DatasetHelpers
|
|
10
|
+
helpers Labimotion::ParamHelpers
|
|
10
11
|
|
|
11
12
|
resource :generic_dataset do
|
|
12
13
|
namespace :klasses do
|
|
@@ -26,7 +27,10 @@ module Labimotion
|
|
|
26
27
|
get do
|
|
27
28
|
list = klass_list(params[:is_active], params[:displayed_in_list])
|
|
28
29
|
serialized_data = Labimotion::DatasetKlassEntity.represent(list,
|
|
29
|
-
displayed_in_list: params[:displayed_in_list]
|
|
30
|
+
displayed_in_list: params[:displayed_in_list],
|
|
31
|
+
with_ownership: true,
|
|
32
|
+
owners: Labimotion::OwnerResolver.map_for(list),
|
|
33
|
+
share_context: Labimotion::ShareResolver.context_for(list, current_user))
|
|
30
34
|
{ mc: 'ss00', data: serialized_data }
|
|
31
35
|
rescue StandardError => e
|
|
32
36
|
Labimotion.log_exception(e, current_user)
|
|
@@ -34,7 +38,8 @@ module Labimotion
|
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
|
|
37
|
-
# Deprecated
|
|
41
|
+
# Deprecated as an endpoint, but it is what the Dataset Designer grid still calls
|
|
42
|
+
# (eln GenericDSsFetcher.listDatasetKlass), so ownership has to be exposed here too.
|
|
38
43
|
# It is replaced by `list_klass`.
|
|
39
44
|
namespace :list_dataset_klass do
|
|
40
45
|
desc 'list Generic Dataset Klass'
|
|
@@ -43,7 +48,9 @@ module Labimotion
|
|
|
43
48
|
end
|
|
44
49
|
get do
|
|
45
50
|
list = klass_list(params[:is_active], false)
|
|
46
|
-
present list, with: Labimotion::DatasetKlassEntity, root: 'klass', displayed_in_list: false
|
|
51
|
+
present list, with: Labimotion::DatasetKlassEntity, root: 'klass', displayed_in_list: false,
|
|
52
|
+
with_ownership: true, owners: Labimotion::OwnerResolver.map_for(list),
|
|
53
|
+
share_context: Labimotion::ShareResolver.context_for(list, current_user)
|
|
47
54
|
end
|
|
48
55
|
end
|
|
49
56
|
|
|
@@ -60,6 +67,9 @@ module Labimotion
|
|
|
60
67
|
requires :identifier, type: String, desc: 'Identifier'
|
|
61
68
|
end
|
|
62
69
|
post do
|
|
70
|
+
# Importing needs the global create right; upgrading an existing template is gated
|
|
71
|
+
# owner-only inside the helper's update branch.
|
|
72
|
+
authenticate_admin!(Labimotion::Constants::Family::DATASET)
|
|
63
73
|
msg = create_repo_klass(params, current_user, request.headers['Origin'])
|
|
64
74
|
klass = Labimotion::DatasetKlassEntity.represent(Labimotion::DatasetKlass.all)
|
|
65
75
|
{ status: msg[:status], message: msg[:message], klass: klass }
|
|
@@ -69,6 +79,34 @@ module Labimotion
|
|
|
69
79
|
end
|
|
70
80
|
end
|
|
71
81
|
|
|
82
|
+
namespace :create_ai_klass do
|
|
83
|
+
desc 'create a Generic Dataset Klass from an AI-generated template'
|
|
84
|
+
params do
|
|
85
|
+
use :create_ai_dataset_klass_params
|
|
86
|
+
end
|
|
87
|
+
post do
|
|
88
|
+
msg = create_ai_dataset_klass(params, current_user)
|
|
89
|
+
klass = Labimotion::DatasetKlassEntity.represent(Labimotion::DatasetKlass.all)
|
|
90
|
+
{ status: msg[:status], message: msg[:message], klass: klass }
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
Labimotion.log_exception(e, current_user)
|
|
93
|
+
{ error: e.message }
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
namespace :refine_ai_klass do
|
|
98
|
+
desc 'refine a Generic Dataset Klass template with AI (returns the revised template; not persisted)'
|
|
99
|
+
params do
|
|
100
|
+
use :refine_ai_dataset_klass_params
|
|
101
|
+
end
|
|
102
|
+
post do
|
|
103
|
+
refine_ai_dataset_klass(params, current_user)
|
|
104
|
+
rescue StandardError => e
|
|
105
|
+
Labimotion.log_exception(e, current_user)
|
|
106
|
+
{ status: 'error', message: e.message }
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
72
110
|
namespace :find_template do
|
|
73
111
|
desc 'Find best matching template for given OLS term ID'
|
|
74
112
|
params do
|
|
@@ -178,7 +178,7 @@ module Labimotion
|
|
|
178
178
|
use :create_element_klass_params
|
|
179
179
|
end
|
|
180
180
|
post do
|
|
181
|
-
authenticate_admin!(
|
|
181
|
+
authenticate_admin!(Labimotion::Constants::Family::ELEMENT)
|
|
182
182
|
create_element_klass(current_user, params)
|
|
183
183
|
status 201
|
|
184
184
|
rescue ActiveRecord::RecordInvalid => e
|
|
@@ -186,13 +186,50 @@ module Labimotion
|
|
|
186
186
|
end
|
|
187
187
|
end
|
|
188
188
|
|
|
189
|
+
namespace :create_ai_klass do
|
|
190
|
+
desc 'create a Generic Element Klass from an AI-generated template'
|
|
191
|
+
params do
|
|
192
|
+
use :create_ai_element_klass_params
|
|
193
|
+
end
|
|
194
|
+
post do
|
|
195
|
+
authenticate_admin!('elements')
|
|
196
|
+
msg = create_ai_element_klass(params, current_user)
|
|
197
|
+
{ status: msg[:status], message: msg[:message],
|
|
198
|
+
klass: Labimotion::ElementKlassEntity.represent(Labimotion::ElementKlass.where(is_active: true)) }
|
|
199
|
+
rescue StandardError => e
|
|
200
|
+
Labimotion.log_exception(e, current_user)
|
|
201
|
+
{ error: e.message }
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# Sibling namespace of :create_ai_klass — MUST stay above the route_param :id
|
|
206
|
+
# blocks below, otherwise POST /ai_fill_data falls through to the element
|
|
207
|
+
# instance route and Element.find(nil) is raised.
|
|
208
|
+
namespace :ai_fill_data do
|
|
209
|
+
desc "Auto-fill a generic element's data values from a document using AI"
|
|
210
|
+
params do
|
|
211
|
+
use :ai_fill_element_data_params
|
|
212
|
+
end
|
|
213
|
+
post do
|
|
214
|
+
msg = ai_fill_element_data(params, current_user)
|
|
215
|
+
if msg[:status] == 'success'
|
|
216
|
+
{ status: 'success', values: msg[:values], summary: msg[:summary] }
|
|
217
|
+
else
|
|
218
|
+
{ status: 'error', message: msg[:message] }
|
|
219
|
+
end
|
|
220
|
+
rescue StandardError => e
|
|
221
|
+
Labimotion.log_exception(e, current_user)
|
|
222
|
+
{ status: 'error', message: e.message }
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
|
|
189
226
|
namespace :update_element_klass do
|
|
190
227
|
desc 'update Generic Element Klass'
|
|
191
228
|
params do
|
|
192
229
|
use :update_element_klass_params
|
|
193
230
|
end
|
|
194
231
|
post do
|
|
195
|
-
|
|
232
|
+
# authorize_klass!(:write) runs inside the helper, on the loaded record.
|
|
196
233
|
update_element_klass(current_user, params)
|
|
197
234
|
rescue StandardError => e
|
|
198
235
|
Labimotion.log_exception(e, current_user)
|
|
@@ -239,7 +276,7 @@ module Labimotion
|
|
|
239
276
|
requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass]
|
|
240
277
|
end
|
|
241
278
|
post do
|
|
242
|
-
|
|
279
|
+
# authorize_klass!(:destroy) runs inside the helper, once the klass row is loaded.
|
|
243
280
|
delete_klass_revision(params)
|
|
244
281
|
status 201
|
|
245
282
|
rescue StandardError => e
|
|
@@ -319,19 +356,26 @@ module Labimotion
|
|
|
319
356
|
scope = scope.where(is_active: params[:is_active]) if params.key?(:is_active)
|
|
320
357
|
|
|
321
358
|
list = scope.sort_by(&:place)
|
|
322
|
-
present list, with: Labimotion::ElementKlassEntity, root: 'klass',
|
|
359
|
+
present list, with: Labimotion::ElementKlassEntity, root: 'klass',
|
|
360
|
+
displayed_in_list: params[:displayed_in_list],
|
|
361
|
+
with_ownership: true, owners: Labimotion::OwnerResolver.map_for(list),
|
|
362
|
+
share_context: Labimotion::ShareResolver.context_for(list, current_user)
|
|
323
363
|
rescue StandardError => e
|
|
324
364
|
Labimotion.log_exception(e, current_user)
|
|
325
365
|
raise e
|
|
326
366
|
end
|
|
327
367
|
end
|
|
328
368
|
|
|
329
|
-
# Deprecated
|
|
369
|
+
# Deprecated as an endpoint, but it is what the Element Designer grid still calls
|
|
370
|
+
# (eln GenericElsFetcher.fetchElementKlasses), so ownership has to be exposed here too —
|
|
371
|
+
# adding `created_by` to `for_list_display` alone would never reach that grid.
|
|
330
372
|
namespace :klasses_all do
|
|
331
373
|
desc 'get all klasses for admin function'
|
|
332
374
|
get do
|
|
333
375
|
list = Labimotion::ElementKlass.all.sort_by { |e| e.place }
|
|
334
|
-
present list, with: Labimotion::ElementKlassEntity, root: 'klass'
|
|
376
|
+
present list, with: Labimotion::ElementKlassEntity, root: 'klass',
|
|
377
|
+
with_ownership: true, owners: Labimotion::OwnerResolver.map_for(list),
|
|
378
|
+
share_context: Labimotion::ShareResolver.context_for(list, current_user)
|
|
335
379
|
rescue StandardError => e
|
|
336
380
|
Labimotion.log_exception(e, current_user)
|
|
337
381
|
[]
|
|
@@ -354,6 +398,9 @@ module Labimotion
|
|
|
354
398
|
requires :identifier, type: String, desc: 'Identifier'
|
|
355
399
|
end
|
|
356
400
|
post do
|
|
401
|
+
# Importing needs the global create right; upgrading an existing template is gated
|
|
402
|
+
# owner-only inside validate_klass, where the create/update branch is decided.
|
|
403
|
+
authenticate_admin!(Labimotion::Constants::Family::ELEMENT)
|
|
357
404
|
msg = create_repo_klass(params, current_user, request.headers['Origin'])
|
|
358
405
|
klass = Labimotion::ElementKlassEntity.represent(Labimotion::ElementKlass.all)
|
|
359
406
|
{ status: msg[:status], message: msg[:message], klass: klass }
|
|
@@ -371,7 +418,8 @@ module Labimotion
|
|
|
371
418
|
requires :is_active, type: Boolean, desc: 'Active or Inactive Klass'
|
|
372
419
|
end
|
|
373
420
|
after_validation do
|
|
374
|
-
|
|
421
|
+
# The share gate needs the loaded record, so it runs inside the helper
|
|
422
|
+
# (authorize_klass!(:deactivate)) rather than up here where only params exist.
|
|
375
423
|
fetch_klass(params[:klass], params[:id])
|
|
376
424
|
end
|
|
377
425
|
post do
|
|
@@ -407,7 +455,8 @@ module Labimotion
|
|
|
407
455
|
optional :release, type: String, default: 'draft', desc: 'release status', values: %w[draft major minor patch]
|
|
408
456
|
end
|
|
409
457
|
after_validation do
|
|
410
|
-
|
|
458
|
+
# authorize_klass! (:write for a draft, :release otherwise) runs inside
|
|
459
|
+
# update_template, on the loaded record.
|
|
411
460
|
fetch_klass(params[:klass], params[:id])
|
|
412
461
|
end
|
|
413
462
|
post do
|
|
@@ -424,6 +473,9 @@ module Labimotion
|
|
|
424
473
|
use :upload_element_klass_params
|
|
425
474
|
end
|
|
426
475
|
post do
|
|
476
|
+
# Same split as create_repo_klass: create right here, owner-only upgrade inside
|
|
477
|
+
# validate_klass. This route previously had no gate at all.
|
|
478
|
+
authenticate_admin!(Labimotion::Constants::Family::ELEMENT)
|
|
427
479
|
declared_params = declared(params, include_missing: false)
|
|
428
480
|
attributes = declared_params.merge(
|
|
429
481
|
created_by: current_user.id,
|
|
@@ -17,15 +17,18 @@ module Labimotion
|
|
|
17
17
|
requires :klass, type: String, desc: 'Klass', values: Labimotion::Constants::Klass::ALL
|
|
18
18
|
end
|
|
19
19
|
get do
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
# This export previously accepted any logged-in account (no admin gate at all) and,
|
|
21
|
+
# worse, wrote through update_columns inside a GET — minting the identifier that Hub
|
|
22
|
+
# sync keys on, for whoever happened to press download first. Gated :read now, and a
|
|
23
|
+
# template with no identifier simply exports without one; minting stays with the
|
|
24
|
+
# write paths.
|
|
25
|
+
entity = fetch_klass(params[:klass], params[:id])
|
|
26
|
+
authorize_klass!(entity, :read)
|
|
22
27
|
env['api.format'] = :binary
|
|
23
28
|
content_type('application/json')
|
|
24
29
|
filename = CGI.escape("LabIMotion_#{params[:klass]}_#{entity.label}-#{Time.new.strftime("%Y%m%d%H%M%S")}.json")
|
|
25
|
-
# header['Content-Disposition'] = "attachment; filename=abc.docx"
|
|
26
30
|
header('Content-Disposition', "attachment; filename=\"#{filename}\"")
|
|
27
31
|
"Labimotion::#{params[:klass]}Entity".constantize.represent(entity)
|
|
28
|
-
# klass.as_json
|
|
29
32
|
rescue StandardError => e
|
|
30
33
|
Labimotion.log_exception(e, current_user)
|
|
31
34
|
{}
|
|
@@ -40,7 +43,7 @@ module Labimotion
|
|
|
40
43
|
requires :is_active, type: Boolean, desc: 'Active or Inactive Klass'
|
|
41
44
|
end
|
|
42
45
|
after_validation do
|
|
43
|
-
|
|
46
|
+
# authorize_klass!(:deactivate) runs inside the helper, on the loaded record.
|
|
44
47
|
fetch_klass(params[:klass], params[:id])
|
|
45
48
|
end
|
|
46
49
|
post do
|
|
@@ -62,7 +65,7 @@ module Labimotion
|
|
|
62
65
|
requires :settings, type: Hash, desc: 'Klass settings'
|
|
63
66
|
end
|
|
64
67
|
after_validation do
|
|
65
|
-
|
|
68
|
+
# authorize_klass!(:write) runs inside the helper, on the loaded record.
|
|
66
69
|
fetch_klass(params[:klass], params[:id])
|
|
67
70
|
end
|
|
68
71
|
post do
|
|
@@ -76,6 +79,47 @@ module Labimotion
|
|
|
76
79
|
end
|
|
77
80
|
end
|
|
78
81
|
|
|
82
|
+
namespace :deleted_klasses do
|
|
83
|
+
desc 'list the soft-deleted Generic Klasses of one type'
|
|
84
|
+
params do
|
|
85
|
+
requires :klass, type: String, desc: 'Klass', values: Labimotion::Constants::Klass::ALL
|
|
86
|
+
end
|
|
87
|
+
after_validation do
|
|
88
|
+
authenticate_admin!(Labimotion::Constants::Family::FAMILY_OF[params[:klass]])
|
|
89
|
+
end
|
|
90
|
+
get do
|
|
91
|
+
list = deleted_klasses(params)
|
|
92
|
+
entity_class = "Labimotion::#{params[:klass]}Entity".constantize
|
|
93
|
+
# `deleted_at` is not on the klass entities (a sibling PR owns those files), and a
|
|
94
|
+
# restore list that cannot say when a template was deleted is not much of a list.
|
|
95
|
+
serialized = list.map do |klz|
|
|
96
|
+
entity_class.represent(klz).as_json.merge(deleted_at: klz.deleted_at&.iso8601)
|
|
97
|
+
end
|
|
98
|
+
{ klass: serialized }
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
Labimotion.log_exception(e, current_user)
|
|
101
|
+
{ mc: 'se00', msg: e.message, klass: [] }
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
namespace :restore_klass do
|
|
106
|
+
desc 'restore a soft-deleted Generic Klass'
|
|
107
|
+
params do
|
|
108
|
+
requires :klass, type: String, desc: 'Klass', values: Labimotion::Constants::Klass::ALL
|
|
109
|
+
requires :id, type: Integer, desc: 'Klass ID'
|
|
110
|
+
end
|
|
111
|
+
# authorize_klass!(:destroy) runs inside the helper, on the loaded deleted record —
|
|
112
|
+
# shares survive deletion, so the owner row is still there to gate the restore.
|
|
113
|
+
post do
|
|
114
|
+
restored_klass = restore_klass(params)
|
|
115
|
+
entity_class = "Labimotion::#{params[:klass]}Entity".constantize
|
|
116
|
+
{ mc: 'ss00', data: entity_class.represent(restored_klass) }
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
Labimotion.log_exception(e, current_user)
|
|
119
|
+
{ mc: 'se00', msg: e.message, data: {} }
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
79
123
|
namespace :fetch do
|
|
80
124
|
desc 'fetch Generic Klass by id'
|
|
81
125
|
params do
|