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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2682c1c72815cd6376ed94350afc4289a414a4f09c71b9788230ec4430b3f8f
4
- data.tar.gz: 603d30166812d812c7c03fdc3c985cc78b2f47965ec76abbfa4481f07d41b88e
3
+ metadata.gz: 20affa359afd5082ac4bc5e900b3e66c2c537a4b83fc9032883e33f25dc60362
4
+ data.tar.gz: e8bb4ae665e5de5166790ac27ecf42a95394057a2fd93d5deebc95dc858a5189
5
5
  SHA512:
6
- metadata.gz: b5dae89c2091aa2f7f4e6b59079d13f3c777c07ba0d0891ecc97daa51b8eb571e90b9517c332d035438aaa45d671184b8d0090db382d3b23fa6e96568cf0e940
7
- data.tar.gz: d66337f46db2b8d49e170bd7078cfae5b45d31fcd8deb92d0c625a09037d4858f21c919339d2547500f5bef0aec67728b7221adb73652961e8ae07c81336aee2
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
- record.layout = params[:layout] || {} if params.key?(:layout) && record.class.column_names.include?('layout')
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: klass_uuid, 'properties_release' => release,
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: klass_uuid, 'properties_release' => release,
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: klass_uuid, 'properties_release' => release,
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
@@ -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(ELN_TIMESTAMP_FORMAT) : nil
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'] = 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: klass_uuid,
82
+ klass_uuid: klass[:uuid],
94
83
  properties: properties,
95
- properties_release: 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, release = Labimotion::KlassVersion.for_update(
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
- ctx = { att: nil, ols: nil, dsr: [] }
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
- # Never close: closing can rewrite the stored file in place
43
- zip_file = Zip::File.new(att.attachment_attacher.file.url)
44
- res = Labimotion::Converter.collect_metadata(zip_file, current_user)
45
- Labimotion::Converter.collect_reaction_converter_metadata(att, zip_file)
46
- ctx[:ols] = res[:o] unless res&.dig(:o).nil?
47
- ctx[:dsr].push(res[:d]) unless res&.dig(:d).nil?
48
- ctx[:dsr].flatten!
49
- dataset = build_ds(att.attachable_id, ctx[:ols])
50
- update_ds(dataset, ctx[:dsr], current_user) if dataset.present?
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
- Labimotion::ConState::COMPLETED
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 = Labimotion::MapperUtils.read_entry(entry).force_encoding('UTF-8')
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
- state = Labimotion::Converter.handle_response(data[:a], response, current_user)
206
- # A failed dataset mapping also marks the source attachment as ERROR
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'] == field_name }
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
- if is_unit
299
- fi['value_system'] = ds[:value]
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