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.
- checksums.yaml +4 -4
- data/lib/labimotion/apis/dose_resp_request_api.rb +241 -0
- data/lib/labimotion/apis/element_variation_api.rb +45 -0
- data/lib/labimotion/apis/generic_element_api.rb +7 -3
- data/lib/labimotion/apis/labimotion_api.rb +3 -0
- data/lib/labimotion/apis/mtt_api.rb +238 -0
- data/lib/labimotion/entities/element_entity.rb +1 -0
- data/lib/labimotion/entities/element_variation_entity.rb +29 -0
- data/lib/labimotion/helpers/element_helpers.rb +22 -16
- data/lib/labimotion/helpers/generic_helpers.rb +1 -1
- data/lib/labimotion/helpers/mtt_helpers.rb +428 -0
- data/lib/labimotion/helpers/param_helpers.rb +6 -0
- data/lib/labimotion/libs/converter.rb +1 -2
- data/lib/labimotion/libs/export_element.rb +245 -19
- data/lib/labimotion/libs/nmr_mapper.rb +2 -2
- data/lib/labimotion/models/concerns/attachment_converter.rb +0 -1
- data/lib/labimotion/models/concerns/element_fetchable.rb +2 -2
- data/lib/labimotion/models/dose_resp_output.rb +27 -0
- data/lib/labimotion/models/dose_resp_request.rb +93 -0
- data/lib/labimotion/models/element.rb +8 -2
- data/lib/labimotion/models/element_variation.rb +21 -0
- data/lib/labimotion/utils/search.rb +2 -2
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +8 -1
- metadata +10 -2
|
@@ -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] =
|
|
129
|
-
field_obj[:not_table] =
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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('&', '&').gsub('<', '<').gsub('>', '>')
|
|
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::
|
|
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::
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
data/lib/labimotion/version.rb
CHANGED
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
|
|
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.
|
|
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-
|
|
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
|