labimotion 2.2.0.rc1 → 2.2.0.rc2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/labimotion/apis/dose_resp_request_api.rb +241 -0
- data/lib/labimotion/apis/generic_element_api.rb +2 -2
- data/lib/labimotion/apis/labimotion_api.rb +2 -0
- data/lib/labimotion/apis/mtt_api.rb +125 -0
- data/lib/labimotion/collection/import.rb +3 -3
- data/lib/labimotion/helpers/element_helpers.rb +5 -2
- data/lib/labimotion/helpers/mtt_helpers.rb +403 -0
- data/lib/labimotion/helpers/param_helpers.rb +10 -0
- data/lib/labimotion/libs/converter.rb +2 -1
- data/lib/labimotion/libs/dataset_builder.rb +2 -1
- data/lib/labimotion/models/concerns/datasetable.rb +3 -2
- data/lib/labimotion/models/concerns/generic_klass_revisions.rb +2 -3
- data/lib/labimotion/models/dose_resp_output.rb +27 -0
- data/lib/labimotion/models/dose_resp_request.rb +93 -0
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +5 -1
- metadata +7 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 201e0597dc0d1a272ec2a74ffd387add0788c96cf31605c32ab1105c8c88fe40
|
|
4
|
+
data.tar.gz: 0bb95660dda6cb62c0449b80c0f73b1cae37e21490c671519dd53065c74b968e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3cab6e2a1d8405d9dc53e7fe352c1bcd28ac7c2af742a1deec806e15895440d502921e15426d85f181835a241fb84e3c17605637046aae428d19b5e6e65b5905
|
|
7
|
+
data.tar.gz: 66aad0cd9770ff0db4b6d17b6224b997ed67726517416f100c24edeb5bbe989d79ca0b54028909529e9eca24dc88ce81a967f452c7198dbaa6cc725de1d57e1c
|
|
@@ -0,0 +1,241 @@
|
|
|
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
|
|
@@ -474,7 +474,7 @@ module Labimotion
|
|
|
474
474
|
detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels,
|
|
475
475
|
policy: @element_policy
|
|
476
476
|
),
|
|
477
|
-
attachments:
|
|
477
|
+
attachments: Entities::AttachmentEntity.represent(element&.attachments)
|
|
478
478
|
}
|
|
479
479
|
rescue StandardError => e
|
|
480
480
|
Labimotion.log_exception(e, current_user)
|
|
@@ -517,7 +517,7 @@ module Labimotion
|
|
|
517
517
|
element,
|
|
518
518
|
detail_levels: ElementDetailLevelCalculator.new(user: current_user, element: element).detail_levels,
|
|
519
519
|
),
|
|
520
|
-
attachments:
|
|
520
|
+
attachments: Entities::AttachmentEntity.represent(element&.attachments),
|
|
521
521
|
}
|
|
522
522
|
rescue StandardError => e
|
|
523
523
|
Labimotion.log_exception(e, current_user)
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'labimotion/version'
|
|
4
|
+
require 'labimotion/libs/export_element'
|
|
5
|
+
require 'labimotion/helpers/mtt_helpers'
|
|
6
|
+
require 'labimotion/models/dose_resp_output'
|
|
7
|
+
|
|
8
|
+
module Labimotion
|
|
9
|
+
# Generic Element API
|
|
10
|
+
class MttAPI < Grape::API
|
|
11
|
+
helpers Labimotion::ParamHelpers
|
|
12
|
+
helpers Labimotion::MttHelpers
|
|
13
|
+
|
|
14
|
+
namespace :public do
|
|
15
|
+
resource :mtt_apps do
|
|
16
|
+
route_param :token do
|
|
17
|
+
desc 'Download data from MTT app (GET endpoint)'
|
|
18
|
+
get do
|
|
19
|
+
download_json_to_external_app
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
desc 'Upload modified data from MTT app (POST endpoint)'
|
|
23
|
+
post do
|
|
24
|
+
upload_json_from_external_app
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
resource :mtt do
|
|
31
|
+
namespace :requests do
|
|
32
|
+
desc 'Get MTT requests for current user'
|
|
33
|
+
get do
|
|
34
|
+
# Get all requests created by current user
|
|
35
|
+
requests = Labimotion::DoseRespRequest
|
|
36
|
+
.includes(:dose_resp_outputs)
|
|
37
|
+
.where(created_by: current_user.id)
|
|
38
|
+
.order(created_at: :desc)
|
|
39
|
+
|
|
40
|
+
# Return formatted response
|
|
41
|
+
requests.map do |req|
|
|
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
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
namespace :create_mtt_request do
|
|
76
|
+
desc 'Create MTT assay request'
|
|
77
|
+
params do
|
|
78
|
+
use :create_mtt_request_params
|
|
79
|
+
end
|
|
80
|
+
post do
|
|
81
|
+
# Find element and wellplates
|
|
82
|
+
element = Labimotion::Element.find_by(id: params[:id])
|
|
83
|
+
error!('Element not found', 404) unless element
|
|
84
|
+
#byebug
|
|
85
|
+
# Verify user has update permission
|
|
86
|
+
error!('Unauthorized', 403) unless ElementPolicy.new(current_user, element).update?
|
|
87
|
+
|
|
88
|
+
wellplates = Wellplate.where(id: params[:wellplate_ids])
|
|
89
|
+
error!('No wellplates found', 404) if wellplates.empty?
|
|
90
|
+
|
|
91
|
+
# Generate wellplates metadata
|
|
92
|
+
wellplates_metadata = generate_wellplates_metadata(wellplates)
|
|
93
|
+
|
|
94
|
+
# Create DoseRespRequest record with token
|
|
95
|
+
dose_resp_request = Labimotion::DoseRespRequest.create!(
|
|
96
|
+
element_id: element.id,
|
|
97
|
+
wellplates_metadata: { wellplates: wellplates_metadata },
|
|
98
|
+
input_metadata: {
|
|
99
|
+
wellplate_ids: params[:wellplate_ids],
|
|
100
|
+
element_id: element.id,
|
|
101
|
+
element_name: element.name,
|
|
102
|
+
created_at: Time.current
|
|
103
|
+
},
|
|
104
|
+
state: Labimotion::DoseRespRequest::STATE_INITIAL,
|
|
105
|
+
created_by: current_user&.id,
|
|
106
|
+
expires_at: TPA_EXPIRATION.from_now
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Generate external app URL with token
|
|
110
|
+
token_uri = token_url(dose_resp_request)
|
|
111
|
+
external_app_url = get_external_app_url
|
|
112
|
+
|
|
113
|
+
# Format: "#{@app.url}?url=#{CGI.escape(token_uri)}&type=ThirdPartyApp"
|
|
114
|
+
"#{external_app_url}?method=DoseResponse&url=#{CGI.escape(token_uri)}"
|
|
115
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
116
|
+
error!("Validation error: #{e.message}", 422)
|
|
117
|
+
rescue ActiveRecord::RecordNotFound => e
|
|
118
|
+
error!("Not found: #{e.message}", 404)
|
|
119
|
+
rescue StandardError => e
|
|
120
|
+
error!("Error: #{e.message}", 500)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -69,7 +69,7 @@ module Labimotion
|
|
|
69
69
|
|
|
70
70
|
dataset = Labimotion::Dataset.create!(
|
|
71
71
|
fields.slice(
|
|
72
|
-
'properties', 'properties_release'
|
|
72
|
+
'properties', 'properties_release', 'metadata'
|
|
73
73
|
).merge(
|
|
74
74
|
## created_by: current_user_id,
|
|
75
75
|
element: element,
|
|
@@ -112,7 +112,7 @@ module Labimotion
|
|
|
112
112
|
|
|
113
113
|
segment = Labimotion::Segment.create!(
|
|
114
114
|
fields.slice(
|
|
115
|
-
'properties', 'properties_release'
|
|
115
|
+
'properties', 'properties_release', 'metadata'
|
|
116
116
|
).merge(
|
|
117
117
|
created_by: current_user_id,
|
|
118
118
|
element: element,
|
|
@@ -149,7 +149,7 @@ module Labimotion
|
|
|
149
149
|
|
|
150
150
|
element = Labimotion::Element.create!(
|
|
151
151
|
fields.slice(
|
|
152
|
-
'name', 'properties', 'properties_release'
|
|
152
|
+
'name', 'properties', 'properties_release', 'metadata'
|
|
153
153
|
).merge(
|
|
154
154
|
created_by: current_user_id,
|
|
155
155
|
element_klass: element_klass,
|
|
@@ -82,7 +82,8 @@ module Labimotion
|
|
|
82
82
|
klass_uuid: klass[:uuid],
|
|
83
83
|
properties: properties,
|
|
84
84
|
properties_release: params[:properties_release],
|
|
85
|
-
|
|
85
|
+
metadata: params[:metadata] || {},
|
|
86
|
+
created_by: current_user.id
|
|
86
87
|
}
|
|
87
88
|
element = Labimotion::Element.new(attributes)
|
|
88
89
|
|
|
@@ -116,7 +117,8 @@ module Labimotion
|
|
|
116
117
|
params.delete(:user_labels)
|
|
117
118
|
attributes = declared(params.except(:segments), include_missing: false)
|
|
118
119
|
properties['pkg'] = Labimotion::Utils.pkg(properties['pkg'])
|
|
119
|
-
|
|
120
|
+
metadata = params[:metadata] || element.metadata || {}
|
|
121
|
+
if element.klass_uuid != properties['klass_uuid'] || element.properties != properties || element.name != params[:name] || element.metadata != metadata
|
|
120
122
|
properties['klass'] = 'Element'
|
|
121
123
|
uuid = SecureRandom.uuid
|
|
122
124
|
properties['uuid'] = uuid
|
|
@@ -128,6 +130,7 @@ module Labimotion
|
|
|
128
130
|
attributes['properties']['uuid'] = uuid
|
|
129
131
|
attributes['uuid'] = uuid
|
|
130
132
|
attributes['klass_uuid'] = properties['klass_uuid']
|
|
133
|
+
attributes['metadata'] = metadata
|
|
131
134
|
attributes['updated_at'] = Time.current
|
|
132
135
|
element.update_columns(attributes)
|
|
133
136
|
end
|
|
@@ -0,0 +1,403 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'grape'
|
|
4
|
+
require 'net/http'
|
|
5
|
+
require 'uri'
|
|
6
|
+
require 'zip'
|
|
7
|
+
|
|
8
|
+
module Labimotion
|
|
9
|
+
## MTT Helpers
|
|
10
|
+
module MttHelpers
|
|
11
|
+
extend Grape::API::Helpers
|
|
12
|
+
|
|
13
|
+
TPA_EXPIRATION = 72.hours
|
|
14
|
+
|
|
15
|
+
def token_url(dose_resp_request)
|
|
16
|
+
# Build the callback URL with token in path
|
|
17
|
+
api_base_url = ENV['PUBLIC_URL'] || 'http://172.28.156.100:3000'
|
|
18
|
+
callback_path = "/api/v1/public/mtt_apps/#{dose_resp_request.access_token}"
|
|
19
|
+
callback_url = "#{api_base_url}#{callback_path}"
|
|
20
|
+
|
|
21
|
+
callback_url
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def get_external_app_url
|
|
25
|
+
ENV['MTT_EXTERNAL_APP_URL'] || 'http://localhost:4050'
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def validate_token(token)
|
|
29
|
+
# Find the request by access token
|
|
30
|
+
request = Labimotion::DoseRespRequest.find_by(access_token: token)
|
|
31
|
+
error!('Token not found', 404) unless request
|
|
32
|
+
|
|
33
|
+
# Check expiration
|
|
34
|
+
error!('Token expired', 403) if request.expired?
|
|
35
|
+
|
|
36
|
+
# Check revocation
|
|
37
|
+
error!('Token revoked', 403) if request.revoked?
|
|
38
|
+
|
|
39
|
+
request
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def validate_user_access(dose_resp_request)
|
|
43
|
+
# Get element and user
|
|
44
|
+
element = dose_resp_request.element
|
|
45
|
+
error!('Element not found', 404) unless element
|
|
46
|
+
|
|
47
|
+
user = dose_resp_request.creator
|
|
48
|
+
error!('User not found', 404) unless user
|
|
49
|
+
|
|
50
|
+
# Check user has update permission on element using ElementPolicy
|
|
51
|
+
policy = ElementPolicy.new(user, element)
|
|
52
|
+
error!('Unauthorized', 403) unless policy.update?
|
|
53
|
+
|
|
54
|
+
{ element: element, user: user }
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def download_json_to_external_app
|
|
58
|
+
# Get token from route params
|
|
59
|
+
token = params[:token]
|
|
60
|
+
dose_resp_request = validate_token(token)
|
|
61
|
+
|
|
62
|
+
# Validate user access
|
|
63
|
+
validate_user_access(dose_resp_request)
|
|
64
|
+
|
|
65
|
+
# Track access and update state
|
|
66
|
+
# dose_resp_request.track_access!
|
|
67
|
+
dose_resp_request.mark_processing! if dose_resp_request.state == Labimotion::DoseRespRequest::STATE_INITIAL
|
|
68
|
+
# Return wellplates metadata as JSON
|
|
69
|
+
# Access the wellplates array from the metadata structure
|
|
70
|
+
wellplates_data = dose_resp_request.wellplates_metadata&.dig('wellplates') ||
|
|
71
|
+
dose_resp_request.wellplates_metadata&.dig(:wellplates) ||
|
|
72
|
+
[]
|
|
73
|
+
|
|
74
|
+
response_data = {
|
|
75
|
+
id: dose_resp_request.id.to_s,
|
|
76
|
+
request_id: dose_resp_request.request_id,
|
|
77
|
+
wellplates: wellplates_data
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
status 200
|
|
81
|
+
response_data
|
|
82
|
+
rescue => e
|
|
83
|
+
error!("Error: #{e.message}", 500)
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def upload_json_from_external_app
|
|
87
|
+
# Get token from route params
|
|
88
|
+
token = params[:token]
|
|
89
|
+
dose_resp_request = validate_token(token)
|
|
90
|
+
|
|
91
|
+
# Validate user access
|
|
92
|
+
access_info = validate_user_access(dose_resp_request)
|
|
93
|
+
user = access_info[:user]
|
|
94
|
+
element = access_info[:element]
|
|
95
|
+
# Handle file upload
|
|
96
|
+
file_param = params['file'] || params[:file]
|
|
97
|
+
error!('No file uploaded', 400) unless file_param && file_param.is_a?(Hash) && file_param['tempfile']
|
|
98
|
+
|
|
99
|
+
tempfile = file_param['tempfile']
|
|
100
|
+
filename = file_param['filename'] || 'upload'
|
|
101
|
+
# Check if it's a zip file
|
|
102
|
+
if filename.end_with?('.zip')
|
|
103
|
+
# Process zip file
|
|
104
|
+
wellplates_data, csv_data = process_zip_file(tempfile)
|
|
105
|
+
|
|
106
|
+
error!('Missing JSON data in zip file', 400) unless wellplates_data
|
|
107
|
+
|
|
108
|
+
# Create analysis container and dataset with CSV if present
|
|
109
|
+
# if csv_data
|
|
110
|
+
# create_analysis_with_csv(element, user, csv_data, dose_resp_request, wellplates_data)
|
|
111
|
+
# end
|
|
112
|
+
# else
|
|
113
|
+
# # Handle single JSON file
|
|
114
|
+
# file_content = tempfile.read
|
|
115
|
+
# tempfile.rewind
|
|
116
|
+
# json_data = JSON.parse(file_content).with_indifferent_access
|
|
117
|
+
# wellplates_data = json_data[:Output]
|
|
118
|
+
|
|
119
|
+
# error!('Missing wellplates data', 400) unless wellplates_data
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
dose_resp_request.track_access!
|
|
123
|
+
|
|
124
|
+
# Save output data to dose_resp_outputs table
|
|
125
|
+
output = dose_resp_request.dose_resp_outputs.create!(
|
|
126
|
+
output_data: { Output: wellplates_data }
|
|
127
|
+
)
|
|
128
|
+
if csv_data
|
|
129
|
+
create_analysis_with_csv(element, user, csv_data, dose_resp_request, output)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
dose_resp_request.update!(
|
|
133
|
+
wellplates_metadata: { wellplates: wellplates_data },
|
|
134
|
+
resp_message: 'Data updated successfully'
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Mark as completed
|
|
138
|
+
dose_resp_request.mark_completed!
|
|
139
|
+
|
|
140
|
+
status 200
|
|
141
|
+
{
|
|
142
|
+
success: true,
|
|
143
|
+
message: 'Data updated successfully',
|
|
144
|
+
request_id: dose_resp_request.id
|
|
145
|
+
}
|
|
146
|
+
rescue JSON::ParserError => e
|
|
147
|
+
error!("Invalid JSON: #{e.message}", 400)
|
|
148
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
149
|
+
dose_resp_request.mark_error!(e.message) if dose_resp_request
|
|
150
|
+
error!("Validation error: #{e.message}", 422)
|
|
151
|
+
rescue => e
|
|
152
|
+
dose_resp_request.mark_error!(e.message) if dose_resp_request
|
|
153
|
+
error!("Error: #{e.message}", 500)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def process_zip_file(tempfile)
|
|
157
|
+
wellplates_data = nil
|
|
158
|
+
csv_data = nil
|
|
159
|
+
|
|
160
|
+
Zip::File.open(tempfile.path) do |zip_file|
|
|
161
|
+
zip_file.each do |entry|
|
|
162
|
+
if entry.name.end_with?('.json')
|
|
163
|
+
# Read JSON file
|
|
164
|
+
json_content = entry.get_input_stream.read
|
|
165
|
+
json_data = JSON.parse(json_content).with_indifferent_access
|
|
166
|
+
wellplates_data = json_data[:Output]
|
|
167
|
+
elsif entry.name.end_with?('.xls', '.xlsx', '.csv')
|
|
168
|
+
# Read CSV/Excel file - extract just the basename without path
|
|
169
|
+
csv_content = entry.get_input_stream.read
|
|
170
|
+
csv_data = {
|
|
171
|
+
filename: File.basename(entry.name),
|
|
172
|
+
content: csv_content
|
|
173
|
+
}
|
|
174
|
+
end
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
[wellplates_data, csv_data]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def create_analysis_with_csv(element, user, csv_data, dose_resp_request, output)
|
|
182
|
+
analysis, dataset = create_analysis_with_dataset(
|
|
183
|
+
element: element,
|
|
184
|
+
analysis_name: "MTT Analysis #{dose_resp_request.request_id}-#{output&.id}",
|
|
185
|
+
dataset_name: 'new',
|
|
186
|
+
analysis_attributes: {}
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Determine content type based on file extension
|
|
190
|
+
content_type = case File.extname(csv_data[:filename]).downcase
|
|
191
|
+
when '.xlsx'
|
|
192
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
193
|
+
when '.xls'
|
|
194
|
+
'application/vnd.ms-excel'
|
|
195
|
+
when '.csv'
|
|
196
|
+
'text/csv'
|
|
197
|
+
else
|
|
198
|
+
'application/octet-stream'
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Create a temporary file for the attachment
|
|
202
|
+
temp_file = Tempfile.new([File.basename(csv_data[:filename], '.*'), File.extname(csv_data[:filename])])
|
|
203
|
+
begin
|
|
204
|
+
temp_file.binmode
|
|
205
|
+
temp_file.write(csv_data[:content])
|
|
206
|
+
temp_file.rewind
|
|
207
|
+
|
|
208
|
+
# Create attachment for CSV/Excel file
|
|
209
|
+
attachment = Attachment.new(
|
|
210
|
+
filename: csv_data[:filename],
|
|
211
|
+
file_path: temp_file.path,
|
|
212
|
+
created_by: user.id,
|
|
213
|
+
created_for: user.id,
|
|
214
|
+
attachable_type: 'Container',
|
|
215
|
+
attachable_id: dataset.id,
|
|
216
|
+
content_type: content_type
|
|
217
|
+
)
|
|
218
|
+
attachment.save! if attachment.valid?
|
|
219
|
+
|
|
220
|
+
{ analysis: analysis, dataset: dataset, attachment: attachment }
|
|
221
|
+
ensure
|
|
222
|
+
temp_file.close
|
|
223
|
+
temp_file.unlink
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def create_analysis_with_dataset(
|
|
228
|
+
element:,
|
|
229
|
+
analysis_name: 'New Analysis',
|
|
230
|
+
dataset_name: 'New Dataset',
|
|
231
|
+
analysis_attributes: {}
|
|
232
|
+
)
|
|
233
|
+
# Ensure the element has a root container
|
|
234
|
+
ensure_root_container(element)
|
|
235
|
+
|
|
236
|
+
# Get or create the analyses container
|
|
237
|
+
analyses_container = element.container.children.find_or_create_by(container_type: 'analyses')
|
|
238
|
+
|
|
239
|
+
# Prepare default extended_metadata for analysis
|
|
240
|
+
default_metadata = {
|
|
241
|
+
'content' => '{"ops":[{"insert":""}]}',
|
|
242
|
+
'report' => true
|
|
243
|
+
}
|
|
244
|
+
extended_metadata = default_metadata.merge(analysis_attributes[:extended_metadata] || {})
|
|
245
|
+
|
|
246
|
+
# Create the analysis container
|
|
247
|
+
analysis_container = analyses_container.children.create(
|
|
248
|
+
container_type: 'analysis',
|
|
249
|
+
name: analysis_name,
|
|
250
|
+
description: analysis_attributes[:description] || '',
|
|
251
|
+
extended_metadata: extended_metadata
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
# Create the dataset container nested under the analysis
|
|
255
|
+
dataset_container = analysis_container.children.create(
|
|
256
|
+
container_type: 'dataset',
|
|
257
|
+
name: dataset_name,
|
|
258
|
+
description: '',
|
|
259
|
+
extended_metadata: {}
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
[analysis_container, dataset_container]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def ensure_root_container(element)
|
|
266
|
+
return if element.container.present?
|
|
267
|
+
|
|
268
|
+
element.container = Container.create_root_container
|
|
269
|
+
end
|
|
270
|
+
# def send_mtt_request(current_user, params)
|
|
271
|
+
|
|
272
|
+
# token_uri = token_url(current_user, params)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
# uri = URI.parse(api_url)
|
|
276
|
+
# http = Net::HTTP.new(uri.host, uri.port)
|
|
277
|
+
# http.use_ssl = (uri.scheme == 'https')
|
|
278
|
+
# http.read_timeout = 30
|
|
279
|
+
|
|
280
|
+
# # Ensure path is not empty, default to '/' if needed
|
|
281
|
+
# path = uri.path.empty? ? '/' : uri.path
|
|
282
|
+
# request = Net::HTTP::Post.new(path, { 'Content-Type' => 'application/json' })
|
|
283
|
+
# request.body = json_data.to_json
|
|
284
|
+
|
|
285
|
+
# response = http.request(request)
|
|
286
|
+
|
|
287
|
+
# {
|
|
288
|
+
# success: response.is_a?(Net::HTTPSuccess),
|
|
289
|
+
# status: response.code,
|
|
290
|
+
# body: (JSON.parse(response.body) rescue response.body),
|
|
291
|
+
# message: response.message
|
|
292
|
+
# }
|
|
293
|
+
# rescue StandardError => e
|
|
294
|
+
# {
|
|
295
|
+
# success: false,
|
|
296
|
+
# error: e.message,
|
|
297
|
+
# backtrace: e.backtrace.first(5)
|
|
298
|
+
# }
|
|
299
|
+
# end
|
|
300
|
+
|
|
301
|
+
def extract_readout_titles(wellplate)
|
|
302
|
+
# Extract readout titles from wellplate
|
|
303
|
+
if wellplate.respond_to?(:readout_titles)
|
|
304
|
+
titles = wellplate.readout_titles
|
|
305
|
+
return titles if titles.is_a?(Array)
|
|
306
|
+
return JSON.parse(titles) if titles.is_a?(String)
|
|
307
|
+
end
|
|
308
|
+
[]
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
def extract_wells(wellplate)
|
|
312
|
+
# Extract wells data from wellplate
|
|
313
|
+
wells = wellplate.wells || []
|
|
314
|
+
wells.map do |well|
|
|
315
|
+
# Position might be stored as a hash or separate fields
|
|
316
|
+
position = if well.respond_to?(:position) && well.position.is_a?(Hash)
|
|
317
|
+
well.position
|
|
318
|
+
elsif well.respond_to?(:position_x)
|
|
319
|
+
{ x: well.position_x, y: well.position_y }
|
|
320
|
+
else
|
|
321
|
+
{ x: 0, y: 0 }
|
|
322
|
+
end
|
|
323
|
+
|
|
324
|
+
well_data = {
|
|
325
|
+
id: well.id,
|
|
326
|
+
position: position
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
# Only include readouts if they have values
|
|
330
|
+
readouts = extract_readouts(well)
|
|
331
|
+
well_data[:readouts] = readouts if readouts.present?
|
|
332
|
+
|
|
333
|
+
# Only include sample if it exists
|
|
334
|
+
sample = extract_sample(well)
|
|
335
|
+
well_data[:sample] = sample if sample.present?
|
|
336
|
+
|
|
337
|
+
well_data
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def extract_readouts(well)
|
|
342
|
+
# Extract readouts from well
|
|
343
|
+
# Readouts are typically stored as JSON data in the well
|
|
344
|
+
readouts = if well.respond_to?(:readouts) && well.readouts.is_a?(Array)
|
|
345
|
+
well.readouts
|
|
346
|
+
elsif well.respond_to?(:readouts) && well.readouts.is_a?(String)
|
|
347
|
+
JSON.parse(well.readouts) rescue []
|
|
348
|
+
elsif well.respond_to?(:readouts) && well.readouts.is_a?(Hash)
|
|
349
|
+
well.readouts.values rescue []
|
|
350
|
+
else
|
|
351
|
+
[]
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Filter out empty readouts (both unit and value are blank)
|
|
355
|
+
readouts.select do |readout|
|
|
356
|
+
readout.is_a?(Hash) &&
|
|
357
|
+
(readout['unit'].to_s.present? || readout['value'].to_s.present? ||
|
|
358
|
+
readout[:unit].to_s.present? || readout[:value].to_s.present?)
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def extract_sample(well)
|
|
363
|
+
# Extract sample information from well
|
|
364
|
+
if well.respond_to?(:sample) && well.sample.present?
|
|
365
|
+
sample = well.sample
|
|
366
|
+
return {
|
|
367
|
+
id: sample.id,
|
|
368
|
+
short_label: sample.short_label,
|
|
369
|
+
conc: sample.try(:molarity_value) || 0
|
|
370
|
+
}
|
|
371
|
+
end
|
|
372
|
+
nil
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def generate_wellplates_metadata(wellplates)
|
|
376
|
+
wellplates.map do |wellplate|
|
|
377
|
+
{
|
|
378
|
+
id: wellplate.id.to_s,
|
|
379
|
+
readoutTitles: extract_readout_titles(wellplate),
|
|
380
|
+
wells: extract_wells(wellplate)
|
|
381
|
+
}
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def generate_json_data(wellplates)
|
|
387
|
+
# # Generate wellplates metadata
|
|
388
|
+
wellplates_metadata = generate_wellplates_metadata(wellplates)
|
|
389
|
+
|
|
390
|
+
# Generate JSON structure
|
|
391
|
+
json_data = {
|
|
392
|
+
id: element.id.to_s,
|
|
393
|
+
request_id: dose_resp_request.id.to_s,
|
|
394
|
+
wellplates: wellplates_metadata
|
|
395
|
+
}
|
|
396
|
+
# Save to JSON file
|
|
397
|
+
filename = "mtt_request_#{element.id}_#{dose_resp_request.id}.json"
|
|
398
|
+
filepath = Rails.root.join('tmp', filename)
|
|
399
|
+
File.write(filepath, JSON.pretty_generate(json_data))
|
|
400
|
+
json_data
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
end
|
|
@@ -13,6 +13,7 @@ module Labimotion
|
|
|
13
13
|
optional :desc, type: String, desc: 'Klass desc'
|
|
14
14
|
optional :klass_prefix, type: String, desc: 'Klass klass_prefix'
|
|
15
15
|
optional :icon_name, type: String, desc: 'Klass icon_name'
|
|
16
|
+
optional :metadata, type: Hash, desc: 'Klass metadata'
|
|
16
17
|
requires :properties_template, type: Hash, desc: 'Klass template'
|
|
17
18
|
optional :properties_release, type: Hash, desc: 'Klass release'
|
|
18
19
|
optional :released_at, type: DateTime, desc: 'Klass released_at'
|
|
@@ -29,6 +30,7 @@ module Labimotion
|
|
|
29
30
|
requires :klass_prefix, type: String, desc: 'Element Klass Short Label Prefix'
|
|
30
31
|
optional :icon_name, type: String, desc: 'Element Klass Icon Name'
|
|
31
32
|
optional :desc, type: String, desc: 'Element Klass Desc'
|
|
33
|
+
optional :metadata, type: Hash, desc: 'Element Klass metadata'
|
|
32
34
|
optional :properties_template, type: Hash, desc: 'Element Klass properties template'
|
|
33
35
|
end
|
|
34
36
|
|
|
@@ -69,6 +71,7 @@ module Labimotion
|
|
|
69
71
|
params :upload_segment_klass_params do
|
|
70
72
|
requires :label, type: String, desc: 'Klass label'
|
|
71
73
|
optional :desc, type: String, desc: 'Klass desc'
|
|
74
|
+
optional :metadata, type: Hash, desc: 'Klass metadata'
|
|
72
75
|
requires :properties_template, type: Hash, desc: 'Klass template'
|
|
73
76
|
optional :properties_release, type: Hash, desc: 'Klass release'
|
|
74
77
|
optional :released_at, type: DateTime, desc: 'Klass released_at'
|
|
@@ -95,6 +98,7 @@ module Labimotion
|
|
|
95
98
|
requires :element_klass, type: Integer, desc: 'Element Klass Id'
|
|
96
99
|
optional :desc, type: String, desc: 'Segment Klass Desc'
|
|
97
100
|
optional :place, type: String, desc: 'Segment Klass Place', default: '100'
|
|
101
|
+
optional :metadata, type: Hash, desc: 'Klass metadata'
|
|
98
102
|
optional :properties_template, type: Hash, desc: 'Element Klass properties template'
|
|
99
103
|
end
|
|
100
104
|
|
|
@@ -142,5 +146,11 @@ module Labimotion
|
|
|
142
146
|
requires :layer_id, type: String, desc: 'layer identifier'
|
|
143
147
|
requires :field_id, type: String, desc: 'field identifier'
|
|
144
148
|
end
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
params :create_mtt_request_params do
|
|
152
|
+
requires :id, type: Integer, desc: 'Element ID'
|
|
153
|
+
requires :wellplate_ids, type: Array, desc: 'Selected Wellplates'
|
|
154
|
+
end
|
|
145
155
|
end
|
|
146
156
|
end
|
|
@@ -29,6 +29,7 @@ module Labimotion
|
|
|
29
29
|
properties: ods.properties,
|
|
30
30
|
properties_release: ods.properties_release,
|
|
31
31
|
klass_uuid: ods.klass_uuid,
|
|
32
|
+
metadata: ods.metadata || {}
|
|
32
33
|
)
|
|
33
34
|
end
|
|
34
35
|
|
|
@@ -49,8 +50,8 @@ module Labimotion
|
|
|
49
50
|
props = Labimotion::VocabularyHandler.update_vocabularies(props, dataset_args[:current_user], dataset_args[:element])
|
|
50
51
|
|
|
51
52
|
ds = Labimotion::Dataset.find_by(element_type: self.class.name, element_id: id)
|
|
52
|
-
if ds.present? && (ds.klass_uuid != klass.uuid || ds.properties != props)
|
|
53
|
-
ds.update!(properties_release: klass.properties_release, uuid: uuid, dataset_klass_id: dataset_klass_id, properties: props, klass_uuid: klass.uuid)
|
|
53
|
+
if ds.present? && (ds.klass_uuid != klass.uuid || ds.properties != props || ds.metadata != metadata)
|
|
54
|
+
ds.update!(properties_release: klass.properties_release, uuid: uuid, dataset_klass_id: dataset_klass_id, properties: props, klass_uuid: klass.uuid, metadata: metadata)
|
|
54
55
|
end
|
|
55
56
|
return if ds.present?
|
|
56
57
|
|
|
@@ -9,7 +9,6 @@ module Labimotion
|
|
|
9
9
|
before_save :check_identifier
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
def check_identifier
|
|
14
13
|
self.identifier = identifier || SecureRandom.uuid if self.has_attribute?(:identifier)
|
|
15
14
|
end
|
|
@@ -34,7 +33,7 @@ module Labimotion
|
|
|
34
33
|
properties_release: properties_release,
|
|
35
34
|
released_at: DateTime.now,
|
|
36
35
|
updated_by: current_user&.id,
|
|
37
|
-
released_by: current_user&.id
|
|
36
|
+
released_by: current_user&.id
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
self.update!(klass_attributes)
|
|
@@ -46,7 +45,7 @@ module Labimotion
|
|
|
46
45
|
created_by: updated_by,
|
|
47
46
|
properties_release: properties_release,
|
|
48
47
|
released_at: released_at,
|
|
49
|
-
metadata: metadata
|
|
48
|
+
metadata: metadata || {}
|
|
50
49
|
}
|
|
51
50
|
attributes["#{self.class.name.underscore.split('/').last}_id"] = id
|
|
52
51
|
"#{self.class.name}esRevision".constantize.create(attributes)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Labimotion
|
|
4
|
+
class DoseRespOutput < ApplicationRecord
|
|
5
|
+
acts_as_paranoid
|
|
6
|
+
self.table_name = :dose_resp_outputs
|
|
7
|
+
|
|
8
|
+
# Associations
|
|
9
|
+
belongs_to :dose_resp_request, class_name: 'Labimotion::DoseRespRequest'
|
|
10
|
+
|
|
11
|
+
# Validations
|
|
12
|
+
validates :dose_resp_request, presence: true
|
|
13
|
+
validates :output_data, presence: true
|
|
14
|
+
validate :output_data_is_hash
|
|
15
|
+
|
|
16
|
+
# Scopes
|
|
17
|
+
scope :recent, -> { order(created_at: :desc) }
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def output_data_is_hash
|
|
22
|
+
return if output_data.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
errors.add(:output_data, 'must be a Hash')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Labimotion
|
|
4
|
+
class DoseRespRequest < ApplicationRecord
|
|
5
|
+
acts_as_paranoid
|
|
6
|
+
self.table_name = :dose_resp_requests
|
|
7
|
+
|
|
8
|
+
# Token generation
|
|
9
|
+
has_secure_token :access_token
|
|
10
|
+
|
|
11
|
+
# Callbacks
|
|
12
|
+
before_create :generate_request_id
|
|
13
|
+
|
|
14
|
+
# Associations
|
|
15
|
+
belongs_to :element, class_name: 'Labimotion::Element'
|
|
16
|
+
belongs_to :creator, foreign_key: :created_by, class_name: 'User'
|
|
17
|
+
has_many :dose_resp_outputs, class_name: 'Labimotion::DoseRespOutput', dependent: :destroy
|
|
18
|
+
|
|
19
|
+
# Validations
|
|
20
|
+
validates :element, presence: true
|
|
21
|
+
validates :creator, presence: true
|
|
22
|
+
validates :expires_at, presence: true
|
|
23
|
+
validates :state, inclusion: { in: [-1, 0, 1, 2] }
|
|
24
|
+
validate :metadata_is_hash
|
|
25
|
+
validate :input_metadata_is_hash
|
|
26
|
+
|
|
27
|
+
# State constants
|
|
28
|
+
STATE_ERROR = -1
|
|
29
|
+
STATE_INITIAL = 0
|
|
30
|
+
STATE_PROCESSING = 1
|
|
31
|
+
STATE_COMPLETED = 2
|
|
32
|
+
|
|
33
|
+
# Scopes
|
|
34
|
+
scope :active, -> { where('expires_at > ?', Time.current).where(revoked_at: nil) }
|
|
35
|
+
scope :expired, -> { where('expires_at <= ?', Time.current) }
|
|
36
|
+
scope :revoked, -> { where.not(revoked_at: nil) }
|
|
37
|
+
|
|
38
|
+
# Instance methods
|
|
39
|
+
def expired?
|
|
40
|
+
expires_at.present? && expires_at < Time.current
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def revoked?
|
|
44
|
+
revoked_at.present?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def active?
|
|
48
|
+
!expired? && !revoked?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def revoke!
|
|
52
|
+
update!(revoked_at: Time.current, state: STATE_ERROR)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def mark_processing!
|
|
56
|
+
update!(state: STATE_PROCESSING) if state == STATE_INITIAL
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def mark_completed!
|
|
60
|
+
update!(state: STATE_COMPLETED)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def mark_error!(message = nil)
|
|
64
|
+
update!(state: STATE_ERROR, resp_message: message)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def track_access!
|
|
68
|
+
increment!(:access_count)
|
|
69
|
+
update_columns(
|
|
70
|
+
first_accessed_at: first_accessed_at || Time.current,
|
|
71
|
+
last_accessed_at: Time.current
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def metadata_is_hash
|
|
78
|
+
return if wellplates_metadata.nil? || wellplates_metadata.is_a?(Hash)
|
|
79
|
+
|
|
80
|
+
errors.add(:wellplates_metadata, 'must be a hash')
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def input_metadata_is_hash
|
|
84
|
+
return if input_metadata.nil? || input_metadata.is_a?(Hash)
|
|
85
|
+
|
|
86
|
+
errors.add(:input_metadata, 'must be a hash')
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def generate_request_id
|
|
90
|
+
self.request_id = "MTT-#{Time.current.strftime('%Y%m%d-%H%M%S')}-#{SecureRandom.hex(3)}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/labimotion/version.rb
CHANGED
data/lib/labimotion.rb
CHANGED
|
@@ -27,6 +27,8 @@ module Labimotion
|
|
|
27
27
|
autoload :ExporterAPI, 'labimotion/apis/exporter_api'
|
|
28
28
|
autoload :StandardLayerAPI, 'labimotion/apis/standard_layer_api'
|
|
29
29
|
autoload :VocabularyAPI, 'labimotion/apis/vocabulary_api'
|
|
30
|
+
autoload :MttAPI, 'labimotion/apis/mtt_api'
|
|
31
|
+
autoload :DoseRespRequestAPI, 'labimotion/apis/dose_resp_request_api'
|
|
30
32
|
|
|
31
33
|
######## Entities
|
|
32
34
|
autoload :PropertiesEntity, 'labimotion/entities/properties_entity'
|
|
@@ -114,6 +116,8 @@ module Labimotion
|
|
|
114
116
|
autoload :StdLayersRevision, 'labimotion/models/std_layers_revision'
|
|
115
117
|
|
|
116
118
|
autoload :DeviceDescription, 'labimotion/models/device_description'
|
|
119
|
+
autoload :DoseRespRequest, 'labimotion/models/dose_resp_request'
|
|
120
|
+
autoload :DoseRespOutput, 'labimotion/models/dose_resp_output'
|
|
117
121
|
autoload :Reaction, 'labimotion/models/reaction'
|
|
118
122
|
autoload :ResearchPlan, 'labimotion/models/research_plan'
|
|
119
123
|
autoload :Sample, 'labimotion/models/sample'
|
|
@@ -126,6 +130,6 @@ module Labimotion
|
|
|
126
130
|
autoload :ElementFetchable, 'labimotion/models/concerns/element_fetchable'
|
|
127
131
|
autoload :Segmentable, 'labimotion/models/concerns/segmentable'
|
|
128
132
|
autoload :Datasetable, 'labimotion/models/concerns/datasetable'
|
|
129
|
-
autoload :AttachmentConverter, 'labimotion/models/concerns/attachment_converter
|
|
133
|
+
autoload :AttachmentConverter, 'labimotion/models/concerns/attachment_converter'
|
|
130
134
|
autoload :LinkedProperties, 'labimotion/models/concerns/linked_properties'
|
|
131
135
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: labimotion
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.2.0.
|
|
4
|
+
version: 2.2.0.rc2
|
|
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:
|
|
12
|
+
date: 2026-01-29 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: caxlsx
|
|
@@ -55,12 +55,14 @@ extra_rdoc_files: []
|
|
|
55
55
|
files:
|
|
56
56
|
- lib/labimotion.rb
|
|
57
57
|
- lib/labimotion/apis/converter_api.rb
|
|
58
|
+
- lib/labimotion/apis/dose_resp_request_api.rb
|
|
58
59
|
- lib/labimotion/apis/exporter_api.rb
|
|
59
60
|
- lib/labimotion/apis/generic_dataset_api.rb
|
|
60
61
|
- lib/labimotion/apis/generic_element_api.rb
|
|
61
62
|
- lib/labimotion/apis/generic_klass_api.rb
|
|
62
63
|
- lib/labimotion/apis/labimotion_api.rb
|
|
63
64
|
- lib/labimotion/apis/labimotion_hub_api.rb
|
|
65
|
+
- lib/labimotion/apis/mtt_api.rb
|
|
64
66
|
- lib/labimotion/apis/segment_api.rb
|
|
65
67
|
- lib/labimotion/apis/standard_api.rb
|
|
66
68
|
- lib/labimotion/apis/standard_layer_api.rb
|
|
@@ -90,6 +92,7 @@ files:
|
|
|
90
92
|
- lib/labimotion/helpers/element_helpers.rb
|
|
91
93
|
- lib/labimotion/helpers/exporter_helpers.rb
|
|
92
94
|
- lib/labimotion/helpers/generic_helpers.rb
|
|
95
|
+
- lib/labimotion/helpers/mtt_helpers.rb
|
|
93
96
|
- lib/labimotion/helpers/param_helpers.rb
|
|
94
97
|
- lib/labimotion/helpers/repository_helpers.rb
|
|
95
98
|
- lib/labimotion/helpers/sample_association_helpers.rb
|
|
@@ -130,6 +133,8 @@ files:
|
|
|
130
133
|
- lib/labimotion/models/dataset_klasses_revision.rb
|
|
131
134
|
- lib/labimotion/models/datasets_revision.rb
|
|
132
135
|
- lib/labimotion/models/device_description.rb
|
|
136
|
+
- lib/labimotion/models/dose_resp_output.rb
|
|
137
|
+
- lib/labimotion/models/dose_resp_request.rb
|
|
133
138
|
- lib/labimotion/models/element.rb
|
|
134
139
|
- lib/labimotion/models/element_klass.rb
|
|
135
140
|
- lib/labimotion/models/element_klasses_revision.rb
|