labimotion 2.2.0.rc8 → 2.2.0.rc9

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.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'date'
2
3
  require 'ostruct'
3
4
  require 'export_table'
4
5
  require 'labimotion/version'
@@ -125,25 +126,13 @@ module Labimotion
125
126
  val = files&.map { |file| "#{file['filename']} #{file['label']}" }&.join('\n')
126
127
  field_obj[:value] = val
127
128
  when Labimotion::FieldType::TABLE
128
- field_obj[:is_table] = true
129
- field_obj[:not_table] = false
130
- tbl = []
131
- ## tbl_idx = []
132
- header = {}
133
- sub_fields = field.fetch('sub_fields', [])
134
- sub_fields.each_with_index do |sub_field, idx|
135
- header["col#{idx}"] = sub_field['col_name']
136
- end
137
- tbl.push(header)
138
- field.fetch('sub_values', []).each do |sub_val|
139
- data = {}
140
- sub_fields.each_with_index do |sub_field, idx|
141
- data["col#{idx}"] = build_table_field(sub_val, sub_field)
142
- end
143
- tbl.push(data)
144
- end
145
- field_obj[:data] = tbl
146
- # field_obj[:value] = 'this is a table'
129
+ field_obj[:is_table] = false
130
+ field_obj[:not_table] = true
131
+ field_obj[:value] = build_table_wordml(field)
132
+ when Labimotion::FieldType::DATETIME_RANGE
133
+ field_obj[:is_table] = false
134
+ field_obj[:not_table] = true
135
+ field_obj[:value] = build_datetime_range_wordml(field)
147
136
  when Labimotion::FieldType::INPUT_GROUP
148
137
  val = []
149
138
  field.fetch('sub_fields', [])&.each do |sub_field|
@@ -210,6 +199,243 @@ module Labimotion
210
199
  end
211
200
  end
212
201
 
202
+ TABLE_BLANK_WORDML = '<w:p/>'
203
+ TABLE_BORDER = '<w:tblBorders>' \
204
+ '<w:top w:val="single" w:sz="4" w:color="auto"/>' \
205
+ '<w:left w:val="single" w:sz="4" w:color="auto"/>' \
206
+ '<w:bottom w:val="single" w:sz="4" w:color="auto"/>' \
207
+ '<w:right w:val="single" w:sz="4" w:color="auto"/>' \
208
+ '<w:insideH w:val="single" w:sz="4" w:color="auto"/>' \
209
+ '<w:insideV w:val="single" w:sz="4" w:color="auto"/>' \
210
+ '</w:tblBorders>'
211
+
212
+ def build_table_wordml(field)
213
+ sub_fields = field.fetch('sub_fields', [])
214
+ return Sablon.content(:word_ml, TABLE_BLANK_WORDML) if sub_fields.empty?
215
+
216
+ width = (9000.0 / sub_fields.length).round
217
+ font_size = sub_fields.length > 6 ? 16 : 20
218
+ grid = sub_fields.map { %(<w:gridCol w:w="#{width}"/>) }.join
219
+ header = build_table_header_row(sub_fields, width, font_size)
220
+ rows = build_table_body_rows(field.fetch('sub_values', []), sub_fields, width, font_size)
221
+ tbl = '<w:tbl><w:tblPr><w:tblW w:w="5000" w:type="pct"/>' \
222
+ "#{TABLE_BORDER}</w:tblPr><w:tblGrid>#{grid}</w:tblGrid>" \
223
+ "#{header}#{rows}</w:tbl>"
224
+ Sablon.content(:word_ml, tbl)
225
+ rescue StandardError => e
226
+ Labimotion.log_exception(e)
227
+ Sablon.content(:word_ml, TABLE_BLANK_WORDML)
228
+ end
229
+
230
+ DATETIME_RANGE_HEADERS = ['Start', 'Stop', 'Duration (calc)', 'Duration'].freeze
231
+ DATETIME_RANGE_PRECISE_LABELS = %w[year month day hour minute second].freeze
232
+ DURATION_UNIT_LABELS = {
233
+ 'd' => 'day',
234
+ 'h' => 'hour',
235
+ 'min' => 'minute',
236
+ 's' => 'second'
237
+ }.freeze
238
+
239
+ def build_datetime_range_wordml(field)
240
+ sub_fields = field.fetch('sub_fields', []) || []
241
+ by_col = sub_fields.each_with_object({}) { |sf, h| h[sf['col_name']] = sf if sf.is_a?(Hash) }
242
+ values = datetime_range_values(by_col)
243
+ datetime_range_wordml_table(values)
244
+ rescue StandardError => e
245
+ Labimotion.log_exception(e)
246
+ Sablon.content(:word_ml, TABLE_BLANK_WORDML)
247
+ end
248
+
249
+ def datetime_range_values(by_col)
250
+ time_start = by_col['timeStart']&.dig('value').to_s
251
+ time_stop = by_col['timeStop']&.dig('value').to_s
252
+ [
253
+ time_start,
254
+ time_stop,
255
+ format_duration_calc(by_col['durationCalc'], time_start, time_stop),
256
+ format_duration_value(by_col['duration'])
257
+ ]
258
+ end
259
+
260
+ def datetime_range_wordml_table(values)
261
+ width = (9000.0 / DATETIME_RANGE_HEADERS.length).round
262
+ font_size = 20
263
+ grid = DATETIME_RANGE_HEADERS.map { %(<w:gridCol w:w="#{width}"/>) }.join
264
+ header_cells = DATETIME_RANGE_HEADERS.map { |h| build_wordml_cell(h, width, font_size, true) }.join
265
+ body_cells = values.map { |v| build_wordml_cell(v, width, font_size, false) }.join
266
+ tbl = '<w:tbl><w:tblPr><w:tblW w:w="5000" w:type="pct"/>' \
267
+ "#{TABLE_BORDER}</w:tblPr><w:tblGrid>#{grid}</w:tblGrid>" \
268
+ "<w:tr>#{header_cells}</w:tr><w:tr>#{body_cells}</w:tr></w:tbl>"
269
+ Sablon.content(:word_ml, tbl)
270
+ end
271
+
272
+ def format_duration_value(duration_sf)
273
+ val = duration_sf&.dig('value')
274
+ return '' if val.nil? || val.to_s.empty?
275
+
276
+ "#{val} #{duration_unit_label(duration_sf['value_system'], val)}".strip
277
+ end
278
+
279
+ def duration_unit_label(value_system, value)
280
+ base = DURATION_UNIT_LABELS[value_system.to_s]
281
+ return value_system.to_s if base.nil?
282
+
283
+ pluralize_unit?(value) ? "#{base}s" : base
284
+ end
285
+
286
+ def pluralize_unit?(value)
287
+ numeric = Float(value.to_s)
288
+ (numeric - 1.0).abs > Float::EPSILON
289
+ rescue ArgumentError, TypeError
290
+ true
291
+ end
292
+
293
+ def format_duration_calc(_duration_calc_sf, time_start, time_stop)
294
+ compute_duration_calc(time_start, time_stop)
295
+ end
296
+
297
+ def compute_duration_calc(time_start, time_stop)
298
+ start_t = parse_datetime_range_value(time_start)
299
+ stop_t = parse_datetime_range_value(time_stop)
300
+ return '' unless start_t && stop_t && stop_t > start_t
301
+
302
+ precise_diff_humanize(start_t, stop_t)
303
+ end
304
+
305
+ def parse_datetime_range_value(str)
306
+ cleaned = str.to_s.strip
307
+ return nil if cleaned.empty?
308
+
309
+ parts = Date._parse(cleaned)
310
+ return nil unless datetime_range_parts_complete?(parts)
311
+
312
+ build_utc_from_parts(parts)
313
+ rescue ArgumentError
314
+ nil
315
+ end
316
+
317
+ def datetime_range_parts_complete?(parts)
318
+ %i[year mon mday].all? { |k| parts[k] }
319
+ end
320
+
321
+ def build_utc_from_parts(parts)
322
+ Time.utc(parts[:year], parts[:mon], parts[:mday],
323
+ parts[:hour] || 0, parts[:min] || 0, parts[:sec] || 0)
324
+ end
325
+
326
+ def precise_diff_humanize(start_t, stop_t)
327
+ components = precise_diff_components(start_t, stop_t)
328
+ parts = components.zip(DATETIME_RANGE_PRECISE_LABELS).reject { |n, _| n <= 0 }
329
+ return '0 seconds' if parts.empty?
330
+
331
+ parts.map { |n, label| format_precise_part(n, label) }.join(' ')
332
+ end
333
+
334
+ def format_precise_part(count, label)
335
+ suffix = count == 1 ? '' : 's'
336
+ "#{count} #{label}#{suffix}"
337
+ end
338
+
339
+ def precise_diff_components(start_t, stop_t)
340
+ y, m, d, h, mi, s = precise_diff_raw(start_t, stop_t)
341
+ mi, s = borrow_unit(mi, s, 60)
342
+ h, mi = borrow_unit(h, mi, 60)
343
+ d, h = borrow_unit(d, h, 24)
344
+ if d.negative?
345
+ m -= 1
346
+ d += (Date.new(stop_t.year, stop_t.month, 1) - 1).day
347
+ end
348
+ y, m = borrow_unit(y, m, 12)
349
+ [y, m, d, h, mi, s]
350
+ end
351
+
352
+ def borrow_unit(higher, lower, base)
353
+ return [higher, lower] unless lower.negative?
354
+
355
+ [higher - 1, lower + base]
356
+ end
357
+
358
+ def precise_diff_raw(start_t, stop_t)
359
+ [
360
+ stop_t.year - start_t.year,
361
+ stop_t.month - start_t.month,
362
+ stop_t.day - start_t.day,
363
+ stop_t.hour - start_t.hour,
364
+ stop_t.min - start_t.min,
365
+ stop_t.sec - start_t.sec
366
+ ]
367
+ end
368
+
369
+ def build_table_header_row(sub_fields, width, font_size)
370
+ cells = sub_fields.map { |sf| build_wordml_cell(sf['col_name'].to_s, width, font_size, true) }.join
371
+ "<w:tr>#{cells}</w:tr>"
372
+ end
373
+
374
+ def build_table_body_rows(sub_values, sub_fields, width, font_size)
375
+ sub_values.map do |sv|
376
+ cells = sub_fields.map { |sf| build_wordml_cell(table_cell_text(sv, sf), width, font_size, false) }.join
377
+ "<w:tr>#{cells}</w:tr>"
378
+ end.join
379
+ end
380
+
381
+ def build_wordml_cell(text, width, font_size, bold)
382
+ bold_xml = bold ? '<w:b/>' : ''
383
+ rpr = "<w:rPr>#{bold_xml}<w:sz w:val=\"#{font_size}\"/></w:rPr>"
384
+ lines = text.to_s.split(/\r?\n/)
385
+ lines = [''] if lines.empty?
386
+ paragraphs = lines.map do |line|
387
+ "<w:p><w:r>#{rpr}<w:t xml:space=\"preserve\">#{xml_escape(line)}</w:t></w:r></w:p>"
388
+ end.join
389
+ "<w:tc><w:tcPr><w:tcW w:w=\"#{width}\" w:type=\"dxa\"/></w:tcPr>#{paragraphs}</w:tc>"
390
+ end
391
+
392
+ def xml_escape(str)
393
+ str.to_s.gsub('&', '&amp;').gsub('<', '&lt;').gsub('>', '&gt;')
394
+ end
395
+
396
+ TABLE_CELL_TEXT_RENDERERS = {
397
+ Labimotion::FieldType::DRAG_SAMPLE => ->(_sf, c, ctx) { ctx.table_cell_sample_text(c['value'] || {}) },
398
+ Labimotion::FieldType::DRAG_MOLECULE => ->(_sf, c, ctx) { ctx.table_cell_molecule_text(c['value'] || {}) },
399
+ Labimotion::FieldType::SELECT => ->(_sf, c, _ctx) { c['value'].to_s },
400
+ Labimotion::FieldType::SYSTEM_DEFINED => ->(sf, c, ctx) { ctx.table_cell_sysdef_text(sf, c) }
401
+ }.freeze
402
+
403
+ def table_cell_text(sub_val, sub_field)
404
+ return '' if sub_field.fetch('id', nil).nil? || sub_val[sub_field['id']].nil?
405
+
406
+ cell = sub_val[sub_field['id']]
407
+ renderer = TABLE_CELL_TEXT_RENDERERS[sub_field['type']]
408
+ return renderer.call(sub_field, cell, self) if renderer
409
+
410
+ raw = cell.is_a?(Hash) ? cell['value'] : cell
411
+ raw.to_s
412
+ end
413
+
414
+ def table_cell_sample_text(val)
415
+ parts = []
416
+ parts << "Short Label: [#{val['el_label']}]" if val['el_label'].present?
417
+ parts << "Name: [#{val['el_name']}]" if val['el_name'].present?
418
+ parts << "Ext. Label: [#{val['el_external_label']}]" if val['el_external_label'].present?
419
+ parts << "Mass: [#{val['el_molecular_weight']}]" if val['el_molecular_weight'].present?
420
+ parts << "#{sample_url}/#{val['el_id']}" if val['el_id'].present?
421
+ parts.join("\n")
422
+ end
423
+
424
+ def table_cell_molecule_text(val)
425
+ parts = []
426
+ parts << "SMILES: [#{val['el_smiles']}]" if val['el_smiles'].present?
427
+ parts << "InChiKey: [#{val['el_inchikey']}]" if val['el_inchikey'].present?
428
+ parts << "IUPAC: [#{val['el_iupac']}]" if val['el_iupac'].present?
429
+ parts << "MASS: [#{val['el_molecular_weight']}]" if val['el_molecular_weight'].present?
430
+ parts.join("\n")
431
+ end
432
+
433
+ def table_cell_sysdef_text(sub_field, cell)
434
+ fdef = Labimotion::Units::FIELDS.find { |o| o[:field] == sub_field['option_layers'] }
435
+ unit = fdef&.fetch(:units, [])&.find { |u| u[:key] == cell['value_system'] }&.fetch(:label, '')
436
+ "#{cell['value']} #{unit}".strip
437
+ end
438
+
213
439
  def build_fields(layer)
214
440
  fields = layer[Labimotion::Prop::FIELDS] || []
215
441
  field_objs = []
@@ -34,7 +34,7 @@ module Labimotion
34
34
  return Labimotion::ConState::NONE if att.nil?
35
35
 
36
36
  result = process(att)
37
- return Labimotion::ConState::WAIT if result.nil?
37
+ return Labimotion::ConState::NONE if result.nil?
38
38
 
39
39
  handle_process_result(result, att, id, current_user)
40
40
  end
@@ -136,7 +136,7 @@ module Labimotion
136
136
  if result[:is_bagit]
137
137
  handle_bagit_result(att, id, current_user)
138
138
  elsif invalid_metadata?(result)
139
- Labimotion::ConState::WAIT
139
+ Labimotion::ConState::NONE
140
140
  else
141
141
  handle_nmr_result(result, att, current_user)
142
142
  end
@@ -31,7 +31,6 @@ module Labimotion
31
31
  case con_state
32
32
  when Labimotion::ConState::NMR
33
33
  self.con_state = Labimotion::NmrMapper.process_ds(id, current_user)
34
- return exec_converter if con_state == Labimotion::ConState::WAIT
35
34
  update_column(:con_state, con_state)
36
35
  when Labimotion::ConState::WAIT
37
36
  self.con_state = Labimotion::Converter.jcamp_converter(id, current_user)
@@ -26,9 +26,9 @@ module Labimotion
26
26
  joins(collections: :user).where(collections: { user_id: user_id })
27
27
  )
28
28
 
29
- # Shared records
29
+ # Shared (synced) records
30
30
  shared = apply_filters.call(
31
- left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
31
+ joins(collections: :sync_collections_users).where(sync_collections_users: { user_id: user_id })
32
32
  )
33
33
 
34
34
  # Combine (remove duplicates), order, and limit
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ class DoseRespOutput < ApplicationRecord
5
+ acts_as_paranoid
6
+ self.table_name = :dose_resp_outputs
7
+
8
+ # Associations
9
+ belongs_to :dose_resp_request, class_name: 'Labimotion::DoseRespRequest'
10
+
11
+ # Validations
12
+ validates :dose_resp_request, presence: true
13
+ validates :output_data, presence: true
14
+ validate :output_data_is_hash
15
+
16
+ # Scopes
17
+ scope :recent, -> { order(created_at: :desc) }
18
+
19
+ private
20
+
21
+ def output_data_is_hash
22
+ return if output_data.is_a?(Hash)
23
+
24
+ errors.add(:output_data, 'must be a Hash')
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ class DoseRespRequest < ApplicationRecord
5
+ acts_as_paranoid
6
+ self.table_name = :dose_resp_requests
7
+
8
+ # Token generation
9
+ has_secure_token :access_token
10
+
11
+ # Callbacks
12
+ before_create :generate_request_id
13
+
14
+ # Associations
15
+ belongs_to :element, class_name: 'Labimotion::Element'
16
+ belongs_to :creator, foreign_key: :created_by, class_name: 'User'
17
+ has_many :dose_resp_outputs, class_name: 'Labimotion::DoseRespOutput', dependent: :destroy
18
+
19
+ # Validations
20
+ validates :element, presence: true
21
+ validates :creator, presence: true
22
+ validates :expires_at, presence: true
23
+ validates :state, inclusion: { in: [-1, 0, 1, 2] }
24
+ validate :metadata_is_hash
25
+ validate :input_metadata_is_hash
26
+
27
+ # State constants
28
+ STATE_ERROR = -1
29
+ STATE_INITIAL = 0
30
+ STATE_PROCESSING = 1
31
+ STATE_COMPLETED = 2
32
+
33
+ # Scopes
34
+ scope :active, -> { where('expires_at > ?', Time.current).where(revoked_at: nil) }
35
+ scope :expired, -> { where('expires_at <= ?', Time.current) }
36
+ scope :revoked, -> { where.not(revoked_at: nil) }
37
+
38
+ # Instance methods
39
+ def expired?
40
+ expires_at.present? && expires_at < Time.current
41
+ end
42
+
43
+ def revoked?
44
+ revoked_at.present?
45
+ end
46
+
47
+ def active?
48
+ !expired? && !revoked?
49
+ end
50
+
51
+ def revoke!
52
+ update!(revoked_at: Time.current, state: STATE_ERROR)
53
+ end
54
+
55
+ def mark_processing!
56
+ update!(state: STATE_PROCESSING) if state == STATE_INITIAL
57
+ end
58
+
59
+ def mark_completed!
60
+ update!(state: STATE_COMPLETED)
61
+ end
62
+
63
+ def mark_error!(message = nil)
64
+ update!(state: STATE_ERROR, resp_message: message)
65
+ end
66
+
67
+ def track_access!
68
+ increment!(:access_count)
69
+ update_columns(
70
+ first_accessed_at: first_accessed_at || Time.current,
71
+ last_accessed_at: Time.current
72
+ )
73
+ end
74
+
75
+ private
76
+
77
+ def metadata_is_hash
78
+ return if wellplates_metadata.nil? || wellplates_metadata.is_a?(Hash)
79
+
80
+ errors.add(:wellplates_metadata, 'must be a hash')
81
+ end
82
+
83
+ def input_metadata_is_hash
84
+ return if input_metadata.nil? || input_metadata.is_a?(Hash)
85
+
86
+ errors.add(:input_metadata, 'must be a hash')
87
+ end
88
+
89
+ def generate_request_id
90
+ self.request_id = "MTT-#{Time.current.strftime('%Y%m%d-%H%M%S')}-#{SecureRandom.hex(3)}"
91
+ end
92
+ end
93
+ end
@@ -41,6 +41,12 @@ module Labimotion
41
41
  has_many :samples, through: :elements_samples, source: :sample
42
42
  has_one :container, :as => :containable
43
43
  has_many :elements_revisions, dependent: :destroy, class_name: 'Labimotion::ElementsRevision'
44
+ has_one :element_variation, dependent: :destroy, class_name: 'Labimotion::ElementVariation', foreign_key: :element_id
45
+
46
+ def variations_count
47
+ rows = element_variation&.variations
48
+ rows.is_a?(Hash) ? rows.size : 0
49
+ end
44
50
 
45
51
  accepts_nested_attributes_for :collections_elements
46
52
 
@@ -135,9 +141,9 @@ module Labimotion
135
141
  joins(collections: :user).where(collections: { user_id: user_id })
136
142
  )
137
143
 
138
- # Shared elements
144
+ # Shared (synced) elements
139
145
  shared = apply_filters.call(
140
- left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
146
+ joins(collections: :sync_collections_users).where(sync_collections_users: { user_id: user_id })
141
147
  )
142
148
 
143
149
  # Combine (remove duplicates), order, and limit
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ class ElementVariation < ApplicationRecord
5
+ self.table_name = :element_variations
6
+
7
+ belongs_to :element, class_name: 'Labimotion::Element'
8
+
9
+ validates :element_id, uniqueness: true
10
+
11
+ def variations_hash
12
+ variations.is_a?(Hash) ? variations : {}
13
+ end
14
+
15
+ def layout_hash
16
+ return {} unless self.class.column_names.include?('layout')
17
+
18
+ layout.is_a?(Hash) ? layout : {}
19
+ end
20
+ end
21
+ end
@@ -12,7 +12,7 @@ module Labimotion
12
12
  end
13
13
 
14
14
  def self.elements_search(params, current_user, c_id, dl)
15
- collection = Collection.accessible_for(current_user).find(c_id)
15
+ collection = Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids).find(c_id)
16
16
  element_scope = Labimotion::Element.joins(:collections_elements).where('collections_elements.collection_id = ?', collection.id).joins(:element_klass).where('element_klasses.id = elements.element_klass_id AND element_klasses.name = ?', params[:selection][:genericElName])
17
17
  element_scope = element_scope.where('elements.name like (?)', "%#{params[:selection][:searchName]}%") if params[:selection][:searchName].present?
18
18
  element_scope = element_scope.where('elements.short_label like (?)', "%#{params[:selection][:searchShowLabel]}%") if params[:selection][:searchShowLabel].present?
@@ -97,7 +97,7 @@ module Labimotion
97
97
  def self.samples_search(c_id = @c_id)
98
98
  sqls = []
99
99
  sps = params[:selection][:searchProperties]
100
- collection = Collection.accessible_for(current_user).find(c_id)
100
+ collection = Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids).find(c_id)
101
101
  element_scope = Sample.joins(:collections_samples).where('collections_samples.collection_id = ?', collection.id)
102
102
  return element_scope if sps.empty?
103
103
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.2.0.rc8'
5
+ VERSION = '2.2.0.rc9'
6
6
  end
data/lib/labimotion.rb CHANGED
@@ -27,6 +27,9 @@ module Labimotion
27
27
  autoload :ExporterAPI, 'labimotion/apis/exporter_api'
28
28
  autoload :StandardLayerAPI, 'labimotion/apis/standard_layer_api'
29
29
  autoload :VocabularyAPI, 'labimotion/apis/vocabulary_api'
30
+ autoload :MttAPI, 'labimotion/apis/mtt_api'
31
+ autoload :DoseRespRequestAPI, 'labimotion/apis/dose_resp_request_api'
32
+ autoload :ElementVariationAPI, 'labimotion/apis/element_variation_api'
30
33
 
31
34
  ######## Entities
32
35
  autoload :PropertiesEntity, 'labimotion/entities/properties_entity'
@@ -49,6 +52,7 @@ module Labimotion
49
52
  autoload :SegmentRevisionEntity, 'labimotion/entities/segment_revision_entity'
50
53
  ## autoload :DatasetRevisionEntity, 'labimotion/entities/dataset_revision_entity'
51
54
  autoload :VocabularyEntity, 'labimotion/entities/vocabulary_entity'
55
+ autoload :ElementVariationEntity, 'labimotion/entities/element_variation_entity'
52
56
 
53
57
  ######## Helpers
54
58
  autoload :GenericHelpers, 'labimotion/helpers/generic_helpers'
@@ -114,11 +118,14 @@ module Labimotion
114
118
  autoload :StdLayersRevision, 'labimotion/models/std_layers_revision'
115
119
 
116
120
  autoload :DeviceDescription, 'labimotion/models/device_description'
121
+ autoload :DoseRespRequest, 'labimotion/models/dose_resp_request'
122
+ autoload :DoseRespOutput, 'labimotion/models/dose_resp_output'
117
123
  autoload :Reaction, 'labimotion/models/reaction'
118
124
  autoload :ResearchPlan, 'labimotion/models/research_plan'
119
125
  autoload :Sample, 'labimotion/models/sample'
120
126
  autoload :Screen, 'labimotion/models/screen'
121
127
  autoload :Wellplate, 'labimotion/models/wellplate'
128
+ autoload :ElementVariation, 'labimotion/models/element_variation'
122
129
 
123
130
  ######## Models/Concerns
124
131
  autoload :GenericKlassRevisions, 'labimotion/models/concerns/generic_klass_revisions'
@@ -126,6 +133,6 @@ module Labimotion
126
133
  autoload :ElementFetchable, 'labimotion/models/concerns/element_fetchable'
127
134
  autoload :Segmentable, 'labimotion/models/concerns/segmentable'
128
135
  autoload :Datasetable, 'labimotion/models/concerns/datasetable'
129
- autoload :AttachmentConverter, 'labimotion/models/concerns/attachment_converter.rb'
136
+ autoload :AttachmentConverter, 'labimotion/models/concerns/attachment_converter'
130
137
  autoload :LinkedProperties, 'labimotion/models/concerns/linked_properties'
131
138
  end
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.2.0.rc8
4
+ version: 2.2.0.rc9
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-05-05 00:00:00.000000000 Z
12
+ date: 2026-06-19 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx
@@ -55,12 +55,15 @@ extra_rdoc_files: []
55
55
  files:
56
56
  - lib/labimotion.rb
57
57
  - lib/labimotion/apis/converter_api.rb
58
+ - lib/labimotion/apis/dose_resp_request_api.rb
59
+ - lib/labimotion/apis/element_variation_api.rb
58
60
  - lib/labimotion/apis/exporter_api.rb
59
61
  - lib/labimotion/apis/generic_dataset_api.rb
60
62
  - lib/labimotion/apis/generic_element_api.rb
61
63
  - lib/labimotion/apis/generic_klass_api.rb
62
64
  - lib/labimotion/apis/labimotion_api.rb
63
65
  - lib/labimotion/apis/labimotion_hub_api.rb
66
+ - lib/labimotion/apis/mtt_api.rb
64
67
  - lib/labimotion/apis/segment_api.rb
65
68
  - lib/labimotion/apis/standard_api.rb
66
69
  - lib/labimotion/apis/standard_layer_api.rb
@@ -75,6 +78,7 @@ files:
75
78
  - lib/labimotion/entities/element_entity.rb
76
79
  - lib/labimotion/entities/element_klass_entity.rb
77
80
  - lib/labimotion/entities/element_revision_entity.rb
81
+ - lib/labimotion/entities/element_variation_entity.rb
78
82
  - lib/labimotion/entities/eln_element_entity.rb
79
83
  - lib/labimotion/entities/generic_entity.rb
80
84
  - lib/labimotion/entities/generic_klass_entity.rb
@@ -90,6 +94,7 @@ files:
90
94
  - lib/labimotion/helpers/element_helpers.rb
91
95
  - lib/labimotion/helpers/exporter_helpers.rb
92
96
  - lib/labimotion/helpers/generic_helpers.rb
97
+ - lib/labimotion/helpers/mtt_helpers.rb
93
98
  - lib/labimotion/helpers/param_helpers.rb
94
99
  - lib/labimotion/helpers/repository_helpers.rb
95
100
  - lib/labimotion/helpers/sample_association_helpers.rb
@@ -129,9 +134,12 @@ files:
129
134
  - lib/labimotion/models/dataset_klasses_revision.rb
130
135
  - lib/labimotion/models/datasets_revision.rb
131
136
  - lib/labimotion/models/device_description.rb
137
+ - lib/labimotion/models/dose_resp_output.rb
138
+ - lib/labimotion/models/dose_resp_request.rb
132
139
  - lib/labimotion/models/element.rb
133
140
  - lib/labimotion/models/element_klass.rb
134
141
  - lib/labimotion/models/element_klasses_revision.rb
142
+ - lib/labimotion/models/element_variation.rb
135
143
  - lib/labimotion/models/elements_element.rb
136
144
  - lib/labimotion/models/elements_revision.rb
137
145
  - lib/labimotion/models/elements_sample.rb