labimotion 2.3.0.rc3 → 2.3.0.rc5
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/CHANGELOG.md +12 -0
- data/lib/labimotion/apis/element_variation_api.rb +58 -3
- data/lib/labimotion/libs/converter.rb +43 -5
- data/lib/labimotion/libs/element_variation_column_set.rb +426 -0
- data/lib/labimotion/libs/element_variation_header.rb +119 -0
- data/lib/labimotion/libs/export_element_variations.rb +245 -0
- data/lib/labimotion/libs/import_element_variations.rb +411 -0
- data/lib/labimotion/models/concerns/element_fetchable.rb +3 -2
- data/lib/labimotion/models/element.rb +3 -2
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +4 -0
- metadata +20 -2
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'caxlsx'
|
|
4
|
+
require 'roo'
|
|
5
|
+
require 'securerandom'
|
|
6
|
+
require 'labimotion/libs/element_variation_column_set'
|
|
7
|
+
|
|
8
|
+
module Labimotion
|
|
9
|
+
## ImportElementVariations
|
|
10
|
+
# Reads an .xlsx workbook produced by ExportElementVariations back into an
|
|
11
|
+
# element's variations.
|
|
12
|
+
#
|
|
13
|
+
# Rows are merged by uuid: a sheet row updates only the columns the sheet
|
|
14
|
+
# actually carries, variations absent from the sheet are left untouched, and a
|
|
15
|
+
# row with a blank `__uuid` becomes a new variation. A wholesale replace would
|
|
16
|
+
# silently drop data held in columns the user had not selected when exporting.
|
|
17
|
+
class ImportElementVariations
|
|
18
|
+
SHEET_NAME = 'Variations'
|
|
19
|
+
# The machine key row is found by its `__uuid` cell rather than fixed at a
|
|
20
|
+
# row number, so a workbook from an older export -- whose header was one row
|
|
21
|
+
# instead of three -- still imports.
|
|
22
|
+
KEY_ROW_SEARCH_LIMIT = 8
|
|
23
|
+
MULTI_VALUE_SEPARATOR = ';'
|
|
24
|
+
GROUP_DISALLOWED = /[^a-zA-Z0-9_\-.,\s]/
|
|
25
|
+
GROUP_MAX_LENGTH = 64
|
|
26
|
+
# A cell whose value a column cannot hold. Treated like a blank cell: it
|
|
27
|
+
# clears what the column held, but never adds a value that was not there.
|
|
28
|
+
REJECTED = Object.new.freeze
|
|
29
|
+
|
|
30
|
+
class InvalidWorkbook < StandardError; end
|
|
31
|
+
|
|
32
|
+
def initialize(element, file_path, segment_klasses: nil, record: nil)
|
|
33
|
+
@element = element
|
|
34
|
+
@file_path = file_path
|
|
35
|
+
@segment_klasses = segment_klasses
|
|
36
|
+
@record = record
|
|
37
|
+
@warnings = []
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
attr_reader :element, :file_path, :warnings
|
|
41
|
+
|
|
42
|
+
def execute!
|
|
43
|
+
sheet = open_sheet
|
|
44
|
+
mapping = build_column_mapping(sheet)
|
|
45
|
+
parsed_rows = parse_rows(sheet, mapping)
|
|
46
|
+
persist(parsed_rows)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
def record
|
|
52
|
+
@record ||= Labimotion::ElementVariation.find_or_initialize_by(element_id: element.id)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def column_set
|
|
56
|
+
@column_set ||= Labimotion::ElementVariationColumnSet.new(
|
|
57
|
+
element, record, segment_klasses: @segment_klasses
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# ---- workbook reading -------------------------------------------------
|
|
62
|
+
|
|
63
|
+
def open_sheet
|
|
64
|
+
workbook = Roo::Spreadsheet.open(file_path, extension: :xlsx)
|
|
65
|
+
unless workbook.sheets.include?(SHEET_NAME)
|
|
66
|
+
raise InvalidWorkbook, "Sheet '#{SHEET_NAME}' not found. Use a file produced by the variations export."
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
workbook.sheet(SHEET_NAME)
|
|
70
|
+
rescue Roo::Error, Zip::Error, IOError => e
|
|
71
|
+
raise InvalidWorkbook, "Cannot read the file as .xlsx: #{e.message}"
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# The machine key row is immediately followed by the unit row, and the data
|
|
75
|
+
# starts after it. Both are hidden in the exported workbook.
|
|
76
|
+
def key_row(sheet)
|
|
77
|
+
@key_row ||= begin
|
|
78
|
+
limit = [sheet.last_row.to_i, KEY_ROW_SEARCH_LIMIT].min
|
|
79
|
+
found = (1..limit).find do |number|
|
|
80
|
+
Array(sheet.row(number)).any? do |cell|
|
|
81
|
+
cell.to_s.strip == Labimotion::ElementVariationColumnSet::UUID_KEY
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
found || raise(
|
|
85
|
+
InvalidWorkbook,
|
|
86
|
+
"No column key row found: none of the first #{KEY_ROW_SEARCH_LIMIT} rows carries a " \
|
|
87
|
+
"'#{Labimotion::ElementVariationColumnSet::UUID_KEY}' cell. " \
|
|
88
|
+
'Use a file produced by the variations export.'
|
|
89
|
+
)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Column index -> { key:, unit: }.
|
|
94
|
+
def build_column_mapping(sheet)
|
|
95
|
+
keys = safe_row(sheet, key_row(sheet))
|
|
96
|
+
units = safe_row(sheet, key_row(sheet) + 1)
|
|
97
|
+
|
|
98
|
+
mapping = {}
|
|
99
|
+
keys.each_with_index do |key, index|
|
|
100
|
+
key = key.to_s.strip
|
|
101
|
+
next if key.empty?
|
|
102
|
+
|
|
103
|
+
mapping[index] = { key: key, unit: units[index].to_s.strip }
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
validate_mapping!(mapping, key_row(sheet))
|
|
107
|
+
warn_unknown_keys(mapping)
|
|
108
|
+
mapping
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def warn_unknown_keys(mapping)
|
|
112
|
+
unknown = mapping.reject { |_index, column| known_key?(column[:key]) }
|
|
113
|
+
return if unknown.empty?
|
|
114
|
+
|
|
115
|
+
named = unknown.map { |index, column| "#{column_ref(index)} (#{column[:key]})" }
|
|
116
|
+
@warnings << "Ignored unknown column(s) #{named.join(', ')} - the element schema may have " \
|
|
117
|
+
'changed since this file was exported.'
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def known_key?(key)
|
|
121
|
+
key == Labimotion::ElementVariationColumnSet::UUID_KEY || !column_set.lookup(key).nil?
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def validate_mapping!(mapping, key_row_number)
|
|
125
|
+
keys = mapping.values.map { |column| column[:key] }
|
|
126
|
+
missing = [
|
|
127
|
+
Labimotion::ElementVariationColumnSet::UUID_KEY,
|
|
128
|
+
Labimotion::ElementVariationColumnSet::NAME_KEY
|
|
129
|
+
] - keys
|
|
130
|
+
return if missing.empty?
|
|
131
|
+
|
|
132
|
+
raise InvalidWorkbook,
|
|
133
|
+
"Row #{key_row_number} is missing the column keys #{missing.join(', ')}. " \
|
|
134
|
+
'Use a file produced by the variations export.'
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def safe_row(sheet, number)
|
|
138
|
+
return [] if sheet.last_row.nil? || sheet.last_row < number
|
|
139
|
+
|
|
140
|
+
Array(sheet.row(number))
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def parse_rows(sheet, mapping)
|
|
144
|
+
last_row = sheet.last_row.to_i
|
|
145
|
+
first_data_row = key_row(sheet) + 2
|
|
146
|
+
return [] if last_row < first_data_row
|
|
147
|
+
|
|
148
|
+
(first_data_row..last_row).filter_map do |number|
|
|
149
|
+
cells = Array(sheet.row(number))
|
|
150
|
+
next if cells.all? { |cell| blank_cell?(cell) }
|
|
151
|
+
|
|
152
|
+
parse_row(cells, mapping, number)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Each cell carries the spreadsheet reference it came from, so a warning can
|
|
157
|
+
# point the user at the cell to fix rather than just naming the bad value.
|
|
158
|
+
def parse_row(cells, mapping, row_number)
|
|
159
|
+
parsed = { uuid: nil, cells: {} }
|
|
160
|
+
mapping.each do |index, column|
|
|
161
|
+
value = cells[index]
|
|
162
|
+
if column[:key] == Labimotion::ElementVariationColumnSet::UUID_KEY
|
|
163
|
+
parsed[:uuid] = blank_cell?(value) ? nil : cell_to_s(value)
|
|
164
|
+
else
|
|
165
|
+
parsed[:cells][column[:key]] =
|
|
166
|
+
{ value: value, unit: column[:unit], ref: cell_ref(index, row_number) }
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
parsed
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
# 0-based column index to the A1 notation a spreadsheet app shows.
|
|
173
|
+
def column_ref(column_index)
|
|
174
|
+
Axlsx.col_ref(column_index)
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def cell_ref(column_index, row_number)
|
|
178
|
+
"#{column_ref(column_index)}#{row_number}"
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# ---- merging ----------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
def persist(parsed_rows)
|
|
184
|
+
variations = deep_dup(record.variations_hash)
|
|
185
|
+
row_order = []
|
|
186
|
+
|
|
187
|
+
parsed_rows.each do |parsed|
|
|
188
|
+
uuid = parsed[:uuid] || SecureRandom.uuid
|
|
189
|
+
base = variations[uuid] || empty_row(uuid)
|
|
190
|
+
variations[uuid] = merge_row(base, parsed[:cells], uuid)
|
|
191
|
+
row_order << uuid
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
record.variations = variations
|
|
195
|
+
apply_row_order(row_order, variations.keys)
|
|
196
|
+
record.save!
|
|
197
|
+
record
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def apply_row_order(row_order, all_uuids)
|
|
201
|
+
return unless record.respond_to?(:layout=)
|
|
202
|
+
return unless record.class.respond_to?(:column_names) && record.class.column_names.include?('layout')
|
|
203
|
+
|
|
204
|
+
layout = deep_dup(record.layout_hash)
|
|
205
|
+
layout['rowOrder'] = row_order + (all_uuids - row_order)
|
|
206
|
+
record.layout = layout
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def empty_row(uuid)
|
|
210
|
+
{
|
|
211
|
+
'uuid' => uuid,
|
|
212
|
+
'name' => 'Variation',
|
|
213
|
+
'properties' => {},
|
|
214
|
+
'metadata' => { 'notes' => '', 'analyses' => [], 'group' => '' },
|
|
215
|
+
'segments' => {}
|
|
216
|
+
}
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def merge_row(base, cells, uuid)
|
|
220
|
+
row = deep_dup(base)
|
|
221
|
+
row['uuid'] = uuid
|
|
222
|
+
row['properties'] ||= {}
|
|
223
|
+
row['metadata'] ||= {}
|
|
224
|
+
row['segments'] ||= {}
|
|
225
|
+
|
|
226
|
+
cells.each do |key, cell|
|
|
227
|
+
column = column_set.lookup(key)
|
|
228
|
+
next if column.nil?
|
|
229
|
+
|
|
230
|
+
apply_cell(row, column, cell)
|
|
231
|
+
end
|
|
232
|
+
row
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
def apply_cell(row, column, cell)
|
|
236
|
+
case column.kind
|
|
237
|
+
when :name then row['name'] = blank_cell?(cell[:value]) ? row['name'] : cell_to_s(cell[:value])
|
|
238
|
+
when :property then apply_property(row, column, cell)
|
|
239
|
+
when :metadata then apply_metadata(row, column, cell)
|
|
240
|
+
when :segment then apply_segment(row, column, cell)
|
|
241
|
+
end
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def apply_property(row, column, cell)
|
|
245
|
+
key = column.key.sub(/\Aprop:/, '')
|
|
246
|
+
existing = row['properties'][key]
|
|
247
|
+
|
|
248
|
+
# A blank cell for a property the row never had is not a deletion.
|
|
249
|
+
return if existing.nil? && blank_cell?(cell[:value])
|
|
250
|
+
|
|
251
|
+
value = if analyses_field?(column.field_type)
|
|
252
|
+
analysis_ids(cell[:value], cell[:ref])
|
|
253
|
+
else
|
|
254
|
+
coerce_in(cell, column)
|
|
255
|
+
end
|
|
256
|
+
if value.equal?(REJECTED)
|
|
257
|
+
return if existing.nil? # never stored, so there is nothing to clear
|
|
258
|
+
|
|
259
|
+
value = nil
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
base_cell = existing.is_a?(Hash) ? existing : {}
|
|
263
|
+
row['properties'][key] = base_cell.merge('value' => value, 'unit' => cell[:unit])
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def apply_metadata(row, column, cell)
|
|
267
|
+
field = column.key.sub(/\Ameta:/, '')
|
|
268
|
+
row['metadata'][field] =
|
|
269
|
+
case field
|
|
270
|
+
when 'analyses' then analysis_ids(cell[:value], cell[:ref])
|
|
271
|
+
when 'group' then sanitize_group(cell[:value])
|
|
272
|
+
else blank_cell?(cell[:value]) ? '' : cell_to_s(cell[:value])
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def apply_segment(row, column, cell)
|
|
277
|
+
_, klass_id, field_key = column.key.split(':', 3)
|
|
278
|
+
snapshot = row['segments'][klass_id.to_s]
|
|
279
|
+
return if snapshot.nil? && blank_cell?(cell[:value])
|
|
280
|
+
|
|
281
|
+
value = coerce_in(cell, column)
|
|
282
|
+
if value.equal?(REJECTED)
|
|
283
|
+
return if snapshot.nil? # never stored, so there is nothing to clear
|
|
284
|
+
|
|
285
|
+
value = nil
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
snapshot ||= {}
|
|
289
|
+
snapshot[field_key] = value
|
|
290
|
+
row['segments'][klass_id.to_s] = snapshot
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# ---- value coercion ---------------------------------------------------
|
|
294
|
+
|
|
295
|
+
def coerce_in(cell, column)
|
|
296
|
+
value = cell[:value]
|
|
297
|
+
return nil if blank_cell?(value)
|
|
298
|
+
|
|
299
|
+
case column.field_type.to_s
|
|
300
|
+
when 'integer' then to_integer(cell, column)
|
|
301
|
+
when 'number', 'system-defined' then to_number(cell, column)
|
|
302
|
+
when 'select-multi' then split_multi(value)
|
|
303
|
+
else cell_to_s(value)
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
def to_integer(cell, column)
|
|
308
|
+
float = numeric(cell[:value])
|
|
309
|
+
return reject(cell, column, 'takes numbers only') if float.nil?
|
|
310
|
+
return reject(cell, column, 'takes whole numbers only') unless (float % 1).zero?
|
|
311
|
+
|
|
312
|
+
float.to_i
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# xlsx has a single numeric type, so a stored 1.0 comes back as 1. Normalise
|
|
316
|
+
# integral values to Integer rather than leave the result depending on how
|
|
317
|
+
# the spreadsheet happened to round-trip the cell.
|
|
318
|
+
def to_number(cell, column)
|
|
319
|
+
float = numeric(cell[:value])
|
|
320
|
+
return reject(cell, column, 'takes numbers only') if float.nil?
|
|
321
|
+
|
|
322
|
+
(float % 1).zero? ? float.to_i : float
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Strict: "12abc" is not a number. A single comma reads as the decimal
|
|
326
|
+
# separator. Kept in step with coerceFieldValue in the React grid, so a cell
|
|
327
|
+
# cannot mean one thing there and another here.
|
|
328
|
+
def numeric(value)
|
|
329
|
+
return value.to_f if value.is_a?(Numeric)
|
|
330
|
+
|
|
331
|
+
Float(value.to_s.strip.sub(',', '.'))
|
|
332
|
+
rescue ArgumentError, TypeError
|
|
333
|
+
nil
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
# A numeric column holds a number or nothing at all. The text is not stored,
|
|
337
|
+
# and whatever the column held before is cleared: keeping it would leave the
|
|
338
|
+
# grid showing the very value the warning says was not accepted -- and that
|
|
339
|
+
# value may itself be text, from before these columns were checked.
|
|
340
|
+
def reject(cell, column, reason)
|
|
341
|
+
@warnings << "Cell #{cell[:ref]}: cleared '#{cell_to_s(cell[:value])}' - " \
|
|
342
|
+
"#{column.label} #{reason}."
|
|
343
|
+
REJECTED
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
def split_multi(value)
|
|
347
|
+
return value if value.is_a?(Array)
|
|
348
|
+
|
|
349
|
+
cell_to_s(value).split(MULTI_VALUE_SEPARATOR).map(&:strip).reject(&:empty?)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
def analyses_field?(field_type)
|
|
353
|
+
field_type.to_s == Labimotion::ElementVariationColumnSet::ANALYSES_FIELD
|
|
354
|
+
end
|
|
355
|
+
|
|
356
|
+
def analysis_ids(value, ref)
|
|
357
|
+
return [] if blank_cell?(value)
|
|
358
|
+
|
|
359
|
+
ids = split_multi(value).filter_map do |token|
|
|
360
|
+
Integer(token)
|
|
361
|
+
rescue ArgumentError, TypeError
|
|
362
|
+
@warnings << "Cell #{ref}: ignored non-numeric analysis reference '#{token}'."
|
|
363
|
+
nil
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
known, unknown = ids.partition { |id| element_analysis_ids.include?(id) }
|
|
367
|
+
unless unknown.empty?
|
|
368
|
+
@warnings << "Cell #{ref}: ignored analysis id(s) #{unknown.join(', ')} - " \
|
|
369
|
+
'they do not belong to this element.'
|
|
370
|
+
end
|
|
371
|
+
known
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def element_analysis_ids
|
|
375
|
+
@element_analysis_ids ||= begin
|
|
376
|
+
element.respond_to?(:analyses) ? Array(element.analyses).map(&:id) : []
|
|
377
|
+
rescue StandardError
|
|
378
|
+
[]
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def sanitize_group(value)
|
|
383
|
+
return '' if blank_cell?(value)
|
|
384
|
+
|
|
385
|
+
cell_to_s(value).gsub(GROUP_DISALLOWED, '')[0, GROUP_MAX_LENGTH].to_s
|
|
386
|
+
end
|
|
387
|
+
|
|
388
|
+
# ---- cell helpers -----------------------------------------------------
|
|
389
|
+
|
|
390
|
+
def blank_cell?(value)
|
|
391
|
+
value.nil? || (value.is_a?(String) && value.strip.empty?)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
# Roo hands back Floats for anything numeric-looking, so an integral value
|
|
395
|
+
# from a text column would otherwise stringify as "12.0".
|
|
396
|
+
def cell_to_s(value)
|
|
397
|
+
return value if value.is_a?(String)
|
|
398
|
+
return format('%d', value) if value.is_a?(Float) && (value % 1).zero?
|
|
399
|
+
|
|
400
|
+
value.to_s
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def deep_dup(value)
|
|
404
|
+
case value
|
|
405
|
+
when Hash then value.each_with_object({}) { |(key, val), acc| acc[key] = deep_dup(val) }
|
|
406
|
+
when Array then value.map { |item| deep_dup(item) }
|
|
407
|
+
else value
|
|
408
|
+
end
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
end
|
|
@@ -26,9 +26,10 @@ module Labimotion
|
|
|
26
26
|
joins(collections: :user).where(collections: { user_id: user_id })
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
-
# Shared records
|
|
29
|
+
# Shared records — collection_shares hangs off Collection, not off the
|
|
30
|
+
# element models themselves, so join through the collections association.
|
|
30
31
|
shared = apply_filters.call(
|
|
31
|
-
left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
|
|
32
|
+
left_joins(collections: :collection_shares).where(collection_shares: { shared_with_id: user_id })
|
|
32
33
|
)
|
|
33
34
|
|
|
34
35
|
# Combine (remove duplicates), order, and limit
|
|
@@ -141,9 +141,10 @@ module Labimotion
|
|
|
141
141
|
joins(collections: :user).where(collections: { user_id: user_id })
|
|
142
142
|
)
|
|
143
143
|
|
|
144
|
-
# Shared elements
|
|
144
|
+
# Shared elements — collection_shares hangs off Collection, not off
|
|
145
|
+
# Element itself, so join through the collections association.
|
|
145
146
|
shared = apply_filters.call(
|
|
146
|
-
left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
|
|
147
|
+
left_joins(collections: :collection_shares).where(collection_shares: { shared_with_id: user_id })
|
|
147
148
|
)
|
|
148
149
|
|
|
149
150
|
# Combine (remove duplicates), order, and limit
|
data/lib/labimotion/version.rb
CHANGED
data/lib/labimotion.rb
CHANGED
|
@@ -92,6 +92,10 @@ module Labimotion
|
|
|
92
92
|
autoload :AttachmentHandler, 'labimotion/libs/attachment_handler'
|
|
93
93
|
autoload :VocabularyHandler, 'labimotion/libs/vocabulary_handler'
|
|
94
94
|
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'
|
|
95
99
|
|
|
96
100
|
######## Utils
|
|
97
101
|
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.
|
|
4
|
+
version: 2.3.0.rc5
|
|
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-
|
|
12
|
+
date: 2026-07-10 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: caxlsx
|
|
@@ -45,6 +45,20 @@ 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'
|
|
48
62
|
description:
|
|
49
63
|
email:
|
|
50
64
|
- chia-lin.lin@kit.edu
|
|
@@ -116,8 +130,12 @@ files:
|
|
|
116
130
|
- lib/labimotion/libs/data/vocab/Standard.json
|
|
117
131
|
- lib/labimotion/libs/data/vocab/System.json
|
|
118
132
|
- lib/labimotion/libs/dataset_builder.rb
|
|
133
|
+
- lib/labimotion/libs/element_variation_column_set.rb
|
|
134
|
+
- lib/labimotion/libs/element_variation_header.rb
|
|
119
135
|
- lib/labimotion/libs/export_dataset.rb
|
|
120
136
|
- lib/labimotion/libs/export_element.rb
|
|
137
|
+
- lib/labimotion/libs/export_element_variations.rb
|
|
138
|
+
- lib/labimotion/libs/import_element_variations.rb
|
|
121
139
|
- lib/labimotion/libs/nmr_mapper.rb
|
|
122
140
|
- lib/labimotion/libs/properties_handler.rb
|
|
123
141
|
- lib/labimotion/libs/sample_association.rb
|