labimotion 2.1.0.rc17 → 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/labimotion_api.rb +2 -0
- data/lib/labimotion/apis/mtt_api.rb +125 -0
- data/lib/labimotion/entities/properties_entity.rb +1 -3
- data/lib/labimotion/helpers/mtt_helpers.rb +403 -0
- data/lib/labimotion/helpers/param_helpers.rb +6 -0
- data/lib/labimotion/libs/converter.rb +2 -4
- 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
|
|
@@ -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
|
|
@@ -13,9 +13,7 @@ module Labimotion
|
|
|
13
13
|
private
|
|
14
14
|
|
|
15
15
|
def process_layers
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
(object.properties[Labimotion::Prop::LAYERS]&.keys || []).each do |key|
|
|
16
|
+
(object&.properties.is_a?(Hash) && (object.properties[Labimotion::Prop::LAYERS]&.keys || [])).each do |key|
|
|
19
17
|
yield(key, object.properties[Labimotion::Prop::LAYERS][key])
|
|
20
18
|
end
|
|
21
19
|
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
|
|
@@ -146,5 +146,11 @@ module Labimotion
|
|
|
146
146
|
requires :layer_id, type: String, desc: 'layer identifier'
|
|
147
147
|
requires :field_id, type: String, desc: 'field identifier'
|
|
148
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
|
|
149
155
|
end
|
|
150
156
|
end
|
|
@@ -154,13 +154,12 @@ module Labimotion
|
|
|
154
154
|
ofile = Rails.root.join(data[:f], data[:a].filename)
|
|
155
155
|
FileUtils.cp(data[:a].attachment_url, ofile)
|
|
156
156
|
|
|
157
|
-
File.open(ofile, '
|
|
157
|
+
File.open(ofile, 'r') do |f|
|
|
158
158
|
body = { file: f }
|
|
159
159
|
response = HTTParty.post(
|
|
160
160
|
uri('conversions'),
|
|
161
161
|
basic_auth: auth,
|
|
162
162
|
body: body,
|
|
163
|
-
multipart: true,
|
|
164
163
|
timeout: timeout,
|
|
165
164
|
)
|
|
166
165
|
end
|
|
@@ -307,13 +306,12 @@ module Labimotion
|
|
|
307
306
|
|
|
308
307
|
def self.create_tables(tmpfile)
|
|
309
308
|
res = {}
|
|
310
|
-
File.open(tmpfile.path, '
|
|
309
|
+
File.open(tmpfile.path, 'r') do |file|
|
|
311
310
|
body = { file: file }
|
|
312
311
|
response = HTTParty.post(
|
|
313
312
|
uri('tables'),
|
|
314
313
|
basic_auth: auth,
|
|
315
314
|
body: body,
|
|
316
|
-
multipart: true,
|
|
317
315
|
timeout: timeout,
|
|
318
316
|
)
|
|
319
317
|
res = response.parsed_response
|
|
@@ -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.
|
|
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: 2026-01-
|
|
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
|