labimotion 2.2.0.rc7 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a7b53d1daca8b636571715c7ff62caa557dc59082c1f7733cdd6e6a875795712
4
- data.tar.gz: cab23343b2c90f335571ae7b5b7e714b63cee7e1f403b50b8b3c89e944b23e4a
3
+ metadata.gz: fde410e148abdf50036fd9146e2faea1ccd5078f7c93ec99dbd1c3fa6680ee96
4
+ data.tar.gz: 3a63d545e4b1c9da579e582f956ead5e2d204c4f9078f47803322a4831ce8b50
5
5
  SHA512:
6
- metadata.gz: ffbd3fac0cba826e516a8dd57fed5c0ac0b29a50c15752ca27453cc9dfd28736e71ce44b76efef72690daf1aa23f42a48309228b038a883ac67852564e74c94d
7
- data.tar.gz: 7a3da058102e765cdfa54fa5ece7e3dc022cff2a0f39e9eae1cc31e4053b8e7735b5eda17bc2c05927b1e54d81883c3a885e1367eb85624d4e8437505ee96191
6
+ metadata.gz: 1c2cd08a3c54a452994ab180b014e53545ade8ff6ebf65d19a06a23fe47770b7e27bb2c4e04e8c7c05d4a3ab45875240f5cdac29e3ae1e64f49281ea3d9a6bb3
7
+ data.tar.gz: 318fa9de8609f13f1074620e09aa24d62ad135d956e5415dfa582d262787897dbb70d119144a80227c4b567138fd69e871196dba072b6fb3e9eb5276ae0239f9
@@ -179,10 +179,11 @@ module Labimotion
179
179
  end
180
180
 
181
181
  namespace :klass_revisions do
182
- desc 'list Generic Element Revisions'
182
+ desc 'list Generic Klass Revisions'
183
183
  params do
184
184
  requires :id, type: Integer, desc: 'Generic Element Klass Id'
185
185
  requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass]
186
+ optional :limit, type: Integer, default: 10, desc: 'Max revisions returned'
186
187
  end
187
188
  get do
188
189
  list = list_klass_revisions(params)
@@ -197,6 +198,7 @@ module Labimotion
197
198
  desc 'list Generic Element Revisions'
198
199
  params do
199
200
  requires :id, type: Integer, desc: 'Generic Element Id'
201
+ optional :limit, type: Integer, default: 10, desc: 'Max revisions returned'
200
202
  end
201
203
  get do
202
204
  list = element_revisions(params)
@@ -241,14 +243,15 @@ module Labimotion
241
243
  end
242
244
 
243
245
  namespace :segment_revisions do
244
- desc 'list Generic Element Revisions'
246
+ desc 'list Generic Segment Revisions'
245
247
  params do
246
248
  optional :id, type: Integer, desc: 'Generic Element Id'
249
+ optional :limit, type: Integer, default: 10, desc: 'Max revisions returned'
247
250
  end
248
251
  get do
249
252
  klass = Labimotion::Segment.find(params[:id])
250
253
  list = klass.segments_revisions unless klass.nil?
251
- present list&.order(created_at: :desc)&.limit(10), with: Labimotion::SegmentRevisionEntity, root: 'revisions'
254
+ present list&.order(created_at: :desc)&.limit(params[:limit]), with: Labimotion::SegmentRevisionEntity, root: 'revisions'
252
255
  rescue StandardError => e
253
256
  Labimotion.log_exception(e, current_user)
254
257
  []
@@ -200,7 +200,7 @@ module Labimotion
200
200
  def element_revisions(params)
201
201
  klass = Labimotion::Element.find(params[:id])
202
202
  list = klass.elements_revisions unless klass.nil?
203
- list&.order(created_at: :desc)&.limit(10)
203
+ list&.order(created_at: :desc)&.limit(params[:limit])
204
204
  rescue StandardError => e
205
205
  Labimotion.log_exception(e, current_user)
206
206
  raise e
@@ -95,7 +95,7 @@ module Labimotion
95
95
  def list_klass_revisions(params)
96
96
  klass = "Labimotion::#{params[:klass]}".constantize.find_by(id: params[:id])
97
97
  list = klass.send("#{params[:klass].underscore}es_revisions") unless klass.nil?
98
- list&.order(released_at: :desc)&.limit(10)
98
+ list&.order(released_at: :desc)&.limit(params[:limit])
99
99
  rescue StandardError => e
100
100
  Labimotion.log_exception(e, current_user)
101
101
  raise e
@@ -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 = []
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.2.0.rc7'
5
+ VERSION = '2.2.0.rc9'
6
6
  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.rc7
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-04 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