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 +4 -4
- data/CHANGELOG.md +0 -12
- data/lib/labimotion/apis/element_variation_api.rb +3 -58
- data/lib/labimotion/apis/generic_element_api.rb +22 -0
- data/lib/labimotion/entities/properties_entity.rb +28 -0
- data/lib/labimotion/helpers/param_helpers.rb +1 -0
- data/lib/labimotion/libs/converter.rb +5 -43
- data/lib/labimotion/libs/linked_element.rb +193 -0
- data/lib/labimotion/models/concerns/element_fetchable.rb +2 -3
- data/lib/labimotion/models/element.rb +2 -3
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +1 -4
- metadata +3 -20
- data/lib/labimotion/libs/element_variation_column_set.rb +0 -426
- data/lib/labimotion/libs/element_variation_header.rb +0 -119
- data/lib/labimotion/libs/export_element_variations.rb +0 -245
- data/lib/labimotion/libs/import_element_variations.rb +0 -411
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 20affa359afd5082ac4bc5e900b3e66c2c537a4b83fc9032883e33f25dc60362
|
|
4
|
+
data.tar.gz: e8bb4ae665e5de5166790ac27ecf42a95394057a2fd93d5deebc95dc858a5189
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
260
|
-
|
|
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
|
|
30
|
-
# element models themselves, so join through the collections association.
|
|
29
|
+
# Shared records
|
|
31
30
|
shared = apply_filters.call(
|
|
32
|
-
left_joins(
|
|
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
|
|
145
|
-
# Element itself, so join through the collections association.
|
|
144
|
+
# Shared elements
|
|
146
145
|
shared = apply_filters.call(
|
|
147
|
-
left_joins(
|
|
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
|
data/lib/labimotion/version.rb
CHANGED
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.
|
|
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-
|
|
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/
|
|
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
|