labimotion 2.3.0.rc1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ed76c84363276af955102072d3491035f014dedb9fcdc835b4ec9c2e9ba199ee
4
- data.tar.gz: 8a5e3a1d1c5ac1fb497d278c1f9a3237dfc0f93271729e05c602339879d6a204
3
+ metadata.gz: 20affa359afd5082ac4bc5e900b3e66c2c537a4b83fc9032883e33f25dc60362
4
+ data.tar.gz: e8bb4ae665e5de5166790ac27ecf42a95394057a2fd93d5deebc95dc858a5189
5
5
  SHA512:
6
- metadata.gz: 1d9d033f42be38117263ab30d502a3eafa5f9fa3b983f834dd1b1e14ab55537c91fafafd5b1643a6d3e2ea5e2e2ec17a8a072220cef56a4b80a34deca8f6600c
7
- data.tar.gz: 926082586397c99e47f634733d2e258b463a9340809e45d5a0d4a788ff508c0335279e80baabef1f89b851691777d764b8657b6825b40a420e962dab4c6e6f36
6
+ metadata.gz: 7e6d407b44837ccb8758aa2a86b6c4b109350dfa50dde7ebb3affae93b070c1efba512b4db2b08f0608b165a25c1a53c4caaa3450f55c1a15e5c1401a1ca87de
7
+ data.tar.gz: 60c1e3df69128dda039990086d98ad816e7fcedbf74e1a90b896b9b94eb7446b3e5feb613e42770be1f8807d72e5ee3a7c07ec8eb8e158b776d5904335d07671
@@ -2,39 +2,103 @@
2
2
 
3
3
  module Labimotion
4
4
  class ConverterAPI < Grape::API
5
+ helpers Labimotion::DatasetHelpers
5
6
  helpers do
7
+ def load_converter_config!
8
+ @conf = Rails.configuration.converter&.url
9
+ @profile = Rails.configuration.converter&.profile
10
+ error!(406) unless @conf && @profile
11
+ end
12
+
13
+ def require_converter_admin!
14
+ error!(401) unless current_user.profile&.data&.fetch('converter_admin', false)
15
+ end
16
+
17
+ def uploaded_file
18
+ params[:file].is_a?(Array) ? params[:file][0] : params[:file]
19
+ end
6
20
  end
21
+
7
22
  resource :converter do
23
+ resource :datasets do
24
+ desc 'list Generic Dataset Klass'
25
+ get do
26
+ list = klass_list(true, false)
27
+ list.map do |kl|
28
+ pr = kl.properties_release
29
+ pr['name'] = kl.label
30
+ pr['ols'] = kl.ols_term_id
31
+ pr
32
+ end || []
33
+ end
34
+ end
35
+
36
+ resource :datasets_units do
37
+ desc 'list Generic Dataset Klass'
38
+ get do
39
+ Labimotion::Units::FIELDS
40
+ end
41
+ end
42
+
8
43
  resource :profiles do
44
+
9
45
  before do
10
- @conf = Rails.configuration.try(:converter).try(:url)
11
- @profile = Rails.configuration.try(:converter).try(:profile)
12
- error!(406) unless @conf && @profile
46
+ load_converter_config!
13
47
  end
14
- desc 'fetch profiles'
48
+
49
+ desc 'Fetch profiles (no admin required)'
15
50
  get do
16
51
  profiles = Labimotion::Converter.fetch_profiles
17
52
  { profiles: profiles, client: @profile }
18
53
  end
19
- desc 'create profile'
54
+
55
+ desc 'Create profile'
20
56
  post do
57
+ require_converter_admin!
21
58
  Labimotion::Converter.create_profile(params)
22
59
  end
23
- desc 'update profile'
60
+
24
61
  route_param :id do
62
+ desc 'Update profile'
25
63
  put do
64
+ require_converter_admin!
26
65
  Labimotion::Converter.update_profile(params)
27
66
  end
28
- end
29
- desc 'delete profile'
30
- route_param :id do
67
+
68
+ desc 'Delete profile'
31
69
  delete do
32
- id = params[:id]
33
- Labimotion::Converter.delete_profile(id)
70
+ require_converter_admin!
71
+ Labimotion::Converter.delete_profile(params[:id])
34
72
  end
35
73
  end
36
- end
37
74
 
75
+ resource :restore do
76
+ route_param :profile_id,
77
+ requirements: {
78
+ profile_id: /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/
79
+ } do
80
+
81
+ route_param :profile_version,
82
+ requirements: {
83
+ profile_version: /\d+\.\d+/
84
+ } do
85
+ get do
86
+ {test_version: params[:profile_id], version: params[:profile_version]}
87
+ end
88
+ desc 'Restore profile'
89
+ post do
90
+ require_converter_admin!
91
+
92
+ Labimotion::Converter.restore(
93
+ params[:profile_id],
94
+ params[:profile_version],
95
+ params[:hard]
96
+ )
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
38
102
  resource :structure do
39
103
  helpers do
40
104
  def convert_structure(molfile)
@@ -66,10 +130,8 @@ module Labimotion
66
130
 
67
131
  resource :options do
68
132
  before do
69
- error!(401) unless current_user.profile&.data['converter_admin'] == true
70
- @conf = Rails.configuration.try(:converter).try(:url)
71
- @profile = Rails.configuration.try(:converter).try(:profile)
72
- error!(406) unless @conf && @profile
133
+ load_converter_config!
134
+ require_converter_admin!
73
135
  end
74
136
  desc 'fetch options'
75
137
  get do
@@ -80,18 +142,31 @@ module Labimotion
80
142
 
81
143
  resource :tables do
82
144
  before do
83
- error!(401) unless current_user.profile&.data['converter_admin'] == true
84
- @conf = Rails.configuration.try(:converter).try(:url)
85
- @profile = Rails.configuration.try(:converter).try(:profile)
86
- error!(406) unless @conf && @profile
145
+ load_converter_config!
146
+ require_converter_admin!
87
147
  end
88
148
  desc 'create tables'
89
149
  post do
90
- res = Labimotion::Converter.create_tables(params[:file][0]['tempfile']) unless params[:file].empty?
91
- res['metadata']['file_name'] = params[:file][0]['filename']
150
+ file = uploaded_file
151
+ res = Labimotion::Converter.create_tables(file['tempfile']) unless file.nil?
152
+ res['metadata']['file_name'] = file['filename']
92
153
  res
93
154
  end
94
155
  end
156
+
157
+ resource :conversions do
158
+ before do
159
+ load_converter_config!
160
+ require_converter_admin!
161
+ end
162
+ desc 'convert file'
163
+ post do
164
+ file = uploaded_file
165
+ res = Labimotion::Converter.test_conversions(file['tempfile'], params[:format]) unless file.nil?
166
+ res
167
+ end
168
+ end
169
+
95
170
  end
96
171
  end
97
- end
172
+ 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
@@ -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
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.3.0.rc1'
5
+ VERSION = '2.4.0.rc1'
6
6
  end
data/lib/labimotion.rb CHANGED
@@ -89,6 +89,7 @@ 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'
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.rc1
4
+ version: 2.4.0.rc1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chia-Lin Lin
@@ -118,6 +118,7 @@ files:
118
118
  - lib/labimotion/libs/dataset_builder.rb
119
119
  - lib/labimotion/libs/export_dataset.rb
120
120
  - lib/labimotion/libs/export_element.rb
121
+ - lib/labimotion/libs/linked_element.rb
121
122
  - lib/labimotion/libs/nmr_mapper.rb
122
123
  - lib/labimotion/libs/properties_handler.rb
123
124
  - lib/labimotion/libs/sample_association.rb