labimotion 2.3.2 → 2.4.0.rc1
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 +0 -22
- data/lib/labimotion/apis/element_variation_api.rb +3 -58
- data/lib/labimotion/apis/generic_element_api.rb +22 -0
- data/lib/labimotion/collection/import.rb +3 -36
- data/lib/labimotion/constants.rb +0 -6
- data/lib/labimotion/entities/application_entity.rb +1 -5
- data/lib/labimotion/entities/properties_entity.rb +28 -0
- data/lib/labimotion/helpers/element_helpers.rb +5 -27
- data/lib/labimotion/helpers/param_helpers.rb +1 -0
- data/lib/labimotion/libs/converter.rb +22 -99
- data/lib/labimotion/libs/linked_element.rb +193 -0
- data/lib/labimotion/libs/nmr_mapper.rb +4 -34
- data/lib/labimotion/models/concerns/attachment_converter.rb +0 -24
- data/lib/labimotion/models/concerns/datasetable.rb +5 -16
- data/lib/labimotion/models/concerns/element_fetchable.rb +2 -3
- data/lib/labimotion/models/concerns/segmentable.rb +5 -24
- data/lib/labimotion/models/element.rb +2 -3
- data/lib/labimotion/utils/mapper_utils.rb +5 -30
- data/lib/labimotion/utils/prop.rb +0 -1
- data/lib/labimotion/utils/units.rb +32 -67
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +1 -11
- metadata +5 -24
- data/lib/labimotion/libs/element_variation_column_set.rb +0 -426
- data/lib/labimotion/libs/element_variation_header.rb +0 -119
- data/lib/labimotion/libs/export_element_variations.rb +0 -245
- data/lib/labimotion/libs/export_tpa_segments.rb +0 -53
- data/lib/labimotion/libs/import_element_variations.rb +0 -411
- data/lib/labimotion/libs/klass_version.rb +0 -66
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20affa359afd5082ac4bc5e900b3e66c2c537a4b83fc9032883e33f25dc60362
|
|
4
|
+
data.tar.gz: e8bb4ae665e5de5166790ac27ecf42a95394057a2fd93d5deebc95dc858a5189
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7e6d407b44837ccb8758aa2a86b6c4b109350dfa50dde7ebb3affae93b070c1efba512b4db2b08f0608b165a25c1a53c4caaa3450f55c1a15e5c1401a1ca87de
|
|
7
|
+
data.tar.gz: 60c1e3df69128dda039990086d98ad816e7fcedbf74e1a90b896b9b94eb7446b3e5feb613e42770be1f8807d72e5ee3a7c07ec8eb8e158b776d5904335d07671
|
data/CHANGELOG.md
CHANGED
|
@@ -1,27 +1,5 @@
|
|
|
1
1
|
# LabIMotion Changelog
|
|
2
2
|
|
|
3
|
-
## [Unreleased]
|
|
4
|
-
* Features and enhancements:
|
|
5
|
-
* Element variations can be exported to and imported from Excel (`.xlsx`). The exported workbook is itself the import template: `GET /api/v1/element_variations/:element_id/export` and `POST /api/v1/element_variations/:element_id/import`.
|
|
6
|
-
* The exported sheet carries the grid's grouped header -- group, sub-group and field rows, merged as the grid draws them -- above the hidden key and unit rows. Workbooks from the earlier single-row header still import.
|
|
7
|
-
* Import merges rows by uuid, so a sheet only updates the columns it carries; a row with a blank `Row ID` is added as a new variation.
|
|
8
|
-
* `number`, `integer` and `system-defined` columns accept numbers only. A cell holding anything else is cleared with a warning rather than stored as text, which also cleans up text those columns already hold.
|
|
9
|
-
* Import warnings name the cell they came from (`Cell I6: ...`), so the user can go straight to it in the spreadsheet.
|
|
10
|
-
* Bug fixes:
|
|
11
|
-
* A copied element, segment or dataset keeps the template release its properties were built on (labimotion#301, labimotion#303). Saves used to stamp every new segment and every dataset save with the klass's current `uuid` and `properties_release`, and an element with whatever the client sent, so a copy of a v1.0.0 record was labelled v2.0.0 and the client never offered Reload. `Labimotion::KlassVersion` now resolves the submitted `properties['klass_uuid']` against the klass's current release and its `*_klasses_revisions`, deleted revisions included, and stores that uuid together with the matching `properties_release`. A new record whose uuid is missing or unknown follows the current release; an existing record keeps its stored uuid when none is sent, then the uuid its stored `properties_release` names, and follows the current release only when it has neither, and a uuid this instance never released (a record imported from an instance with its own release history) keeps the release stored with the record. A new element whose properties carry no uuid follows the template the client built it on; a new segment does the same, and a copied one falls back to the `klass_uuid` of the row it was copied from. The collection import stamps the release the archive names instead of the importing instance's current one: it tries the uuid inside the archived `properties_release`, then `properties['klass_uuid']`, then the `klass_uuid` column, and stores the first one this instance knows with the local release; when none resolves, the archived release keeps the uuid it names (or, naming none, the first uuid the archive carries), and the imported properties carry the same `klass_uuid` as the column. A `klass_uuid` that is not a string is ignored, and a dataset whose klass changed follows the new klass and ignores the client's release. Element create returns 404 when the template is missing or deleted, element update resolves a soft-deleted template (`with_deleted`) before writing anything, and segments and datasets of a deleted template are skipped on save. A new record whose `klass_uuid` this instance cannot resolve keeps it with the `properties_release` the client sent, only when that release names the same uuid. No migration and no client change.
|
|
12
|
-
* `fetch_for_user`: the shared-records branch joined `collection_shares` directly on the element model (an association that only exists on `Collection`), raising `ActiveRecord::ConfigurationError` — rescued into empty search results. Now joins through `collections`. Fixed for both the basic elements (`ElementFetchable`) and the generic element (`Labimotion::Element`), so `search_by_like` and `search_basic_by_like` return shared records again.
|
|
13
|
-
* Dependencies:
|
|
14
|
-
* Added `roo` (~> 2.10) for reading the uploaded workbook.
|
|
15
|
-
|
|
16
|
-
## [2.1.2]
|
|
17
|
-
> Unreleased
|
|
18
|
-
* Bug fixes:
|
|
19
|
-
* Stored zip attachments are opened read-only, so they are never rewritten in place. A rewrite could delete the upload on a CIFS/SMB share.
|
|
20
|
-
* A converter or NMR mapping failure no longer fails the save: the attachment ends in `ERROR` and the error is reported to Sentry. A file that is not a zip ends in `NONE`.
|
|
21
|
-
* Encrypted zips end in `ERROR` instead of being read as garbage.
|
|
22
|
-
* Maintenance:
|
|
23
|
-
* `ERROR` is terminal, also for transient faults. To retry, e.g. after restoring a lost file, call `attachment.reprocess_converter!` in the Rails console.
|
|
24
|
-
|
|
25
3
|
## [2.0.0]
|
|
26
4
|
> 2025-03-25
|
|
27
5
|
* Features and enhancements:
|
|
@@ -1,39 +1,15 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'uri'
|
|
4
|
-
|
|
5
3
|
module Labimotion
|
|
6
4
|
class ElementVariationAPI < Grape::API
|
|
7
|
-
XLSX_CONTENT_TYPE = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
8
|
-
|
|
9
5
|
rescue_from ActiveRecord::RecordNotFound do
|
|
10
6
|
error!('404 Element not found', 404)
|
|
11
7
|
end
|
|
12
8
|
|
|
13
|
-
helpers do
|
|
14
|
-
def configure_xlsx_response(filename)
|
|
15
|
-
env['api.format'] = :binary
|
|
16
|
-
content_type XLSX_CONTENT_TYPE
|
|
17
|
-
encoded = URI.encode_www_form_component(filename)
|
|
18
|
-
header('Content-Disposition', "attachment; filename=\"#{filename}\"; filename*=UTF-8''#{encoded}")
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
def uploaded_xlsx_path!(file)
|
|
22
|
-
filename = file[:filename].to_s
|
|
23
|
-
error!('400 Bad Request - expected an .xlsx file', 400) unless filename.downcase.end_with?('.xlsx')
|
|
24
|
-
|
|
25
|
-
tempfile = file[:tempfile]
|
|
26
|
-
error!('400 Bad Request - upload is empty', 400) if tempfile.nil?
|
|
27
|
-
|
|
28
|
-
tempfile.path
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
9
|
resource :element_variations do
|
|
33
10
|
params do
|
|
34
11
|
requires :element_id, type: Integer, desc: 'Generic element id'
|
|
35
12
|
end
|
|
36
|
-
# rubocop:disable Metrics/BlockLength
|
|
37
13
|
route_param :element_id do
|
|
38
14
|
before do
|
|
39
15
|
@element = Labimotion::Element.find(params[:element_id])
|
|
@@ -56,45 +32,14 @@ module Labimotion
|
|
|
56
32
|
|
|
57
33
|
record = Labimotion::ElementVariation.find_or_initialize_by(element_id: @element.id)
|
|
58
34
|
record.variations = params[:variations] || {}
|
|
59
|
-
|
|
35
|
+
if params.key?(:layout) && record.class.column_names.include?('layout')
|
|
36
|
+
record.layout = params[:layout] || {}
|
|
37
|
+
end
|
|
60
38
|
record.save!
|
|
61
39
|
|
|
62
40
|
present record, with: Labimotion::ElementVariationEntity, root: 'element_variation'
|
|
63
41
|
end
|
|
64
|
-
|
|
65
|
-
desc 'Export element variations as an xlsx workbook'
|
|
66
|
-
get :export do
|
|
67
|
-
exporter = Labimotion::ExportElementVariations.new(@element, user: current_user)
|
|
68
|
-
configure_xlsx_response(exporter.filename)
|
|
69
|
-
exporter.read
|
|
70
|
-
rescue StandardError => e
|
|
71
|
-
Labimotion.log_exception(e, current_user)
|
|
72
|
-
error!("500 Internal Server Error: #{e.message}", 500)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
desc 'Import element variations from an xlsx workbook produced by the export'
|
|
76
|
-
params do
|
|
77
|
-
requires :file, type: File, desc: 'xlsx file'
|
|
78
|
-
end
|
|
79
|
-
post :import do
|
|
80
|
-
error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, @element).update?
|
|
81
|
-
|
|
82
|
-
path = uploaded_xlsx_path!(params[:file])
|
|
83
|
-
importer = Labimotion::ImportElementVariations.new(@element, path)
|
|
84
|
-
record = importer.execute!
|
|
85
|
-
|
|
86
|
-
{
|
|
87
|
-
element_variation: Labimotion::ElementVariationEntity.represent(record),
|
|
88
|
-
warnings: importer.warnings
|
|
89
|
-
}
|
|
90
|
-
rescue Labimotion::ImportElementVariations::InvalidWorkbook => e
|
|
91
|
-
error!(e.message, 422)
|
|
92
|
-
rescue StandardError => e
|
|
93
|
-
Labimotion.log_exception(e, current_user)
|
|
94
|
-
error!("500 Internal Server Error: #{e.message}", 500)
|
|
95
|
-
end
|
|
96
42
|
end
|
|
97
|
-
# rubocop:enable Metrics/BlockLength
|
|
98
43
|
end
|
|
99
44
|
end
|
|
100
45
|
end
|
|
@@ -88,6 +88,28 @@ module Labimotion
|
|
|
88
88
|
end
|
|
89
89
|
end
|
|
90
90
|
|
|
91
|
+
namespace :linked_attributes do
|
|
92
|
+
desc 'List the selectable inline attributes of an element linked from a drag_element field'
|
|
93
|
+
params do
|
|
94
|
+
optional :el_klass, type: String, desc: 'Generic element klass name or permit-target key (reaction, sample, ...)'
|
|
95
|
+
optional :el_type, type: String, desc: 'Linked element type (element, sample, molecule, ...)'
|
|
96
|
+
requires :el_id, type: Integer, desc: 'Linked element id'
|
|
97
|
+
end
|
|
98
|
+
get do
|
|
99
|
+
linked = Labimotion::LinkedElement.new(params[:el_klass], params[:el_id], params[:el_type])
|
|
100
|
+
record = linked.record
|
|
101
|
+
error!('404 Not Found', 404) if record.nil?
|
|
102
|
+
# Reference data (molecules) is public; everything else is collection-scoped.
|
|
103
|
+
readable = record.is_a?(::Molecule) || ElementPolicy.new(current_user, record).read?
|
|
104
|
+
error!('401 Unauthorized', 401) unless readable
|
|
105
|
+
|
|
106
|
+
{ attributes: linked.available }
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
Labimotion.log_exception(e, current_user)
|
|
109
|
+
{ attributes: [], error: e.message }
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
91
113
|
namespace :search_basic_by_like do
|
|
92
114
|
desc 'Search basic elements by name and short label (case-insensitive like search)'
|
|
93
115
|
params do
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'labimotion/utils/import_utils'
|
|
4
|
-
require 'labimotion/libs/klass_version'
|
|
5
4
|
module Labimotion
|
|
6
5
|
class Import
|
|
7
6
|
def self.import_repo_segment_props(instances, attachments, attachable_uuid, fields)
|
|
@@ -68,8 +67,6 @@ module Labimotion
|
|
|
68
67
|
dataset_klass = Labimotion::DatasetKlass.find_by(ols_term_id: dk_obj['ols_term_id']) if dataset_klass.nil?
|
|
69
68
|
next if dataset_klass.nil?
|
|
70
69
|
|
|
71
|
-
klass_uuid, release = archived_version(dataset_klass, dataset_klass.dataset_klasses_revisions, fields)
|
|
72
|
-
klass_uuid ||= dkr&.uuid || dataset_klass.uuid
|
|
73
70
|
dataset = Labimotion::Dataset.create!(
|
|
74
71
|
fields.slice(
|
|
75
72
|
'properties', 'properties_release', 'metadata'
|
|
@@ -78,8 +75,7 @@ module Labimotion
|
|
|
78
75
|
element: element,
|
|
79
76
|
dataset_klass: dataset_klass,
|
|
80
77
|
uuid: SecureRandom.uuid,
|
|
81
|
-
klass_uuid:
|
|
82
|
-
'properties' => stamped_properties(fields['properties'], klass_uuid)
|
|
78
|
+
klass_uuid: dkr&.uuid || dataset_klass.uuid
|
|
83
79
|
)
|
|
84
80
|
)
|
|
85
81
|
update_instances.call(uuid, dataset)
|
|
@@ -114,8 +110,6 @@ module Labimotion
|
|
|
114
110
|
|
|
115
111
|
## segment_klass = Labimotion::ImportUtils.create_segment_klass(sk_obj, segment_klass, element_klass, current_user_id)
|
|
116
112
|
|
|
117
|
-
klass_uuid, release = archived_version(segment_klass, segment_klass.segment_klasses_revisions, fields)
|
|
118
|
-
klass_uuid ||= skr&.uuid || segment_klass.uuid
|
|
119
113
|
segment = Labimotion::Segment.create!(
|
|
120
114
|
fields.slice(
|
|
121
115
|
'properties', 'properties_release', 'metadata'
|
|
@@ -124,8 +118,7 @@ module Labimotion
|
|
|
124
118
|
element: element,
|
|
125
119
|
segment_klass: segment_klass,
|
|
126
120
|
uuid: SecureRandom.uuid,
|
|
127
|
-
klass_uuid:
|
|
128
|
-
'properties' => stamped_properties(fields['properties'], klass_uuid)
|
|
121
|
+
klass_uuid: skr&.uuid || segment_klass.uuid
|
|
129
122
|
)
|
|
130
123
|
)
|
|
131
124
|
properties = Labimotion::ImportUtils.properties_handler(data, instances, attachments, segment, nil)
|
|
@@ -154,8 +147,6 @@ module Labimotion
|
|
|
154
147
|
end
|
|
155
148
|
next if element_klass.nil?
|
|
156
149
|
|
|
157
|
-
klass_uuid, release = archived_version(element_klass, element_klass.element_klasses_revisions, fields)
|
|
158
|
-
klass_uuid ||= ekr&.uuid || element_klass.uuid
|
|
159
150
|
element = Labimotion::Element.create!(
|
|
160
151
|
fields.slice(
|
|
161
152
|
'name', 'properties', 'properties_release', 'metadata'
|
|
@@ -166,8 +157,7 @@ module Labimotion
|
|
|
166
157
|
'Collection', 'Labimotion::CollectionsElement', 'element_id', 'collection_id', uuid
|
|
167
158
|
),
|
|
168
159
|
uuid: SecureRandom.uuid,
|
|
169
|
-
klass_uuid:
|
|
170
|
-
'properties' => stamped_properties(fields['properties'], klass_uuid)
|
|
160
|
+
klass_uuid: ekr&.uuid || element_klass.uuid
|
|
171
161
|
)
|
|
172
162
|
)
|
|
173
163
|
elements[uuid] = element
|
|
@@ -184,28 +174,5 @@ module Labimotion
|
|
|
184
174
|
Labimotion.log_exception(e)
|
|
185
175
|
raise
|
|
186
176
|
end
|
|
187
|
-
|
|
188
|
-
# The archive names the release a record followed in three places: the uuid inside its
|
|
189
|
-
# properties_release, properties['klass_uuid'] and the klass_uuid column. The first one this
|
|
190
|
-
# instance resolves is stored with the local release; otherwise the archive's release keeps
|
|
191
|
-
# its own uuid, so the pair stays consistent and the Reload the properties need still shows.
|
|
192
|
-
def self.archived_version(klass, revisions, fields)
|
|
193
|
-
archived_release = fields['properties_release']
|
|
194
|
-
release_uuid = Labimotion::KlassVersion.release_uuid(archived_release)
|
|
195
|
-
uuids = [release_uuid, fields.dig('properties', 'klass_uuid'), fields['klass_uuid']]
|
|
196
|
-
.filter_map { |uuid| Labimotion::KlassVersion.string_uuid(uuid).presence }.uniq
|
|
197
|
-
uuids.each do |uuid|
|
|
198
|
-
release = Labimotion::KlassVersion.release_of(klass, revisions, uuid)
|
|
199
|
-
return [uuid, release] if release
|
|
200
|
-
end
|
|
201
|
-
[release_uuid || uuids.first, archived_release]
|
|
202
|
-
end
|
|
203
|
-
private_class_method :archived_version
|
|
204
|
-
|
|
205
|
-
# The stored properties name the same release as the klass_uuid column.
|
|
206
|
-
def self.stamped_properties(properties, klass_uuid)
|
|
207
|
-
properties.is_a?(Hash) ? properties.merge('klass_uuid' => klass_uuid) : properties
|
|
208
|
-
end
|
|
209
|
-
private_class_method :stamped_properties
|
|
210
177
|
end
|
|
211
178
|
end
|
data/lib/labimotion/constants.rb
CHANGED
|
@@ -28,11 +28,5 @@ module Labimotion
|
|
|
28
28
|
DATASET = 'DatasetKlass'
|
|
29
29
|
ALL = [ELEMENT, SEGMENT, DATASET].freeze
|
|
30
30
|
end
|
|
31
|
-
|
|
32
|
-
module Tpa
|
|
33
|
-
# Segments and layers sent along with reaction variations to a third-party
|
|
34
|
-
# app (see ExportTpaSegments), as segment-klass label => layer keys.
|
|
35
|
-
SEGMENT_LAYERS = { 'Mol interactions' => %w[binding_props bind_partners].freeze }.freeze
|
|
36
|
-
end
|
|
37
31
|
end
|
|
38
32
|
end
|
|
@@ -8,12 +8,8 @@ module Labimotion
|
|
|
8
8
|
# Common condition for fields that should not be displayed in list views
|
|
9
9
|
DISPLAYED_IN_LIST_CONDITION = { unless: :displayed_in_list }.freeze
|
|
10
10
|
|
|
11
|
-
# ISO 8601. A zone *name* (strftime '%Z', e.g. "UTC") is not a valid ISO zone designator, so
|
|
12
|
-
# clients parsing it with moment.js fall back to `new Date()` — engine-specific behaviour.
|
|
13
|
-
ELN_TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%S%z'
|
|
14
|
-
|
|
15
11
|
format_with(:eln_timestamp) do |datetime|
|
|
16
|
-
datetime.present? ? datetime.strftime(
|
|
12
|
+
datetime.present? ? datetime.strftime('%Y-%m-%d %H:%M:%S %Z') : nil
|
|
17
13
|
end
|
|
18
14
|
end
|
|
19
15
|
end
|
|
@@ -20,6 +20,7 @@ module Labimotion
|
|
|
20
20
|
|
|
21
21
|
def process_fields(key, layer)
|
|
22
22
|
process_sample_and_molecule_fields(key, layer)
|
|
23
|
+
process_linked_attr_fields(key, layer)
|
|
23
24
|
process_reaction_fields(key, layer)
|
|
24
25
|
process_table_fields(key, layer)
|
|
25
26
|
# process_voc_fields(key, layer)
|
|
@@ -32,6 +33,18 @@ module Labimotion
|
|
|
32
33
|
end
|
|
33
34
|
end
|
|
34
35
|
|
|
36
|
+
# Resolve the linked attributes a user chose to surface inline on a drag field
|
|
37
|
+
# (stored as value['el_attrs']). Only the selection is persisted; the current
|
|
38
|
+
# values are refreshed here from the linked element on every load. Works for
|
|
39
|
+
# drag_element (generic element or any permit-target type) as well as
|
|
40
|
+
# drag_sample / drag_molecule.
|
|
41
|
+
def process_linked_attr_fields(key, layer)
|
|
42
|
+
select_fields(layer, [Labimotion::FieldType::DRAG_ELEMENT, Labimotion::FieldType::DRAG_SAMPLE,
|
|
43
|
+
Labimotion::FieldType::DRAG_MOLECULE]).each do |field, idx|
|
|
44
|
+
update_linked_attr_field(key, field, idx)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
35
48
|
def process_reaction_fields(key, layer)
|
|
36
49
|
select_fields(layer, [Labimotion::FieldType::SYS_REACTION]).each do |field, idx|
|
|
37
50
|
update_reaction_field(key, field, idx)
|
|
@@ -80,6 +93,21 @@ module Labimotion
|
|
|
80
93
|
})
|
|
81
94
|
end
|
|
82
95
|
|
|
96
|
+
def update_linked_attr_field(key, field, idx)
|
|
97
|
+
return unless field['value'].is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
attrs = field.dig('value', 'el_attrs')
|
|
100
|
+
return unless attrs.is_a?(Array) && attrs.present?
|
|
101
|
+
return unless object.properties.dig(Labimotion::Prop::LAYERS, key, Labimotion::Prop::FIELDS, idx,
|
|
102
|
+
'value').present?
|
|
103
|
+
|
|
104
|
+
el_klass = field.dig('value', 'el_klass')
|
|
105
|
+
el_id = field.dig('value', 'el_id')
|
|
106
|
+
el_type = field.dig('value', 'el_type')
|
|
107
|
+
object.properties[Labimotion::Prop::LAYERS][key][Labimotion::Prop::FIELDS][idx]['value']['el_attrs'] =
|
|
108
|
+
Labimotion::LinkedElement.new(el_klass, el_id, el_type).resolve(attrs)
|
|
109
|
+
end
|
|
110
|
+
|
|
83
111
|
def update_reaction_field(key, field, idx)
|
|
84
112
|
return unless field['value'].is_a?(Hash)
|
|
85
113
|
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
require 'grape'
|
|
4
4
|
require 'labimotion/conf'
|
|
5
5
|
require 'labimotion/utils/utils'
|
|
6
|
-
require 'labimotion/libs/klass_version'
|
|
7
6
|
# require 'labimotion/models/element_klass'
|
|
8
7
|
module Labimotion
|
|
9
8
|
## ElementHelpers
|
|
@@ -66,19 +65,9 @@ module Labimotion
|
|
|
66
65
|
|
|
67
66
|
def create_element(current_user, params)
|
|
68
67
|
klass = params[:element_klass] || {}
|
|
69
|
-
element_klass = Labimotion::ElementKlass.find_by(id: klass[:id])
|
|
70
|
-
error!('Template is invalid.', 404) if element_klass.nil?
|
|
71
|
-
|
|
72
|
-
# A new element's properties carry no klass_uuid; the template the client built them on
|
|
73
|
-
# names their release, which may be older than the current one after a release mid-session.
|
|
74
|
-
klass_uuid, release = Labimotion::KlassVersion.for_create(
|
|
75
|
-
element_klass, element_klass.element_klasses_revisions,
|
|
76
|
-
Labimotion::KlassVersion.string_uuid(params[:properties]['klass_uuid']).presence || klass[:uuid],
|
|
77
|
-
client_release: params[:properties_release]
|
|
78
|
-
)
|
|
79
68
|
uuid = SecureRandom.uuid
|
|
80
69
|
params[:properties]['uuid'] = uuid
|
|
81
|
-
params[:properties]['klass_uuid'] =
|
|
70
|
+
params[:properties]['klass_uuid'] = klass[:uuid]
|
|
82
71
|
params[:properties]['pkg'] = Labimotion::Utils.pkg(params[:properties]['pkg'])
|
|
83
72
|
params[:properties]['klass'] = 'Element'
|
|
84
73
|
params[:properties]['identifier'] = klass[:identifier]
|
|
@@ -90,9 +79,9 @@ module Labimotion
|
|
|
90
79
|
name: params[:name],
|
|
91
80
|
element_klass_id: klass[:id],
|
|
92
81
|
uuid: uuid,
|
|
93
|
-
klass_uuid:
|
|
82
|
+
klass_uuid: klass[:uuid],
|
|
94
83
|
properties: properties,
|
|
95
|
-
properties_release:
|
|
84
|
+
properties_release: params[:properties_release],
|
|
96
85
|
metadata: params[:metadata] || {},
|
|
97
86
|
created_by: current_user.id
|
|
98
87
|
}
|
|
@@ -121,12 +110,6 @@ module Labimotion
|
|
|
121
110
|
|
|
122
111
|
def update_element_by_id(current_user, params)
|
|
123
112
|
element = Labimotion::Element.find(params[:id])
|
|
124
|
-
# Resolved before the container and sample-association writes, so a missing template fails
|
|
125
|
-
# the request whole; with_deleted because a deleted template still names the release the
|
|
126
|
-
# element's properties follow. error! throws, so it escapes this method's rescue-and-log.
|
|
127
|
-
element_klass = Labimotion::ElementKlass.with_deleted.find_by(id: element.element_klass_id)
|
|
128
|
-
error!('Template is invalid.', 404) if element_klass.nil?
|
|
129
|
-
|
|
130
113
|
update_datamodel(params[:container], current_user)
|
|
131
114
|
properties = update_sample_association(params[:properties], current_user, element)
|
|
132
115
|
params.delete(:container)
|
|
@@ -136,11 +119,7 @@ module Labimotion
|
|
|
136
119
|
attributes = declared(params.except(:segments), include_missing: false)
|
|
137
120
|
properties['pkg'] = Labimotion::Utils.pkg(properties['pkg'])
|
|
138
121
|
metadata = params[:metadata] || element.metadata || {}
|
|
139
|
-
klass_uuid
|
|
140
|
-
element_klass, element_klass.element_klasses_revisions, properties['klass_uuid'], element
|
|
141
|
-
)
|
|
142
|
-
properties['klass_uuid'] = klass_uuid if klass_uuid.present?
|
|
143
|
-
if element.klass_uuid != klass_uuid || element.properties != properties || element.name != params[:name] || element.metadata != metadata
|
|
122
|
+
if element.klass_uuid != properties['klass_uuid'] || element.properties != properties || element.name != params[:name] || element.metadata != metadata
|
|
144
123
|
properties['klass'] = 'Element'
|
|
145
124
|
uuid = SecureRandom.uuid
|
|
146
125
|
properties['uuid'] = uuid
|
|
@@ -151,8 +130,7 @@ module Labimotion
|
|
|
151
130
|
attributes['properties'] = properties
|
|
152
131
|
attributes['properties']['uuid'] = uuid
|
|
153
132
|
attributes['uuid'] = uuid
|
|
154
|
-
attributes['klass_uuid'] = klass_uuid
|
|
155
|
-
attributes['properties_release'] = release
|
|
133
|
+
attributes['klass_uuid'] = properties['klass_uuid']
|
|
156
134
|
attributes['metadata'] = metadata
|
|
157
135
|
attributes['updated_at'] = Time.current
|
|
158
136
|
element.update_columns(attributes)
|
|
@@ -91,6 +91,7 @@ module Labimotion
|
|
|
91
91
|
optional :desc, type: String, desc: 'Segment Klass Desc'
|
|
92
92
|
optional :place, type: String, desc: 'Segment Klass Place', default: '100'
|
|
93
93
|
optional :identifier, type: String, desc: 'Segment Identifier'
|
|
94
|
+
optional :metadata, type: Hash, desc: 'Klass metadata'
|
|
94
95
|
end
|
|
95
96
|
|
|
96
97
|
params :create_segment_klass_params do
|
|
@@ -6,7 +6,6 @@ require 'json'
|
|
|
6
6
|
require 'date'
|
|
7
7
|
require 'labimotion/version'
|
|
8
8
|
require 'labimotion/utils/utils'
|
|
9
|
-
require 'labimotion/utils/mapper_utils'
|
|
10
9
|
require 'labimotion/helpers/converter_helpers'
|
|
11
10
|
|
|
12
11
|
# rubocop: disable Metrics/AbcSize
|
|
@@ -21,37 +20,27 @@ module Labimotion
|
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
def self.process_ds(id, current_user = {})
|
|
24
|
-
|
|
25
|
-
# Savepoint: a failure leaves no half-built dataset and keeps the caller's transaction usable
|
|
26
|
-
Attachment.transaction(requires_new: true) { map_ds(id, current_user, ctx) }
|
|
27
|
-
rescue StandardError => e
|
|
28
|
-
Labimotion::Converter.logger.error ["Att ID: #{ctx[:att]&.id}, OLS: #{ctx[:ols]}", "DSR: #{ctx[:dsr]}", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR)
|
|
29
|
-
Labimotion.report_exception(e)
|
|
30
|
-
# Terminal state: do not fail the request, and do not retry on the next save
|
|
31
|
-
ctx[:att]&.update_column(:con_state, Labimotion::ConState::ERROR)
|
|
32
|
-
Labimotion::ConState::ERROR
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def self.map_ds(id, current_user, ctx)
|
|
36
|
-
att = ctx[:att] = Attachment.find_by(id: id, con_state: Labimotion::ConState::CONVERTED)
|
|
23
|
+
att = Attachment.find_by(id: id, con_state: Labimotion::ConState::CONVERTED)
|
|
37
24
|
return if att.nil? || att.attachable_id.nil? || att.attachable_type != 'Container' || att.filename.split('.')&.last != 'zip'
|
|
38
25
|
|
|
39
26
|
eln_ds = Container.find_by(id: att.attachable_id, container_type: 'dataset')
|
|
40
27
|
return if eln_ds.nil? || eln_ds.parent.nil? || eln_ds.parent&.container_type != 'analysis'
|
|
41
28
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
29
|
+
dsr = []
|
|
30
|
+
ols = nil
|
|
31
|
+
Zip::File.open(att.attachment_attacher.file.url) do |zip_file|
|
|
32
|
+
res = Labimotion::Converter.collect_metadata(zip_file, current_user) if att.filename.split('.')&.last == 'zip'
|
|
33
|
+
ols = res[:o] unless res&.dig(:o).nil?
|
|
34
|
+
dsr.push(res[:d]) unless res&.dig(:d).nil?
|
|
35
|
+
end
|
|
36
|
+
dsr.flatten!
|
|
37
|
+
dataset = build_ds(att.attachable_id, ols)
|
|
38
|
+
update_ds(dataset, dsr, current_user) if dataset.present?
|
|
51
39
|
att.update_column(:con_state, Labimotion::ConState::COMPLETED)
|
|
52
|
-
|
|
40
|
+
rescue StandardError => e
|
|
41
|
+
Labimotion::Converter.logger.error ["Att ID: #{att&.id}, OLS: #{ols}", "DSR: #{dsr}", e.message, *e.backtrace].join($INPUT_RECORD_SEPARATOR)
|
|
42
|
+
raise e
|
|
53
43
|
end
|
|
54
|
-
private_class_method :map_ds
|
|
55
44
|
|
|
56
45
|
def self.uri(api_name)
|
|
57
46
|
url = Rails.configuration.converter.url
|
|
@@ -104,40 +93,13 @@ module Labimotion
|
|
|
104
93
|
{ a: oa, f: folder }
|
|
105
94
|
end
|
|
106
95
|
|
|
107
|
-
def self.collect_reaction_converter_metadata(oat, zip_file)
|
|
108
|
-
zip_file.each do |entry|
|
|
109
|
-
next unless entry.name == 'metadata/reaction_variation.json'
|
|
110
|
-
|
|
111
|
-
# Create temp file
|
|
112
|
-
tmp_file = Tempfile.new(['reaction_variation', '.json'])
|
|
113
|
-
tmp_file.binmode
|
|
114
|
-
|
|
115
|
-
# Write ZIP entry content into temp file
|
|
116
|
-
entry.extract(tmp_file.path) { true }
|
|
117
|
-
|
|
118
|
-
# Create attachment
|
|
119
|
-
att = Attachment.create!(
|
|
120
|
-
filename: 'reaction_variation.json',
|
|
121
|
-
file_path: tmp_file.path,
|
|
122
|
-
content_type: 'application/json',
|
|
123
|
-
attachable_id: oat.attachable_id,
|
|
124
|
-
attachable_type: 'Container',
|
|
125
|
-
con_state: Labimotion::ConState::CONVERTED,
|
|
126
|
-
created_by: oat.created_by,
|
|
127
|
-
created_for: oat.created_for
|
|
128
|
-
)
|
|
129
|
-
|
|
130
|
-
att.save! if att.valid?
|
|
131
|
-
end
|
|
132
|
-
end
|
|
133
|
-
|
|
134
96
|
def self.collect_metadata(zip_file, current_user = {}) # rubocop: disable Metrics/PerceivedComplexity
|
|
135
97
|
dsr = []
|
|
136
98
|
ols = nil
|
|
137
99
|
zip_file.each do |entry|
|
|
138
100
|
next unless entry.name == 'metadata/converter.json'
|
|
139
101
|
|
|
140
|
-
metadata =
|
|
102
|
+
metadata = entry.get_input_stream.read.force_encoding('UTF-8')
|
|
141
103
|
jdata = JSON.parse(metadata)
|
|
142
104
|
|
|
143
105
|
ols = jdata['ols']
|
|
@@ -163,6 +125,7 @@ module Labimotion
|
|
|
163
125
|
tmp_file.rewind
|
|
164
126
|
|
|
165
127
|
filename = oat.filename
|
|
128
|
+
# name = "#{File.basename(filename, '.*')}.zip"
|
|
166
129
|
name = "#{File.basename(filename, '.*')}#{File.extname(filename) == '.zip' ? '.bagit.zip' : '.zip'}"
|
|
167
130
|
att = Attachment.new(
|
|
168
131
|
filename: name,
|
|
@@ -202,9 +165,8 @@ module Labimotion
|
|
|
202
165
|
)
|
|
203
166
|
end
|
|
204
167
|
if response.ok?
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
state == Labimotion::ConState::ERROR ? state : Labimotion::ConState::PROCESSED
|
|
168
|
+
Labimotion::Converter.handle_response(data[:a], response, current_user)
|
|
169
|
+
Labimotion::ConState::PROCESSED
|
|
208
170
|
else
|
|
209
171
|
Labimotion::Converter.logger.error ["Converter Response Error: id: [#{data[:a]&.id}], filename: [#{data[:a]&.filename}], response: #{response}"].join($INPUT_RECORD_SEPARATOR)
|
|
210
172
|
Labimotion::ConState::ERROR
|
|
@@ -285,22 +247,14 @@ module Labimotion
|
|
|
285
247
|
dsr.each do |ds|
|
|
286
248
|
layer = layers[ds[:layer]]
|
|
287
249
|
next if layer.blank? || layer[Labimotion::Prop::FIELDS].blank?
|
|
288
|
-
is_unit = ds[:field].start_with?(Prop::CONVERTER_FIELD_UINT_PREFIX)
|
|
289
|
-
next if is_unit && ds[:value].nil?
|
|
290
|
-
|
|
291
|
-
field_name = ds[:field].delete_prefix(Prop::CONVERTER_FIELD_UINT_PREFIX)
|
|
292
250
|
|
|
293
|
-
fields = layer[Labimotion::Prop::FIELDS].select{ |f| f['field'] ==
|
|
251
|
+
fields = layer[Labimotion::Prop::FIELDS].select{ |f| f['field'] == ds[:field] }
|
|
294
252
|
fi = fields&.first
|
|
295
253
|
next if fi.blank?
|
|
296
254
|
|
|
297
255
|
idx = layer[Labimotion::Prop::FIELDS].find_index(fi)
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
else
|
|
301
|
-
fi['value'] = ds[:value]
|
|
302
|
-
fi['device'] = ds[:device] || ds[:value]
|
|
303
|
-
end
|
|
256
|
+
fi['value'] = ds[:value]
|
|
257
|
+
fi['device'] = ds[:device] || ds[:value]
|
|
304
258
|
new_prop[Labimotion::Prop::LAYERS][ds[:layer]][Labimotion::Prop::FIELDS][idx] = fi
|
|
305
259
|
end
|
|
306
260
|
element = Container.find(dataset.element_id)&.root_element
|
|
@@ -366,37 +320,6 @@ module Labimotion
|
|
|
366
320
|
res
|
|
367
321
|
end
|
|
368
322
|
|
|
369
|
-
def self.test_conversions(tmpfile, format)
|
|
370
|
-
res = {}
|
|
371
|
-
File.open(tmpfile.path, 'r') do |file|
|
|
372
|
-
body = { file: file, format: format }
|
|
373
|
-
response = HTTParty.post(
|
|
374
|
-
uri('conversions'),
|
|
375
|
-
basic_auth: auth,
|
|
376
|
-
body: body,
|
|
377
|
-
timeout: timeout,
|
|
378
|
-
)
|
|
379
|
-
res = response.parsed_response
|
|
380
|
-
end
|
|
381
|
-
res
|
|
382
|
-
end
|
|
383
|
-
|
|
384
|
-
def self.restore(profile_id, version, hard)
|
|
385
|
-
body = { hard: hard }
|
|
386
|
-
response = HTTParty.post(
|
|
387
|
-
uri("profiles/restore/#{profile_id}/#{version}"),
|
|
388
|
-
headers: {
|
|
389
|
-
"Content-Type" => "application/json"
|
|
390
|
-
},
|
|
391
|
-
basic_auth: auth,
|
|
392
|
-
body: body.to_json,
|
|
393
|
-
timeout: timeout,
|
|
394
|
-
)
|
|
395
|
-
res = response.parsed_response
|
|
396
|
-
|
|
397
|
-
res
|
|
398
|
-
end
|
|
399
|
-
|
|
400
323
|
def self.metadata(id, current_user)
|
|
401
324
|
att = Attachment.find(id)
|
|
402
325
|
return if att.nil? || att.attachable_id.nil? || att.attachable_type != Labimotion::Prop::CONTAINER
|
|
@@ -411,4 +334,4 @@ end
|
|
|
411
334
|
# rubocop: enable Metrics/AbcSize
|
|
412
335
|
# rubocop: enable Metrics/MethodLength
|
|
413
336
|
# rubocop: enable Metrics/ClassLength
|
|
414
|
-
# rubocop: enable Metrics/CyclomaticComplexity
|
|
337
|
+
# rubocop: enable Metrics/CyclomaticComplexity
|