labimotion 2.2.0.rc16 → 2.2.0.rc18
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/generic_element_api.rb +0 -1
- data/lib/labimotion/apis/labimotion_api.rb +1 -1
- data/lib/labimotion/apis/mtt_api.rb +37 -42
- data/lib/labimotion/apis/wellplate_api.rb +60 -0
- data/lib/labimotion/helpers/element_helpers.rb +15 -21
- data/lib/labimotion/helpers/mtt_helpers.rb +92 -0
- data/lib/labimotion/libs/data/mapper/Source.json +4 -2
- data/lib/labimotion/models/concerns/element_fetchable.rb +2 -2
- data/lib/labimotion/models/element.rb +2 -2
- data/lib/labimotion/models/elements_wellplate.rb +10 -0
- data/lib/labimotion/utils/mapper_utils.rb +55 -4
- data/lib/labimotion/utils/search.rb +2 -2
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +2 -1
- metadata +4 -3
- data/lib/labimotion/apis/dose_resp_request_api.rb +0 -241
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ac9e13a86048207de58add4ce3c2670dd3d3ece7b334103a4b8afef774309bd7
|
|
4
|
+
data.tar.gz: a21c3957630ea45d248eec5f49999e31424d3ea30c70b3d9b34b11dbe3105c12
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7ec1e065535fe7fbfa1c94f82f1f5f06057857aa6439b41fab7e8570b4b584276a9625fd5d806431a6abe3307b147107f6e91469c65b351bcf3b5a9f868d8d83
|
|
7
|
+
data.tar.gz: 4ab8e424d00107d029dbe0cdd97740a2d2470994121c1d82b40fd902c5bbd1d1712849756da75631f675666ee39f09a4d56f0839452eb0a29a9b8a78c1ef37bc
|
|
@@ -432,7 +432,6 @@ module Labimotion
|
|
|
432
432
|
desc 'Return serialized elements of current user'
|
|
433
433
|
params do
|
|
434
434
|
optional :collection_id, type: Integer, desc: 'Collection id'
|
|
435
|
-
optional :sync_collection_id, type: Integer, desc: 'SyncCollectionsUser id'
|
|
436
435
|
optional :el_type, type: String, desc: 'element klass name'
|
|
437
436
|
optional :from_date, type: Integer, desc: 'created_date from in ms'
|
|
438
437
|
optional :to_date, type: Integer, desc: 'created_date to in ms'
|
|
@@ -14,9 +14,9 @@ module Labimotion
|
|
|
14
14
|
mount Labimotion::VocabularyAPI
|
|
15
15
|
mount Labimotion::UserAPI
|
|
16
16
|
mount Labimotion::MttAPI
|
|
17
|
-
mount Labimotion::DoseRespRequestAPI
|
|
18
17
|
mount Labimotion::ElementVariationAPI
|
|
19
18
|
mount Labimotion::LabimotionDoiAPI
|
|
20
19
|
mount Labimotion::LabimotionTemplateBrowseAPI
|
|
20
|
+
mount Labimotion::WellplateAPI
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -29,46 +29,19 @@ module Labimotion
|
|
|
29
29
|
|
|
30
30
|
resource :mtt do
|
|
31
31
|
namespace :requests do
|
|
32
|
-
desc 'Get MTT requests for current user'
|
|
32
|
+
desc 'Get MTT requests for current user, scoped to a single element'
|
|
33
|
+
params do
|
|
34
|
+
requires :element_id, type: Integer, desc: 'Only return requests for this element'
|
|
35
|
+
end
|
|
33
36
|
get do
|
|
34
|
-
# Get
|
|
37
|
+
# Get requests created by current user for the selected element only
|
|
35
38
|
requests = Labimotion::DoseRespRequest
|
|
36
39
|
.includes(:dose_resp_outputs)
|
|
37
|
-
.where(created_by: current_user.id)
|
|
40
|
+
.where(created_by: current_user.id, element_id: params[:element_id])
|
|
38
41
|
.order(created_at: :desc)
|
|
39
42
|
|
|
40
43
|
# Return formatted response
|
|
41
|
-
requests.map
|
|
42
|
-
{
|
|
43
|
-
id: req.id,
|
|
44
|
-
request_id: req.request_id,
|
|
45
|
-
element_id: req.element_id,
|
|
46
|
-
state: req.state,
|
|
47
|
-
state_name: case req.state
|
|
48
|
-
when Labimotion::DoseRespRequest::STATE_ERROR then 'error'
|
|
49
|
-
when Labimotion::DoseRespRequest::STATE_INITIAL then 'initial'
|
|
50
|
-
when Labimotion::DoseRespRequest::STATE_PROCESSING then 'processing'
|
|
51
|
-
when Labimotion::DoseRespRequest::STATE_COMPLETED then 'completed'
|
|
52
|
-
else 'unknown'
|
|
53
|
-
end,
|
|
54
|
-
created_at: req.created_at,
|
|
55
|
-
expires_at: req.expires_at,
|
|
56
|
-
expired: req.expired?,
|
|
57
|
-
revoked: req.revoked?,
|
|
58
|
-
active: req.active?,
|
|
59
|
-
resp_message: req.resp_message,
|
|
60
|
-
last_accessed_at: req.last_accessed_at,
|
|
61
|
-
access_count: req.access_count || 0,
|
|
62
|
-
outputs: req.dose_resp_outputs.map do |output|
|
|
63
|
-
{
|
|
64
|
-
id: output.id,
|
|
65
|
-
output_data: output.output_data,
|
|
66
|
-
notes: output.notes,
|
|
67
|
-
created_at: output.created_at
|
|
68
|
-
}
|
|
69
|
-
end
|
|
70
|
-
}
|
|
71
|
-
end
|
|
44
|
+
requests.map { |req| mtt_request_json(req, include_outputs: true) }
|
|
72
45
|
end
|
|
73
46
|
|
|
74
47
|
desc 'Delete one or multiple MTT requests'
|
|
@@ -132,13 +105,7 @@ module Labimotion
|
|
|
132
105
|
id: request.id,
|
|
133
106
|
request_id: request.request_id,
|
|
134
107
|
state: request.state,
|
|
135
|
-
state_name:
|
|
136
|
-
when Labimotion::DoseRespRequest::STATE_ERROR then 'error'
|
|
137
|
-
when Labimotion::DoseRespRequest::STATE_INITIAL then 'initial'
|
|
138
|
-
when Labimotion::DoseRespRequest::STATE_PROCESSING then 'processing'
|
|
139
|
-
when Labimotion::DoseRespRequest::STATE_COMPLETED then 'completed'
|
|
140
|
-
else 'unknown'
|
|
141
|
-
end,
|
|
108
|
+
state_name: mtt_state_name(request.state),
|
|
142
109
|
resp_message: request.resp_message,
|
|
143
110
|
revoked: request.revoked?,
|
|
144
111
|
updated_at: request.updated_at
|
|
@@ -183,6 +150,34 @@ module Labimotion
|
|
|
183
150
|
rescue StandardError => e
|
|
184
151
|
error!("Error deleting outputs: #{e.message}", 500)
|
|
185
152
|
end
|
|
153
|
+
|
|
154
|
+
desc 'Delete a single result (by sample name) from an output'
|
|
155
|
+
params do
|
|
156
|
+
requires :id, type: Integer, desc: 'Output ID'
|
|
157
|
+
requires :sample_name, type: String, desc: 'Sample name (result[].name) to remove'
|
|
158
|
+
end
|
|
159
|
+
delete ':id/results' do
|
|
160
|
+
# Only operate on outputs whose parent request belongs to the current user
|
|
161
|
+
output = Labimotion::DoseRespOutput
|
|
162
|
+
.joins(:dose_resp_request)
|
|
163
|
+
.where(dose_resp_requests: { created_by: current_user.id })
|
|
164
|
+
.find_by(id: params[:id])
|
|
165
|
+
|
|
166
|
+
error!('Output not found or unauthorized', 404) unless output
|
|
167
|
+
|
|
168
|
+
outcome = remove_mtt_result_by_sample_name(output, params[:sample_name])
|
|
169
|
+
error!('Result not found in output', 404) unless outcome[:removed]
|
|
170
|
+
|
|
171
|
+
{
|
|
172
|
+
success: true,
|
|
173
|
+
message: "Removed result '#{params[:sample_name]}' from output #{params[:id]}",
|
|
174
|
+
output_id: params[:id],
|
|
175
|
+
output_deleted: outcome[:output_deleted],
|
|
176
|
+
output: outcome[:output_deleted] ? nil : mtt_output_json(output)
|
|
177
|
+
}
|
|
178
|
+
rescue StandardError => e
|
|
179
|
+
error!("Error deleting result: #{e.message}", 500)
|
|
180
|
+
end
|
|
186
181
|
end
|
|
187
182
|
|
|
188
183
|
namespace :create_mtt_request do
|
|
@@ -194,7 +189,7 @@ module Labimotion
|
|
|
194
189
|
# Find element and wellplates
|
|
195
190
|
element = Labimotion::Element.find_by(id: params[:id])
|
|
196
191
|
error!('Element not found', 404) unless element
|
|
197
|
-
|
|
192
|
+
|
|
198
193
|
# Verify user has update permission
|
|
199
194
|
error!('Unauthorized', 403) unless ElementPolicy.new(current_user, element).update?
|
|
200
195
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Labimotion
|
|
4
|
+
# API for wellplates linked to a generic element
|
|
5
|
+
class WellplateAPI < Grape::API
|
|
6
|
+
resource :wellplates do
|
|
7
|
+
namespace :by_generic_element do
|
|
8
|
+
desc 'Get wellplates for a generic element'
|
|
9
|
+
params do
|
|
10
|
+
requires :element_id, type: Integer, desc: 'Generic element id'
|
|
11
|
+
end
|
|
12
|
+
route_param :element_id do
|
|
13
|
+
get do
|
|
14
|
+
element = Labimotion::Element.find_by(id: params[:element_id])
|
|
15
|
+
error!('404 Not Found', 404) unless element
|
|
16
|
+
error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, element).read?
|
|
17
|
+
|
|
18
|
+
wellplate_ids = Labimotion::ElementsWellplate.where(element_id: element.id).pluck(:wellplate_id)
|
|
19
|
+
wellplates = Wellplate.where(id: wellplate_ids)
|
|
20
|
+
.includes(:wells, wells: :sample)
|
|
21
|
+
|
|
22
|
+
serialized_wellplates = wellplates.map do |wellplate|
|
|
23
|
+
detail_levels = ElementDetailLevelCalculator.new(user: current_user, element: wellplate).detail_levels
|
|
24
|
+
Entities::WellplateEntity.represent(wellplate, detail_levels: detail_levels)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
{ wellplates: serialized_wellplates }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
desc 'Update wellplates for a generic element'
|
|
31
|
+
params do
|
|
32
|
+
requires :wellplate_ids, type: Array, desc: 'Wellplate IDs'
|
|
33
|
+
end
|
|
34
|
+
put do
|
|
35
|
+
element = Labimotion::Element.find_by(id: params[:element_id])
|
|
36
|
+
error!('404 Not Found', 404) unless element
|
|
37
|
+
error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, element).update?
|
|
38
|
+
|
|
39
|
+
ActiveRecord::Base.transaction do
|
|
40
|
+
current_ids = Labimotion::ElementsWellplate.where(element_id: element.id).pluck(:wellplate_id)
|
|
41
|
+
new_ids = params[:wellplate_ids] || []
|
|
42
|
+
|
|
43
|
+
ids_to_remove = current_ids - new_ids
|
|
44
|
+
if ids_to_remove.any?
|
|
45
|
+
Labimotion::ElementsWellplate.where(element_id: element.id, wellplate_id: ids_to_remove).destroy_all
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
ids_to_add = new_ids - current_ids
|
|
49
|
+
ids_to_add.each do |wellplate_id|
|
|
50
|
+
Labimotion::ElementsWellplate.create!(element_id: element.id, wellplate_id: wellplate_id)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
{ wellplate_ids: Labimotion::ElementsWellplate.where(element_id: element.id).pluck(:wellplate_id) }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -215,7 +215,7 @@ module Labimotion
|
|
|
215
215
|
layer, field = params[:sort_column].split('.')
|
|
216
216
|
|
|
217
217
|
element_klass = Labimotion::ElementKlass.find_by(name: params[:el_type])
|
|
218
|
-
allowed_fields = element_klass
|
|
218
|
+
allowed_fields = element_klass&.properties_release&.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
|
|
219
219
|
|
|
220
220
|
if field.in?(allowed_fields)
|
|
221
221
|
query = ActiveRecord::Base.sanitize_sql(
|
|
@@ -247,27 +247,21 @@ module Labimotion
|
|
|
247
247
|
end
|
|
248
248
|
|
|
249
249
|
def list_serialized_elements(params, current_user)
|
|
250
|
-
|
|
251
|
-
if params[:collection_id]
|
|
252
|
-
Collection
|
|
253
|
-
.belongs_to_or_shared_by(current_user.id, current_user.group_ids)
|
|
254
|
-
.find_by(id: params[:collection_id])&.id
|
|
255
|
-
elsif params[:sync_collection_id]
|
|
256
|
-
current_user
|
|
257
|
-
.all_sync_in_collections_users
|
|
258
|
-
.find_by(id: params[:sync_collection_id])&.collection&.id
|
|
259
|
-
end
|
|
250
|
+
scope = Labimotion::Element.none
|
|
260
251
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
.
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
252
|
+
if params[:collection_id]
|
|
253
|
+
begin
|
|
254
|
+
collection = Collection.accessible_for(current_user).find(params[:collection_id])
|
|
255
|
+
scope = collection.elements
|
|
256
|
+
.joins(:element_klass)
|
|
257
|
+
.where(element_klasses: { name: params[:el_type] })
|
|
258
|
+
.includes(:tag)
|
|
259
|
+
rescue ActiveRecord::RecordNotFound
|
|
260
|
+
Labimotion::Element.none
|
|
261
|
+
end
|
|
269
262
|
else
|
|
270
|
-
|
|
263
|
+
# All collection of current_user
|
|
264
|
+
scope = Labimotion::Element.for_user(current_user.id)
|
|
271
265
|
end
|
|
272
266
|
|
|
273
267
|
## TO DO: refactor labimotion
|
|
@@ -278,7 +272,7 @@ module Labimotion
|
|
|
278
272
|
layer, field = params[:sort_column].split('.')
|
|
279
273
|
|
|
280
274
|
element_klass = Labimotion::ElementKlass.find_by(name: params[:el_type])
|
|
281
|
-
allowed_fields = element_klass
|
|
275
|
+
allowed_fields = element_klass&.properties_release&.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
|
|
282
276
|
|
|
283
277
|
if field.in?(allowed_fields)
|
|
284
278
|
query = ActiveRecord::Base.sanitize_sql(
|
|
@@ -25,6 +25,98 @@ module Labimotion
|
|
|
25
25
|
ENV['MTT_EXTERNAL_APP_URL'] || 'http://localhost:4050'
|
|
26
26
|
end
|
|
27
27
|
|
|
28
|
+
# --- Serialization helpers (shared across the MTT request endpoints) ---
|
|
29
|
+
|
|
30
|
+
def mtt_state_name(state)
|
|
31
|
+
case state
|
|
32
|
+
when Labimotion::DoseRespRequest::STATE_ERROR then 'error'
|
|
33
|
+
when Labimotion::DoseRespRequest::STATE_INITIAL then 'initial'
|
|
34
|
+
when Labimotion::DoseRespRequest::STATE_PROCESSING then 'processing'
|
|
35
|
+
when Labimotion::DoseRespRequest::STATE_COMPLETED then 'completed'
|
|
36
|
+
else 'unknown'
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def mtt_output_json(output)
|
|
41
|
+
{
|
|
42
|
+
id: output.id,
|
|
43
|
+
output_data: output.output_data,
|
|
44
|
+
notes: output.notes,
|
|
45
|
+
created_at: output.created_at
|
|
46
|
+
}
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def mtt_request_json(req, include_outputs: false)
|
|
50
|
+
json = {
|
|
51
|
+
id: req.id,
|
|
52
|
+
request_id: req.request_id,
|
|
53
|
+
element_id: req.element_id,
|
|
54
|
+
state: req.state,
|
|
55
|
+
state_name: mtt_state_name(req.state),
|
|
56
|
+
created_at: req.created_at,
|
|
57
|
+
expires_at: req.expires_at,
|
|
58
|
+
expired: req.expired?,
|
|
59
|
+
revoked: req.revoked?,
|
|
60
|
+
active: req.active?,
|
|
61
|
+
resp_message: req.resp_message,
|
|
62
|
+
last_accessed_at: req.last_accessed_at,
|
|
63
|
+
access_count: req.access_count || 0
|
|
64
|
+
}
|
|
65
|
+
json[:outputs] = req.dose_resp_outputs.map { |output| mtt_output_json(output) } if include_outputs
|
|
66
|
+
json
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# The sample name of a result node, i.e. result[0].name. Used to match a
|
|
70
|
+
# single result row across both output_data shapes (see below). JSONB columns
|
|
71
|
+
# deserialize with string keys; symbol keys are tolerated defensively.
|
|
72
|
+
def mtt_result_name(node)
|
|
73
|
+
return nil unless node.is_a?(Hash)
|
|
74
|
+
|
|
75
|
+
result = node['result'] || node[:result]
|
|
76
|
+
return nil unless result.is_a?(Array) && result.first.is_a?(Hash)
|
|
77
|
+
|
|
78
|
+
result.first['name'] || result.first[:name]
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Remove a single result row (matched by sample name) from an output's
|
|
82
|
+
# output_data JSON, supporting both the new (Output[].items[]) and the legacy
|
|
83
|
+
# (Output[].result[]) shapes. If the output has no results left afterwards the
|
|
84
|
+
# record is soft-deleted (acts_as_paranoid), consistent with the bulk delete.
|
|
85
|
+
#
|
|
86
|
+
# Returns { removed:, output_deleted: }.
|
|
87
|
+
def remove_mtt_result_by_sample_name(output, sample_name)
|
|
88
|
+
data = output.output_data || {}
|
|
89
|
+
groups = data['Output'] || data[:Output]
|
|
90
|
+
return { removed: false, output_deleted: false } unless groups.is_a?(Array)
|
|
91
|
+
|
|
92
|
+
removed = false
|
|
93
|
+
new_groups = groups.map do |group|
|
|
94
|
+
items = group['items'] || group[:items]
|
|
95
|
+
if items.is_a?(Array)
|
|
96
|
+
# New structure: drop the matching item(s) from the group.
|
|
97
|
+
kept = items.reject { |item| mtt_result_name(item) == sample_name }
|
|
98
|
+
removed ||= kept.length != items.length
|
|
99
|
+
kept.empty? ? nil : group.merge('items' => kept)
|
|
100
|
+
elsif mtt_result_name(group) == sample_name
|
|
101
|
+
# Legacy structure: drop the whole group.
|
|
102
|
+
removed = true
|
|
103
|
+
nil
|
|
104
|
+
else
|
|
105
|
+
group
|
|
106
|
+
end
|
|
107
|
+
end.compact
|
|
108
|
+
|
|
109
|
+
return { removed: false, output_deleted: false } unless removed
|
|
110
|
+
|
|
111
|
+
if new_groups.empty?
|
|
112
|
+
output.destroy
|
|
113
|
+
{ removed: true, output_deleted: true }
|
|
114
|
+
else
|
|
115
|
+
output.update!(output_data: data.merge('Output' => new_groups))
|
|
116
|
+
{ removed: true, output_deleted: false }
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
28
120
|
def validate_token(token)
|
|
29
121
|
# Find the request by access token
|
|
30
122
|
request = Labimotion::DoseRespRequest.find_by(access_token: token)
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
"file": "acqus",
|
|
10
10
|
"parameters": [
|
|
11
11
|
"DATE",
|
|
12
|
-
"D1",
|
|
13
12
|
"INSTRUM",
|
|
14
13
|
"NS",
|
|
15
14
|
"NUC1",
|
|
@@ -23,7 +22,10 @@
|
|
|
23
22
|
"TD",
|
|
24
23
|
"TE",
|
|
25
24
|
"TITLE"
|
|
26
|
-
]
|
|
25
|
+
],
|
|
26
|
+
"arrayParameters": {
|
|
27
|
+
"D1": { "name": "D", "index": 1 }
|
|
28
|
+
}
|
|
27
29
|
},
|
|
28
30
|
"procs": {
|
|
29
31
|
"file": "pdata/1/procs",
|
|
@@ -26,9 +26,9 @@ module Labimotion
|
|
|
26
26
|
joins(collections: :user).where(collections: { user_id: user_id })
|
|
27
27
|
)
|
|
28
28
|
|
|
29
|
-
# Shared
|
|
29
|
+
# Shared records
|
|
30
30
|
shared = apply_filters.call(
|
|
31
|
-
|
|
31
|
+
left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
# Combine (remove duplicates), order, and limit
|
|
@@ -141,9 +141,9 @@ module Labimotion
|
|
|
141
141
|
joins(collections: :user).where(collections: { user_id: user_id })
|
|
142
142
|
)
|
|
143
143
|
|
|
144
|
-
# Shared
|
|
144
|
+
# Shared elements
|
|
145
145
|
shared = apply_filters.call(
|
|
146
|
-
|
|
146
|
+
left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
|
|
147
147
|
)
|
|
148
148
|
|
|
149
149
|
# Combine (remove duplicates), order, and limit
|
|
@@ -70,6 +70,37 @@ module Labimotion
|
|
|
70
70
|
extracted_parameters
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
# Extracts scalar values from Bruker array parameters such as
|
|
74
|
+
# ##$D= (0..63)
|
|
75
|
+
# 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0002 ...
|
|
76
|
+
# where the values live on the line(s) following the header. The config maps
|
|
77
|
+
# each output key to an array name + index, e.g.
|
|
78
|
+
# { "D1" => { "name" => "D", "index" => 1 } } => D1 = the 2nd value (here 1).
|
|
79
|
+
def extract_array_parameters(file_content, array_parameters)
|
|
80
|
+
return {} if file_content.blank? || array_parameters.blank?
|
|
81
|
+
|
|
82
|
+
lines = file_content.lines
|
|
83
|
+
extracted = {}
|
|
84
|
+
lines.each_with_index do |line, idx|
|
|
85
|
+
header = line.match(/^\s*##\$(?<name>[A-Za-z0-9_]+)\s*=\s*\(\s*\d+\s*\.\.\s*\d+\s*\)/)
|
|
86
|
+
next unless header
|
|
87
|
+
|
|
88
|
+
targets = array_parameters.select { |_key, cfg| cfg['name'] == header[:name] }
|
|
89
|
+
next if targets.empty?
|
|
90
|
+
|
|
91
|
+
values = collect_array_values(lines, idx + 1)
|
|
92
|
+
targets.each do |out_key, cfg|
|
|
93
|
+
value = values[cfg['index'].to_i]
|
|
94
|
+
extracted[out_key] = clean_value(value) if value.present?
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
extracted.compact_blank!
|
|
98
|
+
extracted
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
Rails.logger.error "Error extracting array parameters: #{e.message}"
|
|
101
|
+
{}
|
|
102
|
+
end
|
|
103
|
+
|
|
73
104
|
def format_timestamp(timestamp_str, give_format = nil)
|
|
74
105
|
return nil if timestamp_str.blank?
|
|
75
106
|
|
|
@@ -118,6 +149,21 @@ module Labimotion
|
|
|
118
149
|
value
|
|
119
150
|
end
|
|
120
151
|
|
|
152
|
+
# Collects whitespace-separated values from the line(s) following an array
|
|
153
|
+
# header, stopping at the next directive (line starting with '##') or a blank line.
|
|
154
|
+
def collect_array_values(lines, start_index)
|
|
155
|
+
values = []
|
|
156
|
+
index = start_index
|
|
157
|
+
while index < lines.length
|
|
158
|
+
stripped = lines[index].strip
|
|
159
|
+
break if stripped.empty? || stripped.start_with?('##')
|
|
160
|
+
|
|
161
|
+
values.concat(stripped.split(/\s+/))
|
|
162
|
+
index += 1
|
|
163
|
+
end
|
|
164
|
+
values
|
|
165
|
+
end
|
|
166
|
+
|
|
121
167
|
def process_zip_file(zip_file_url, source_map)
|
|
122
168
|
final_parameters = {}
|
|
123
169
|
|
|
@@ -137,23 +183,28 @@ module Labimotion
|
|
|
137
183
|
|
|
138
184
|
zip_file.each do |entry|
|
|
139
185
|
if source_file?(entry, source_config)
|
|
140
|
-
process_file_entry(entry, source_config
|
|
186
|
+
process_file_entry(entry, source_config, final_parameters)
|
|
141
187
|
elsif bagit_metadata_file?(entry)
|
|
142
188
|
return { is_bagit: true, metadata: nil }
|
|
143
189
|
end
|
|
144
190
|
end
|
|
145
191
|
end
|
|
146
192
|
|
|
147
|
-
def process_file_entry(entry,
|
|
193
|
+
def process_file_entry(entry, source_config, final_parameters)
|
|
148
194
|
file_content = entry.get_input_stream.read.force_encoding(Constants::File::ENCODING)
|
|
149
|
-
extracted_parameters = extract_parameters(file_content, parameters)
|
|
195
|
+
extracted_parameters = extract_parameters(file_content, source_config['parameters'])
|
|
150
196
|
final_parameters.merge!(extracted_parameters) if extracted_parameters.present?
|
|
197
|
+
|
|
198
|
+
# Array parameters (e.g. D1 from acqus ##$D) are a fallback: they only fill a
|
|
199
|
+
# value that an earlier/higher-priority source (e.g. parm.txt D1) did not provide.
|
|
200
|
+
array_parameters = extract_array_parameters(file_content, source_config['arrayParameters'])
|
|
201
|
+
final_parameters.reverse_merge!(array_parameters) if array_parameters.present?
|
|
151
202
|
end
|
|
152
203
|
|
|
153
204
|
def invalid_source_config?(source_config)
|
|
154
205
|
source_config.nil? ||
|
|
155
206
|
source_config['file'].nil? ||
|
|
156
|
-
source_config['parameters'].nil?
|
|
207
|
+
(source_config['parameters'].nil? && source_config['arrayParameters'].nil?)
|
|
157
208
|
end
|
|
158
209
|
|
|
159
210
|
def source_file?(entry, source_config)
|
|
@@ -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.accessible_for(current_user).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.accessible_for(current_user).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
|
@@ -29,10 +29,10 @@ module Labimotion
|
|
|
29
29
|
autoload :VocabularyAPI, 'labimotion/apis/vocabulary_api'
|
|
30
30
|
autoload :UserAPI, 'labimotion/apis/user_api'
|
|
31
31
|
autoload :MttAPI, 'labimotion/apis/mtt_api'
|
|
32
|
-
autoload :DoseRespRequestAPI, 'labimotion/apis/dose_resp_request_api'
|
|
33
32
|
autoload :ElementVariationAPI, 'labimotion/apis/element_variation_api'
|
|
34
33
|
autoload :LabimotionDoiAPI, 'labimotion/apis/labimotion_doi_api'
|
|
35
34
|
autoload :LabimotionTemplateBrowseAPI, 'labimotion/apis/labimotion_template_browse_api'
|
|
35
|
+
autoload :WellplateAPI, 'labimotion/apis/wellplate_api'
|
|
36
36
|
|
|
37
37
|
######## Entities
|
|
38
38
|
autoload :PropertiesEntity, 'labimotion/entities/properties_entity'
|
|
@@ -124,6 +124,7 @@ module Labimotion
|
|
|
124
124
|
|
|
125
125
|
autoload :ElementsSample, 'labimotion/models/elements_sample'
|
|
126
126
|
autoload :ElementsElement, 'labimotion/models/elements_element'
|
|
127
|
+
autoload :ElementsWellplate, 'labimotion/models/elements_wellplate'
|
|
127
128
|
autoload :CollectionsElement, 'labimotion/models/collections_element'
|
|
128
129
|
|
|
129
130
|
autoload :StdLayer, 'labimotion/models/std_layer'
|
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.rc18
|
|
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-07-01 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: caxlsx
|
|
@@ -58,7 +58,6 @@ files:
|
|
|
58
58
|
- README.md
|
|
59
59
|
- lib/labimotion.rb
|
|
60
60
|
- lib/labimotion/apis/converter_api.rb
|
|
61
|
-
- lib/labimotion/apis/dose_resp_request_api.rb
|
|
62
61
|
- lib/labimotion/apis/element_variation_api.rb
|
|
63
62
|
- lib/labimotion/apis/exporter_api.rb
|
|
64
63
|
- lib/labimotion/apis/generic_dataset_api.rb
|
|
@@ -74,6 +73,7 @@ files:
|
|
|
74
73
|
- lib/labimotion/apis/standard_layer_api.rb
|
|
75
74
|
- lib/labimotion/apis/user_api.rb
|
|
76
75
|
- lib/labimotion/apis/vocabulary_api.rb
|
|
76
|
+
- lib/labimotion/apis/wellplate_api.rb
|
|
77
77
|
- lib/labimotion/collection/export.rb
|
|
78
78
|
- lib/labimotion/collection/import.rb
|
|
79
79
|
- lib/labimotion/conf.rb
|
|
@@ -151,6 +151,7 @@ files:
|
|
|
151
151
|
- lib/labimotion/models/elements_element.rb
|
|
152
152
|
- lib/labimotion/models/elements_revision.rb
|
|
153
153
|
- lib/labimotion/models/elements_sample.rb
|
|
154
|
+
- lib/labimotion/models/elements_wellplate.rb
|
|
154
155
|
- lib/labimotion/models/hub_log.rb
|
|
155
156
|
- lib/labimotion/models/reaction.rb
|
|
156
157
|
- lib/labimotion/models/research_plan.rb
|
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'labimotion/version'
|
|
4
|
-
|
|
5
|
-
module Labimotion
|
|
6
|
-
# Dose Response Request API
|
|
7
|
-
class DoseRespRequestAPI < Grape::API
|
|
8
|
-
helpers Labimotion::ParamHelpers
|
|
9
|
-
|
|
10
|
-
resource :dose_resp_requests do
|
|
11
|
-
desc 'Get all dose response requests for current user'
|
|
12
|
-
params do
|
|
13
|
-
optional :element_id, type: Integer, desc: 'Filter by element ID'
|
|
14
|
-
optional :state, type: Integer, desc: 'Filter by state (-1, 0, 1, 2)'
|
|
15
|
-
optional :page, type: Integer, desc: 'Page number', default: 1
|
|
16
|
-
optional :per_page, type: Integer, desc: 'Items per page', default: 20
|
|
17
|
-
end
|
|
18
|
-
get do
|
|
19
|
-
requests = Labimotion::DoseRespRequest.where(created_by: current_user.id)
|
|
20
|
-
|
|
21
|
-
# Apply filters
|
|
22
|
-
requests = requests.where(element_id: params[:element_id]) if params[:element_id]
|
|
23
|
-
requests = requests.where(state: params[:state]) if params[:state]
|
|
24
|
-
|
|
25
|
-
# Pagination
|
|
26
|
-
page = params[:page] || 1
|
|
27
|
-
per_page = [params[:per_page] || 20, 100].min # Max 100 per page
|
|
28
|
-
|
|
29
|
-
total = requests.count
|
|
30
|
-
requests = requests.order(created_at: :desc)
|
|
31
|
-
.offset((page - 1) * per_page)
|
|
32
|
-
.limit(per_page)
|
|
33
|
-
|
|
34
|
-
{
|
|
35
|
-
requests: requests.map do |req|
|
|
36
|
-
{
|
|
37
|
-
id: req.id,
|
|
38
|
-
request_id: req.request_id,
|
|
39
|
-
element_id: req.element_id,
|
|
40
|
-
state: req.state,
|
|
41
|
-
state_label: state_label(req.state),
|
|
42
|
-
expires_at: req.expires_at,
|
|
43
|
-
created_at: req.created_at,
|
|
44
|
-
updated_at: req.updated_at,
|
|
45
|
-
first_accessed_at: req.first_accessed_at,
|
|
46
|
-
last_accessed_at: req.last_accessed_at,
|
|
47
|
-
access_count: req.access_count,
|
|
48
|
-
resp_message: req.resp_message,
|
|
49
|
-
active: req.active?
|
|
50
|
-
}
|
|
51
|
-
end,
|
|
52
|
-
pagination: {
|
|
53
|
-
page: page,
|
|
54
|
-
per_page: per_page,
|
|
55
|
-
total: total,
|
|
56
|
-
total_pages: (total.to_f / per_page).ceil
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
rescue StandardError => e
|
|
60
|
-
error!("Error: #{e.message}", 500)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
desc 'Get a dose response request by ID'
|
|
64
|
-
params do
|
|
65
|
-
requires :id, type: Integer, desc: 'Request ID'
|
|
66
|
-
end
|
|
67
|
-
get ':id' do
|
|
68
|
-
request = Labimotion::DoseRespRequest.find_by(id: params[:id])
|
|
69
|
-
error!('Request not found', 404) unless request
|
|
70
|
-
|
|
71
|
-
# Check authorization
|
|
72
|
-
error!('Unauthorized', 403) unless request.created_by == current_user.id
|
|
73
|
-
|
|
74
|
-
{
|
|
75
|
-
id: request.id,
|
|
76
|
-
request_id: request.request_id,
|
|
77
|
-
element_id: request.element_id,
|
|
78
|
-
state: request.state,
|
|
79
|
-
state_label: state_label(request.state),
|
|
80
|
-
wellplates_metadata: request.wellplates_metadata,
|
|
81
|
-
input_metadata: request.input_metadata,
|
|
82
|
-
expires_at: request.expires_at,
|
|
83
|
-
revoked_at: request.revoked_at,
|
|
84
|
-
created_at: request.created_at,
|
|
85
|
-
updated_at: request.updated_at,
|
|
86
|
-
first_accessed_at: request.first_accessed_at,
|
|
87
|
-
last_accessed_at: request.last_accessed_at,
|
|
88
|
-
access_count: request.access_count,
|
|
89
|
-
resp_message: request.resp_message,
|
|
90
|
-
active: request.active?,
|
|
91
|
-
expired: request.expired?,
|
|
92
|
-
revoked: request.revoked?
|
|
93
|
-
}
|
|
94
|
-
rescue StandardError => e
|
|
95
|
-
error!("Error: #{e.message}", 500)
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
desc 'Update a dose response request'
|
|
99
|
-
params do
|
|
100
|
-
requires :id, type: Integer, desc: 'Request ID'
|
|
101
|
-
optional :state, type: Integer, desc: 'State', values: [-1, 0, 1, 2]
|
|
102
|
-
optional :resp_message, type: String, desc: 'Response message'
|
|
103
|
-
optional :wellplates_metadata, type: Hash, desc: 'Wellplates metadata'
|
|
104
|
-
end
|
|
105
|
-
put ':id' do
|
|
106
|
-
request = Labimotion::DoseRespRequest.find_by(id: params[:id])
|
|
107
|
-
error!('Request not found', 404) unless request
|
|
108
|
-
|
|
109
|
-
# Check authorization
|
|
110
|
-
error!('Unauthorized', 403) unless request.created_by == current_user.id
|
|
111
|
-
|
|
112
|
-
update_params = {}
|
|
113
|
-
update_params[:state] = params[:state] if params[:state]
|
|
114
|
-
update_params[:resp_message] = params[:resp_message] if params[:resp_message]
|
|
115
|
-
update_params[:wellplates_metadata] = params[:wellplates_metadata] if params[:wellplates_metadata]
|
|
116
|
-
|
|
117
|
-
request.update!(update_params)
|
|
118
|
-
|
|
119
|
-
{
|
|
120
|
-
success: true,
|
|
121
|
-
message: 'Request updated successfully',
|
|
122
|
-
request: {
|
|
123
|
-
id: request.id,
|
|
124
|
-
request_id: request.request_id,
|
|
125
|
-
state: request.state,
|
|
126
|
-
state_label: state_label(request.state),
|
|
127
|
-
resp_message: request.resp_message,
|
|
128
|
-
updated_at: request.updated_at
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
rescue ActiveRecord::RecordInvalid => e
|
|
132
|
-
error!("Validation error: #{e.message}", 422)
|
|
133
|
-
rescue StandardError => e
|
|
134
|
-
error!("Error: #{e.message}", 500)
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
desc 'Revoke a dose response request'
|
|
138
|
-
params do
|
|
139
|
-
requires :id, type: Integer, desc: 'Request ID'
|
|
140
|
-
end
|
|
141
|
-
post ':id/revoke' do
|
|
142
|
-
request = Labimotion::DoseRespRequest.find_by(id: params[:id])
|
|
143
|
-
error!('Request not found', 404) unless request
|
|
144
|
-
|
|
145
|
-
# Check authorization
|
|
146
|
-
error!('Unauthorized', 403) unless request.created_by == current_user.id
|
|
147
|
-
|
|
148
|
-
request.revoke!
|
|
149
|
-
|
|
150
|
-
{
|
|
151
|
-
success: true,
|
|
152
|
-
message: 'Request revoked successfully',
|
|
153
|
-
request: {
|
|
154
|
-
id: request.id,
|
|
155
|
-
request_id: request.request_id,
|
|
156
|
-
state: request.state,
|
|
157
|
-
revoked_at: request.revoked_at,
|
|
158
|
-
active: request.active?
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
rescue StandardError => e
|
|
162
|
-
error!("Error: #{e.message}", 500)
|
|
163
|
-
end
|
|
164
|
-
|
|
165
|
-
desc 'Delete a dose response request'
|
|
166
|
-
params do
|
|
167
|
-
requires :id, type: Integer, desc: 'Request ID'
|
|
168
|
-
end
|
|
169
|
-
delete ':id' do
|
|
170
|
-
request = Labimotion::DoseRespRequest.find_by(id: params[:id])
|
|
171
|
-
error!('Request not found', 404) unless request
|
|
172
|
-
|
|
173
|
-
# Check authorization
|
|
174
|
-
error!('Unauthorized', 403) unless request.created_by == current_user.id
|
|
175
|
-
|
|
176
|
-
# Soft delete if acts_as_paranoid is enabled
|
|
177
|
-
request.destroy
|
|
178
|
-
|
|
179
|
-
{
|
|
180
|
-
success: true,
|
|
181
|
-
message: 'Request deleted successfully'
|
|
182
|
-
}
|
|
183
|
-
rescue StandardError => e
|
|
184
|
-
error!("Error: #{e.message}", 500)
|
|
185
|
-
end
|
|
186
|
-
|
|
187
|
-
desc 'Get dose response requests by element ID'
|
|
188
|
-
params do
|
|
189
|
-
requires :element_id, type: Integer, desc: 'Element ID'
|
|
190
|
-
end
|
|
191
|
-
get 'by_element/:element_id' do
|
|
192
|
-
element = Labimotion::Element.find_by(id: params[:element_id])
|
|
193
|
-
error!('Element not found', 404) unless element
|
|
194
|
-
|
|
195
|
-
# Check if user has access to element
|
|
196
|
-
policy = ElementPolicy.new(current_user, element)
|
|
197
|
-
error!('Unauthorized', 403) unless policy.read?
|
|
198
|
-
|
|
199
|
-
requests = Labimotion::DoseRespRequest.where(element_id: params[:element_id])
|
|
200
|
-
.where(created_by: current_user.id)
|
|
201
|
-
.order(created_at: :desc)
|
|
202
|
-
|
|
203
|
-
{
|
|
204
|
-
element_id: element.id,
|
|
205
|
-
element_name: element.name,
|
|
206
|
-
requests: requests.map do |req|
|
|
207
|
-
{
|
|
208
|
-
id: req.id,
|
|
209
|
-
request_id: req.request_id,
|
|
210
|
-
state: req.state,
|
|
211
|
-
state_label: state_label(req.state),
|
|
212
|
-
expires_at: req.expires_at,
|
|
213
|
-
created_at: req.created_at,
|
|
214
|
-
access_count: req.access_count,
|
|
215
|
-
active: req.active?
|
|
216
|
-
}
|
|
217
|
-
end
|
|
218
|
-
}
|
|
219
|
-
rescue StandardError => e
|
|
220
|
-
error!("Error: #{e.message}", 500)
|
|
221
|
-
end
|
|
222
|
-
end
|
|
223
|
-
|
|
224
|
-
helpers do
|
|
225
|
-
def state_label(state)
|
|
226
|
-
case state
|
|
227
|
-
when Labimotion::DoseRespRequest::STATE_ERROR
|
|
228
|
-
'error'
|
|
229
|
-
when Labimotion::DoseRespRequest::STATE_INITIAL
|
|
230
|
-
'initial'
|
|
231
|
-
when Labimotion::DoseRespRequest::STATE_PROCESSING
|
|
232
|
-
'processing'
|
|
233
|
-
when Labimotion::DoseRespRequest::STATE_COMPLETED
|
|
234
|
-
'completed'
|
|
235
|
-
else
|
|
236
|
-
'unknown'
|
|
237
|
-
end
|
|
238
|
-
end
|
|
239
|
-
end
|
|
240
|
-
end
|
|
241
|
-
end
|