labimotion 2.3.0.rc5 → 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: 57f6db758edf09de9105830d7a9f9cfda1270ac36bb6ee4351da8ab65b2ea2dc
4
- data.tar.gz: 0e220ffc2a76645f8e82536cd592d7ee9eecd28e54073b408a602da019839e9f
3
+ metadata.gz: 20affa359afd5082ac4bc5e900b3e66c2c537a4b83fc9032883e33f25dc60362
4
+ data.tar.gz: e8bb4ae665e5de5166790ac27ecf42a95394057a2fd93d5deebc95dc858a5189
5
5
  SHA512:
6
- metadata.gz: f3f5ca1afdb69edbbf36f09834a04843d72d9b964db836719991a5d8b1acb10036cf4470f4ca1810bac6b629bc6908dc2ed5054127216ed9d6109b01d205ab07
7
- data.tar.gz: cf72bfa7a774ef98e1474d5a4e3914304f940105137f18f05adb4b5675afdeab668e305386bda9ce1b842b2083005fc622c210d27c60b1005cfcba90c1eb2617
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
@@ -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
@@ -125,6 +125,7 @@ module Labimotion
125
125
  tmp_file.rewind
126
126
 
127
127
  filename = oat.filename
128
+ # name = "#{File.basename(filename, '.*')}.zip"
128
129
  name = "#{File.basename(filename, '.*')}#{File.extname(filename) == '.zip' ? '.bagit.zip' : '.zip'}"
129
130
  att = Attachment.new(
130
131
  filename: name,
@@ -246,22 +247,14 @@ module Labimotion
246
247
  dsr.each do |ds|
247
248
  layer = layers[ds[:layer]]
248
249
  next if layer.blank? || layer[Labimotion::Prop::FIELDS].blank?
249
- is_unit = ds[:field].start_with?(Prop::CONVERTER_FIELD_UINT_PREFIX)
250
- next if is_unit && ds[:value].nil?
251
250
 
252
- field_name = ds[:field].delete_prefix(Prop::CONVERTER_FIELD_UINT_PREFIX)
253
-
254
- fields = layer[Labimotion::Prop::FIELDS].select{ |f| f['field'] == field_name }
251
+ fields = layer[Labimotion::Prop::FIELDS].select{ |f| f['field'] == ds[:field] }
255
252
  fi = fields&.first
256
253
  next if fi.blank?
257
254
 
258
255
  idx = layer[Labimotion::Prop::FIELDS].find_index(fi)
259
- if is_unit
260
- fi['value_system'] = ds[:value]
261
- else
262
- fi['value'] = ds[:value]
263
- fi['device'] = ds[:device] || ds[:value]
264
- end
256
+ fi['value'] = ds[:value]
257
+ fi['device'] = ds[:device] || ds[:value]
265
258
  new_prop[Labimotion::Prop::LAYERS][ds[:layer]][Labimotion::Prop::FIELDS][idx] = fi
266
259
  end
267
260
  element = Container.find(dataset.element_id)&.root_element
@@ -327,37 +320,6 @@ module Labimotion
327
320
  res
328
321
  end
329
322
 
330
- def self.test_conversions(tmpfile, format)
331
- res = {}
332
- File.open(tmpfile.path, 'r') do |file|
333
- body = { file: file, format: format }
334
- response = HTTParty.post(
335
- uri('conversions'),
336
- basic_auth: auth,
337
- body: body,
338
- timeout: timeout,
339
- )
340
- res = response.parsed_response
341
- end
342
- res
343
- end
344
-
345
- def self.restore(profile_id, version, hard)
346
- body = { hard: hard }
347
- response = HTTParty.post(
348
- uri("profiles/restore/#{profile_id}/#{version}"),
349
- headers: {
350
- "Content-Type" => "application/json"
351
- },
352
- basic_auth: auth,
353
- body: body.to_json,
354
- timeout: timeout,
355
- )
356
- res = response.parsed_response
357
-
358
- res
359
- end
360
-
361
323
  def self.metadata(id, current_user)
362
324
  att = Attachment.find(id)
363
325
  return if att.nil? || att.attachable_id.nil? || att.attachable_type != Labimotion::Prop::CONTAINER
@@ -372,4 +334,4 @@ end
372
334
  # rubocop: enable Metrics/AbcSize
373
335
  # rubocop: enable Metrics/MethodLength
374
336
  # rubocop: enable Metrics/ClassLength
375
- # 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
@@ -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
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.3.0.rc5'
5
+ VERSION = '2.4.0.rc1'
6
6
  end
data/lib/labimotion.rb CHANGED
@@ -89,13 +89,10 @@ module Labimotion
89
89
  autoload :ExportDataset, 'labimotion/libs/export_dataset'
90
90
  autoload :SampleAssociation, 'labimotion/libs/sample_association'
91
91
  autoload :PropertiesHandler, 'labimotion/libs/properties_handler'
92
+ autoload :LinkedElement, 'labimotion/libs/linked_element'
92
93
  autoload :AttachmentHandler, 'labimotion/libs/attachment_handler'
93
94
  autoload :VocabularyHandler, 'labimotion/libs/vocabulary_handler'
94
95
  autoload :XlsxExporter, 'labimotion/libs/xlsx_exporter'
95
- autoload :ElementVariationColumnSet, 'labimotion/libs/element_variation_column_set'
96
- autoload :ElementVariationHeader, 'labimotion/libs/element_variation_header'
97
- autoload :ExportElementVariations, 'labimotion/libs/export_element_variations'
98
- autoload :ImportElementVariations, 'labimotion/libs/import_element_variations'
99
96
 
100
97
  ######## Utils
101
98
  autoload :Prop, 'labimotion/utils/prop'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: labimotion
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.3.0.rc5
4
+ version: 2.4.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chia-Lin Lin
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2026-07-10 00:00:00.000000000 Z
12
+ date: 2026-07-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx
@@ -45,20 +45,6 @@ dependencies:
45
45
  - - "<"
46
46
  - !ruby/object:Gem::Version
47
47
  version: '8.0'
48
- - !ruby/object:Gem::Dependency
49
- name: roo
50
- requirement: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: '2.10'
55
- type: :runtime
56
- prerelease: false
57
- version_requirements: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '2.10'
62
48
  description:
63
49
  email:
64
50
  - chia-lin.lin@kit.edu
@@ -130,12 +116,9 @@ files:
130
116
  - lib/labimotion/libs/data/vocab/Standard.json
131
117
  - lib/labimotion/libs/data/vocab/System.json
132
118
  - lib/labimotion/libs/dataset_builder.rb
133
- - lib/labimotion/libs/element_variation_column_set.rb
134
- - lib/labimotion/libs/element_variation_header.rb
135
119
  - lib/labimotion/libs/export_dataset.rb
136
120
  - lib/labimotion/libs/export_element.rb
137
- - lib/labimotion/libs/export_element_variations.rb
138
- - lib/labimotion/libs/import_element_variations.rb
121
+ - lib/labimotion/libs/linked_element.rb
139
122
  - lib/labimotion/libs/nmr_mapper.rb
140
123
  - lib/labimotion/libs/properties_handler.rb
141
124
  - lib/labimotion/libs/sample_association.rb