labimotion 2.3.1 → 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: 50d1572a0d16e5ac73fdf5aa6a22e5188c9fd2f008c671fc9705a310fbd090ac
4
- data.tar.gz: 85631425352102b56f1c0c9998710c0e9af6c4f416b272cee3e05cffd6c4209e
3
+ metadata.gz: 20affa359afd5082ac4bc5e900b3e66c2c537a4b83fc9032883e33f25dc60362
4
+ data.tar.gz: e8bb4ae665e5de5166790ac27ecf42a95394057a2fd93d5deebc95dc858a5189
5
5
  SHA512:
6
- metadata.gz: a8d4af5906da69a5ce49e100fb5ac4d63ea399bd4cc587af58dff73b2563eebe196d7658aa0257665b439be486b5f1d2bc851226bc265cd14b309e04120ae842
7
- data.tar.gz: 57e90440bc487d8fc950bb408e5c8794fa0566bad76d3a731f3cc92460623234a4893fdd1f6e61ed9f2730a1ec2b045b78ff940556fa49f6e92a6bc73c7475e6
6
+ metadata.gz: 7e6d407b44837ccb8758aa2a86b6c4b109350dfa50dde7ebb3affae93b070c1efba512b4db2b08f0608b165a25c1a53c4caaa3450f55c1a15e5c1401a1ca87de
7
+ data.tar.gz: 60c1e3df69128dda039990086d98ad816e7fcedbf74e1a90b896b9b94eb7446b3e5feb613e42770be1f8807d72e5ee3a7c07ec8eb8e158b776d5904335d07671
data/CHANGELOG.md CHANGED
@@ -1,17 +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
- * `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.
12
- * Dependencies:
13
- * Added `roo` (~> 2.10) for reading the uploaded workbook.
14
-
15
3
  ## [2.0.0]
16
4
  > 2025-03-25
17
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
@@ -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
 
@@ -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
@@ -29,10 +29,7 @@ module Labimotion
29
29
  dsr = []
30
30
  ols = nil
31
31
  Zip::File.open(att.attachment_attacher.file.url) do |zip_file|
32
- if att.filename.split('.')&.last == 'zip'
33
- res = Labimotion::Converter.collect_metadata(zip_file, current_user)
34
- Labimotion::Converter.collect_reaction_converter_metadata(att, zip_file)
35
- end
32
+ res = Labimotion::Converter.collect_metadata(zip_file, current_user) if att.filename.split('.')&.last == 'zip'
36
33
  ols = res[:o] unless res&.dig(:o).nil?
37
34
  dsr.push(res[:d]) unless res&.dig(:d).nil?
38
35
  end
@@ -96,33 +93,6 @@ module Labimotion
96
93
  { a: oa, f: folder }
97
94
  end
98
95
 
99
- def self.collect_reaction_converter_metadata(oat, zip_file)
100
- zip_file.each do |entry|
101
- next unless entry.name == 'metadata/reaction_variation.json'
102
-
103
- # Create temp file
104
- tmp_file = Tempfile.new(['reaction_variation', '.json'])
105
- tmp_file.binmode
106
-
107
- # Write ZIP entry content into temp file
108
- entry.extract(tmp_file.path) { true }
109
-
110
- # Create attachment
111
- att = Attachment.create!(
112
- filename: 'reaction_variation.json',
113
- file_path: tmp_file.path,
114
- content_type: 'application/json',
115
- attachable_id: oat.attachable_id,
116
- attachable_type: 'Container',
117
- con_state: Labimotion::ConState::CONVERTED,
118
- created_by: oat.created_by,
119
- created_for: oat.created_for
120
- )
121
-
122
- att.save! if att.valid?
123
- end
124
- end
125
-
126
96
  def self.collect_metadata(zip_file, current_user = {}) # rubocop: disable Metrics/PerceivedComplexity
127
97
  dsr = []
128
98
  ols = nil
@@ -155,6 +125,7 @@ module Labimotion
155
125
  tmp_file.rewind
156
126
 
157
127
  filename = oat.filename
128
+ # name = "#{File.basename(filename, '.*')}.zip"
158
129
  name = "#{File.basename(filename, '.*')}#{File.extname(filename) == '.zip' ? '.bagit.zip' : '.zip'}"
159
130
  att = Attachment.new(
160
131
  filename: name,
@@ -276,22 +247,14 @@ module Labimotion
276
247
  dsr.each do |ds|
277
248
  layer = layers[ds[:layer]]
278
249
  next if layer.blank? || layer[Labimotion::Prop::FIELDS].blank?
279
- is_unit = ds[:field].start_with?(Prop::CONVERTER_FIELD_UINT_PREFIX)
280
- next if is_unit && ds[:value].nil?
281
250
 
282
- field_name = ds[:field].delete_prefix(Prop::CONVERTER_FIELD_UINT_PREFIX)
283
-
284
- fields = layer[Labimotion::Prop::FIELDS].select{ |f| f['field'] == field_name }
251
+ fields = layer[Labimotion::Prop::FIELDS].select{ |f| f['field'] == ds[:field] }
285
252
  fi = fields&.first
286
253
  next if fi.blank?
287
254
 
288
255
  idx = layer[Labimotion::Prop::FIELDS].find_index(fi)
289
- if is_unit
290
- fi['value_system'] = ds[:value]
291
- else
292
- fi['value'] = ds[:value]
293
- fi['device'] = ds[:device] || ds[:value]
294
- end
256
+ fi['value'] = ds[:value]
257
+ fi['device'] = ds[:device] || ds[:value]
295
258
  new_prop[Labimotion::Prop::LAYERS][ds[:layer]][Labimotion::Prop::FIELDS][idx] = fi
296
259
  end
297
260
  element = Container.find(dataset.element_id)&.root_element
@@ -357,37 +320,6 @@ module Labimotion
357
320
  res
358
321
  end
359
322
 
360
- def self.test_conversions(tmpfile, format)
361
- res = {}
362
- File.open(tmpfile.path, 'r') do |file|
363
- body = { file: file, format: format }
364
- response = HTTParty.post(
365
- uri('conversions'),
366
- basic_auth: auth,
367
- body: body,
368
- timeout: timeout,
369
- )
370
- res = response.parsed_response
371
- end
372
- res
373
- end
374
-
375
- def self.restore(profile_id, version, hard)
376
- body = { hard: hard }
377
- response = HTTParty.post(
378
- uri("profiles/restore/#{profile_id}/#{version}"),
379
- headers: {
380
- "Content-Type" => "application/json"
381
- },
382
- basic_auth: auth,
383
- body: body.to_json,
384
- timeout: timeout,
385
- )
386
- res = response.parsed_response
387
-
388
- res
389
- end
390
-
391
323
  def self.metadata(id, current_user)
392
324
  att = Attachment.find(id)
393
325
  return if att.nil? || att.attachable_id.nil? || att.attachable_type != Labimotion::Prop::CONTAINER
@@ -402,4 +334,4 @@ end
402
334
  # rubocop: enable Metrics/AbcSize
403
335
  # rubocop: enable Metrics/MethodLength
404
336
  # rubocop: enable Metrics/ClassLength
405
- # rubocop: enable Metrics/CyclomaticComplexity
337
+ # rubocop: enable Metrics/CyclomaticComplexity
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ # Resolves the "inline attributes" of an element linked from a generic
5
+ # drag_element field.
6
+ #
7
+ # A drag_element link target is identified by the stored value's +el_klass+:
8
+ # * a generic element klass name -> the target is a Labimotion::Element and
9
+ # its selectable attributes are its own property fields (layer + field);
10
+ # * a permit-target key (reaction, sample, molecule, wellplate, screen,
11
+ # research_plan, device_description) -> the target is a standard ELN model
12
+ # and its selectable attributes are a curated set of database columns.
13
+ #
14
+ # Used both to list the attributes a user can pick (#available) and to refresh
15
+ # the current values of a previously stored selection (#resolve).
16
+ class LinkedElement
17
+ SOURCE_PROPERTY = 'property'
18
+ SOURCE_COLUMN = 'column'
19
+
20
+ # el_klass (permit-target key) => model class name
21
+ MODELS = {
22
+ 'reaction' => '::Reaction',
23
+ 'sample' => '::Sample',
24
+ 'molecule' => '::Molecule',
25
+ 'wellplate' => '::Wellplate',
26
+ 'screen' => '::Screen',
27
+ 'research_plan' => '::ResearchPlan',
28
+ 'device_description' => '::DeviceDescription'
29
+ }.freeze
30
+
31
+ # Curated, scalar columns exposed per type: [column/method, label, unit_column?]
32
+ COLUMNS = {
33
+ 'reaction' => [
34
+ %w[name Name], %w[short_label], ['status', 'Status'], ['role', 'Role'],
35
+ ['rxno', 'Reaction Type'], ['conditions', 'Conditions'],
36
+ ['duration', 'Duration'], %w[solvent Solvent]
37
+ ],
38
+ 'sample' => [
39
+ %w[name Name], %w[short_label], ['external_label', 'External Label'],
40
+ ['sum_formula', 'Sum Formula'], ['molecular_mass', 'Molecular Mass'],
41
+ %w[purity Purity], %w[density Density],
42
+ ['real_amount_value', 'Real Amount', 'real_amount_unit'],
43
+ ['target_amount_value', 'Target Amount', 'target_amount_unit'],
44
+ %w[location Location]
45
+ ],
46
+ 'molecule' => [
47
+ ['iupac_name', 'IUPAC Name'], ['sum_formular', 'Sum Formula'],
48
+ ['molecular_weight', 'Molecular Weight'],
49
+ ['exact_molecular_weight', 'Exact Molecular Weight'],
50
+ ['cano_smiles', 'Canonical SMILES'], ['melting_point', 'Melting Point'],
51
+ ['boiling_point', 'Boiling Point'], %w[density Density]
52
+ ],
53
+ 'wellplate' => [
54
+ %w[name Name], %w[short_label], ['description', 'Description'],
55
+ %w[width Width], %w[height Height]
56
+ ],
57
+ 'screen' => [
58
+ %w[name Name], ['description', 'Description'], %w[result Result],
59
+ %w[collaborator Collaborator], ['conditions', 'Conditions'],
60
+ %w[requirements Requirements]
61
+ ],
62
+ 'research_plan' => [
63
+ %w[name Name], %w[short_label]
64
+ ],
65
+ 'device_description' => [
66
+ %w[name Name], %w[short_label], ['serial_number', 'Serial Number'],
67
+ ['device_class', 'Device Class'], ['operation_mode', 'Operation Mode'],
68
+ ['application_name', 'Application Name'], ['vendor_url', 'Vendor URL'],
69
+ %w[institute Institute], %w[building Building], %w[room Room]
70
+ ]
71
+ }.freeze
72
+
73
+ # Property field types whose value is a scalar worth surfacing inline.
74
+ PROPERTY_TYPES = %w[
75
+ text textarea number integer select select-multi checkbox
76
+ datetime system-defined formula-field text-formula ontology-select
77
+ ].freeze
78
+
79
+ def initialize(el_klass, el_id, el_type = nil)
80
+ @el_klass = el_klass.to_s
81
+ @el_type = el_type.to_s
82
+ @el_id = el_id
83
+ end
84
+
85
+ # drag_element keeps the linked class in +el_klass+; drag_sample / drag_molecule
86
+ # keep it in +el_type+ (and may omit el_klass). Resolve one effective dispatch key.
87
+ def target_key
88
+ return @target_key if defined?(@target_key)
89
+
90
+ @target_key =
91
+ if MODELS.key?(@el_klass) || generic_klass?(@el_klass)
92
+ @el_klass
93
+ elsif MODELS.key?(@el_type)
94
+ @el_type
95
+ else
96
+ ''
97
+ end
98
+ end
99
+
100
+ def column_based?
101
+ MODELS.key?(target_key)
102
+ end
103
+
104
+ def generic?
105
+ generic_klass?(target_key)
106
+ end
107
+
108
+ def record
109
+ return @record if defined?(@record)
110
+
111
+ @record =
112
+ if column_based?
113
+ MODELS[target_key].safe_constantize&.find_by(id: @el_id)
114
+ elsif generic?
115
+ Labimotion::Element.find_by(id: @el_id)
116
+ end
117
+ end
118
+
119
+ # All attributes the user can pick, each with its current value.
120
+ def available
121
+ return generic_attributes if generic?
122
+ return column_attributes if column_based?
123
+
124
+ []
125
+ end
126
+
127
+ # Refresh the values/labels of a previously stored selection. Selections whose
128
+ # source attribute no longer exists are kept but flagged as missing; for an
129
+ # unknown/unsupported el_klass the selection is returned untouched.
130
+ def resolve(selected)
131
+ return selected unless selected.is_a?(Array)
132
+ return selected unless generic? || column_based?
133
+
134
+ index = available.index_by { |a| [a['source'], a['key']] }
135
+ selected.map do |attr|
136
+ next attr unless attr.is_a?(Hash)
137
+
138
+ index[[attr['source'], attr['key']]] || attr.merge('value' => nil, 'missing' => true)
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def generic_klass?(name)
145
+ name.present? && !MODELS.key?(name) && Labimotion::ElementKlass.exists?(name: name)
146
+ end
147
+
148
+ def generic_attributes
149
+ el = record
150
+ return [] unless el&.properties.is_a?(Hash)
151
+
152
+ attrs = []
153
+ layers = el.properties[Labimotion::Prop::LAYERS] || {}
154
+ layers.values.sort_by { |layer| layer['position'] || 0 }.each do |layer|
155
+ (layer[Labimotion::Prop::FIELDS] || []).each do |field|
156
+ next unless PROPERTY_TYPES.include?(field['type'])
157
+
158
+ attr = {
159
+ 'source' => SOURCE_PROPERTY,
160
+ 'key' => "#{layer['key']}::#{field['field']}",
161
+ 'label' => field['label'].presence || field['field'],
162
+ 'type' => field['type'],
163
+ 'value' => field['value'],
164
+ 'group' => layer['label'].presence || layer['key']
165
+ }
166
+ attr['value_system'] = field['value_system'] if field['value_system'].present?
167
+ attrs << attr
168
+ end
169
+ end
170
+ attrs
171
+ end
172
+
173
+ def column_attributes
174
+ rec = record
175
+ return [] if rec.blank?
176
+
177
+ (COLUMNS[target_key] || []).filter_map do |col, label, unit_col|
178
+ next unless rec.respond_to?(col)
179
+
180
+ attr = {
181
+ 'source' => SOURCE_COLUMN,
182
+ 'key' => col,
183
+ 'label' => label.presence || col.to_s.humanize,
184
+ 'type' => SOURCE_COLUMN,
185
+ 'value' => rec.public_send(col),
186
+ 'group' => target_key
187
+ }
188
+ attr['value_system'] = rec.public_send(unit_col) if unit_col && rec.respond_to?(unit_col) && rec.public_send(unit_col).present?
189
+ attr
190
+ end
191
+ end
192
+ end
193
+ end
@@ -41,10 +41,9 @@ module Labimotion
41
41
 
42
42
  def process(att)
43
43
  config = Labimotion::MapperUtils.load_brucker_config
44
- attacher = att&.attachment_attacher
45
- return Labimotion::MapperUtils.check_if_bagit(attacher&.file&.url) if config.nil?
46
-
44
+ return if config.nil?
47
45
 
46
+ attacher = att&.attachment_attacher
48
47
  extracted_data = Labimotion::MapperUtils.extract_data_from_zip(attacher&.file&.url, config['sourceMap'])
49
48
  return if extracted_data.nil?
50
49
 
@@ -26,10 +26,9 @@ module Labimotion
26
26
  joins(collections: :user).where(collections: { user_id: user_id })
27
27
  )
28
28
 
29
- # Shared records — collection_shares hangs off Collection, not off the
30
- # element models themselves, so join through the collections association.
29
+ # Shared records
31
30
  shared = apply_filters.call(
32
- left_joins(collections: :collection_shares).where(collection_shares: { shared_with_id: user_id })
31
+ left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
33
32
  )
34
33
 
35
34
  # Combine (remove duplicates), order, and limit
@@ -141,10 +141,9 @@ module Labimotion
141
141
  joins(collections: :user).where(collections: { user_id: user_id })
142
142
  )
143
143
 
144
- # Shared elements — collection_shares hangs off Collection, not off
145
- # Element itself, so join through the collections association.
144
+ # Shared elements
146
145
  shared = apply_filters.call(
147
- left_joins(collections: :collection_shares).where(collection_shares: { shared_with_id: user_id })
146
+ left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
148
147
  )
149
148
 
150
149
  # Combine (remove duplicates), order, and limit
@@ -46,22 +46,6 @@ module Labimotion
46
46
  nil
47
47
  end
48
48
 
49
- def check_if_bagit(zip_file_url)
50
- return nil if zip_file_url.nil?
51
- Zip::File.open(zip_file_url) do |zip_file|
52
- zip_file.each do |entry|
53
- return { is_bagit: true, metadata: nil } if bagit_metadata_file?(entry)
54
- end
55
- end
56
-
57
- rescue Zip::Error => e
58
- Rails.logger.error "Zip file error: #{e.message}"
59
- nil
60
- rescue StandardError => e
61
- Rails.logger.error "Unexpected error extracting metadata: #{e.message}"
62
- nil
63
- end
64
-
65
49
  def extract_parameters(file_content, parameter_names)
66
50
  return nil if file_content.blank? || parameter_names.blank?
67
51
 
@@ -1,7 +1,6 @@
1
1
  module Labimotion
2
2
  ## Converter State
3
3
  class Prop
4
- CONVERTER_FIELD_UINT_PREFIX = '___unit___'.freeze
5
4
  LAYERS = 'layers'.freeze
6
5
  FIELDS = 'fields'.freeze
7
6
  SUBFIELDS = 'sub_fields'.freeze