labimotion 2.2.0.rc8 → 2.2.0.rc10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +117 -0
- data/LICENSE +661 -0
- data/README.md +68 -0
- data/lib/labimotion/apis/dose_resp_request_api.rb +241 -0
- data/lib/labimotion/apis/element_variation_api.rb +45 -0
- data/lib/labimotion/apis/generic_element_api.rb +6 -3
- data/lib/labimotion/apis/labimotion_api.rb +4 -0
- data/lib/labimotion/apis/mtt_api.rb +238 -0
- data/lib/labimotion/apis/user_api.rb +56 -0
- data/lib/labimotion/entities/element_entity.rb +1 -0
- data/lib/labimotion/entities/element_variation_entity.rb +29 -0
- data/lib/labimotion/entities/user_entity.rb +9 -0
- data/lib/labimotion/helpers/element_helpers.rb +1 -1
- data/lib/labimotion/helpers/generic_helpers.rb +1 -1
- data/lib/labimotion/helpers/mtt_helpers.rb +428 -0
- data/lib/labimotion/helpers/param_helpers.rb +6 -0
- data/lib/labimotion/libs/export_element.rb +245 -19
- data/lib/labimotion/models/dose_resp_output.rb +27 -0
- data/lib/labimotion/models/dose_resp_request.rb +93 -0
- data/lib/labimotion/models/element.rb +6 -0
- data/lib/labimotion/models/element_variation.rb +21 -0
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +10 -1
- metadata +15 -2
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'labimotion/version'
|
|
4
|
+
|
|
5
|
+
module Labimotion
|
|
6
|
+
# User API
|
|
7
|
+
class UserAPI < Grape::API
|
|
8
|
+
helpers Labimotion::ParamHelpers
|
|
9
|
+
helpers Labimotion::GenericHelpers
|
|
10
|
+
|
|
11
|
+
resource :limo do
|
|
12
|
+
resource :users do
|
|
13
|
+
namespace :list do
|
|
14
|
+
desc 'List users by keyword'
|
|
15
|
+
params do
|
|
16
|
+
requires :name, type: String, desc: 'Keyword'
|
|
17
|
+
optional :limit, type: Integer, default: 5, desc: 'Limit (max 10)'
|
|
18
|
+
optional :type, type: String, default: 'Person', desc: 'User type'
|
|
19
|
+
end
|
|
20
|
+
get do
|
|
21
|
+
error!('401 Unauthorized', 401) unless current_user
|
|
22
|
+
return { mc: 'ss00', msg: 'Entered name too short', data: [] } if params[:name].to_s.length < 3
|
|
23
|
+
|
|
24
|
+
query = 'first_name ILIKE :q OR last_name ILIKE :q OR name ILIKE :q OR name_abbreviation ILIKE :q'
|
|
25
|
+
users = User.where(query, q: "%#{params[:name]}%")
|
|
26
|
+
.where(type: params[:type])
|
|
27
|
+
.limit([params[:limit].to_i, 10].min)
|
|
28
|
+
data = Labimotion::UserEntity.represent(users)
|
|
29
|
+
{ mc: 'ss00', data: data }
|
|
30
|
+
rescue StandardError => e
|
|
31
|
+
Labimotion.log_exception(e, current_user)
|
|
32
|
+
{ mc: 'se00', msg: e.message, data: [] }
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
desc 'Get user info by id'
|
|
37
|
+
params do
|
|
38
|
+
requires :id, type: Integer, desc: 'User id'
|
|
39
|
+
end
|
|
40
|
+
route_param :id do
|
|
41
|
+
get do
|
|
42
|
+
error!('401 Unauthorized', 401) unless current_user
|
|
43
|
+
user = User.find(params[:id])
|
|
44
|
+
data = Labimotion::UserEntity.represent(user)
|
|
45
|
+
{ mc: 'ss00', data: data }
|
|
46
|
+
rescue ActiveRecord::RecordNotFound
|
|
47
|
+
error!('404 Not Found', 404)
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
Labimotion.log_exception(e, current_user)
|
|
50
|
+
{ mc: 'se00', msg: e.message, data: {} }
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'labimotion/entities/application_entity'
|
|
4
|
+
|
|
5
|
+
module Labimotion
|
|
6
|
+
class ElementVariationEntity < Labimotion::ApplicationEntity
|
|
7
|
+
expose :id
|
|
8
|
+
expose :element_id, as: :elementId
|
|
9
|
+
expose :variations
|
|
10
|
+
expose :layout
|
|
11
|
+
|
|
12
|
+
def variations
|
|
13
|
+
rows = object.variations_hash
|
|
14
|
+
rows.transform_values do |row|
|
|
15
|
+
next row unless row.is_a?(Hash)
|
|
16
|
+
|
|
17
|
+
row.symbolize_keys.slice(:uuid, :name, :properties, :metadata, :segments).tap do |slim|
|
|
18
|
+
slim[:properties] = (slim[:properties] || {})
|
|
19
|
+
slim[:metadata] = (slim[:metadata] || {}).slice('notes', 'analyses', 'group', :notes, :analyses, :group)
|
|
20
|
+
slim[:segments] = (slim[:segments] || {})
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def layout
|
|
26
|
+
object.layout_hash
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -200,7 +200,7 @@ module Labimotion
|
|
|
200
200
|
def element_revisions(params)
|
|
201
201
|
klass = Labimotion::Element.find(params[:id])
|
|
202
202
|
list = klass.elements_revisions unless klass.nil?
|
|
203
|
-
list&.order(created_at: :desc)&.limit(
|
|
203
|
+
list&.order(created_at: :desc)&.limit(params[:limit])
|
|
204
204
|
rescue StandardError => e
|
|
205
205
|
Labimotion.log_exception(e, current_user)
|
|
206
206
|
raise e
|
|
@@ -95,7 +95,7 @@ module Labimotion
|
|
|
95
95
|
def list_klass_revisions(params)
|
|
96
96
|
klass = "Labimotion::#{params[:klass]}".constantize.find_by(id: params[:id])
|
|
97
97
|
list = klass.send("#{params[:klass].underscore}es_revisions") unless klass.nil?
|
|
98
|
-
list&.order(released_at: :desc)&.limit(
|
|
98
|
+
list&.order(released_at: :desc)&.limit(params[:limit])
|
|
99
99
|
rescue StandardError => e
|
|
100
100
|
Labimotion.log_exception(e, current_user)
|
|
101
101
|
raise e
|
|
@@ -0,0 +1,428 @@
|
|
|
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
|
+
element_info: extract_element_properties(dose_resp_request.element),
|
|
78
|
+
wellplates: wellplates_data
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
status 200
|
|
82
|
+
response_data
|
|
83
|
+
rescue => e
|
|
84
|
+
error!("Error: #{e.message}", 500)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def upload_json_from_external_app
|
|
88
|
+
# Get token from route params
|
|
89
|
+
token = params[:token]
|
|
90
|
+
dose_resp_request = validate_token(token)
|
|
91
|
+
|
|
92
|
+
# Validate user access
|
|
93
|
+
access_info = validate_user_access(dose_resp_request)
|
|
94
|
+
user = access_info[:user]
|
|
95
|
+
element = access_info[:element]
|
|
96
|
+
# Handle file upload
|
|
97
|
+
file_param = params['file'] || params[:file]
|
|
98
|
+
error!('No file uploaded', 400) unless file_param && file_param.is_a?(Hash) && file_param['tempfile']
|
|
99
|
+
|
|
100
|
+
tempfile = file_param['tempfile']
|
|
101
|
+
filename = file_param['filename'] || 'upload'
|
|
102
|
+
# Check if it's a zip file
|
|
103
|
+
if filename.end_with?('.zip')
|
|
104
|
+
# Process zip file
|
|
105
|
+
wellplates_data, csv_data = process_zip_file(tempfile)
|
|
106
|
+
|
|
107
|
+
error!('Missing JSON data in zip file', 400) unless wellplates_data
|
|
108
|
+
|
|
109
|
+
# Create analysis container and dataset with CSV if present
|
|
110
|
+
# if csv_data
|
|
111
|
+
# create_analysis_with_csv(element, user, csv_data, dose_resp_request, wellplates_data)
|
|
112
|
+
# end
|
|
113
|
+
# else
|
|
114
|
+
# # Handle single JSON file
|
|
115
|
+
# file_content = tempfile.read
|
|
116
|
+
# tempfile.rewind
|
|
117
|
+
# json_data = JSON.parse(file_content).with_indifferent_access
|
|
118
|
+
# wellplates_data = json_data[:Output]
|
|
119
|
+
|
|
120
|
+
# error!('Missing wellplates data', 400) unless wellplates_data
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
dose_resp_request.track_access!
|
|
124
|
+
|
|
125
|
+
# Save output data to dose_resp_outputs table
|
|
126
|
+
output = dose_resp_request.dose_resp_outputs.create!(
|
|
127
|
+
output_data: { Output: wellplates_data }
|
|
128
|
+
)
|
|
129
|
+
if csv_data
|
|
130
|
+
create_analysis_with_csv(element, user, csv_data, dose_resp_request, output)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
dose_resp_request.update!(
|
|
134
|
+
wellplates_metadata: { wellplates: wellplates_data },
|
|
135
|
+
resp_message: 'Data updated successfully'
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
# Mark as completed
|
|
139
|
+
dose_resp_request.mark_completed!
|
|
140
|
+
|
|
141
|
+
status 200
|
|
142
|
+
{
|
|
143
|
+
success: true,
|
|
144
|
+
message: 'Data updated successfully',
|
|
145
|
+
request_id: dose_resp_request.id
|
|
146
|
+
}
|
|
147
|
+
rescue JSON::ParserError => e
|
|
148
|
+
error!("Invalid JSON: #{e.message}", 400)
|
|
149
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
150
|
+
dose_resp_request.mark_error!(e.message) if dose_resp_request
|
|
151
|
+
error!("Validation error: #{e.message}", 422)
|
|
152
|
+
rescue => e
|
|
153
|
+
dose_resp_request.mark_error!(e.message) if dose_resp_request
|
|
154
|
+
error!("Error: #{e.message}", 500)
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def process_zip_file(tempfile)
|
|
158
|
+
wellplates_data = nil
|
|
159
|
+
csv_data = nil
|
|
160
|
+
|
|
161
|
+
Zip::File.open(tempfile.path) do |zip_file|
|
|
162
|
+
zip_file.each do |entry|
|
|
163
|
+
if entry.name.end_with?('.json')
|
|
164
|
+
# Read JSON file
|
|
165
|
+
json_content = entry.get_input_stream.read
|
|
166
|
+
json_data = JSON.parse(json_content).with_indifferent_access
|
|
167
|
+
wellplates_data = json_data[:Output]
|
|
168
|
+
elsif entry.name.end_with?('.xls', '.xlsx', '.csv')
|
|
169
|
+
# Read CSV/Excel file - extract just the basename without path
|
|
170
|
+
csv_content = entry.get_input_stream.read
|
|
171
|
+
csv_data = {
|
|
172
|
+
filename: File.basename(entry.name),
|
|
173
|
+
content: csv_content
|
|
174
|
+
}
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
[wellplates_data, csv_data]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def create_analysis_with_csv(element, user, csv_data, dose_resp_request, output)
|
|
183
|
+
analysis, dataset = create_analysis_with_dataset(
|
|
184
|
+
element: element,
|
|
185
|
+
analysis_name: "MTT Analysis #{dose_resp_request.request_id}-#{output&.id}",
|
|
186
|
+
dataset_name: 'new',
|
|
187
|
+
analysis_attributes: {}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
# Determine content type based on file extension
|
|
191
|
+
content_type = case File.extname(csv_data[:filename]).downcase
|
|
192
|
+
when '.xlsx'
|
|
193
|
+
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
194
|
+
when '.xls'
|
|
195
|
+
'application/vnd.ms-excel'
|
|
196
|
+
when '.csv'
|
|
197
|
+
'text/csv'
|
|
198
|
+
else
|
|
199
|
+
'application/octet-stream'
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
# Create a temporary file for the attachment
|
|
203
|
+
temp_file = Tempfile.new([File.basename(csv_data[:filename], '.*'), File.extname(csv_data[:filename])])
|
|
204
|
+
begin
|
|
205
|
+
temp_file.binmode
|
|
206
|
+
temp_file.write(csv_data[:content])
|
|
207
|
+
temp_file.rewind
|
|
208
|
+
|
|
209
|
+
# Create attachment for CSV/Excel file
|
|
210
|
+
attachment = Attachment.new(
|
|
211
|
+
filename: csv_data[:filename],
|
|
212
|
+
file_path: temp_file.path,
|
|
213
|
+
created_by: user.id,
|
|
214
|
+
created_for: user.id,
|
|
215
|
+
attachable_type: 'Container',
|
|
216
|
+
attachable_id: dataset.id,
|
|
217
|
+
content_type: content_type
|
|
218
|
+
)
|
|
219
|
+
attachment.save! if attachment.valid?
|
|
220
|
+
|
|
221
|
+
{ analysis: analysis, dataset: dataset, attachment: attachment }
|
|
222
|
+
ensure
|
|
223
|
+
temp_file.close
|
|
224
|
+
temp_file.unlink
|
|
225
|
+
end
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def create_analysis_with_dataset(
|
|
229
|
+
element:,
|
|
230
|
+
analysis_name: 'New Analysis',
|
|
231
|
+
dataset_name: 'New Dataset',
|
|
232
|
+
analysis_attributes: {}
|
|
233
|
+
)
|
|
234
|
+
# Ensure the element has a root container
|
|
235
|
+
ensure_root_container(element)
|
|
236
|
+
|
|
237
|
+
# Get or create the analyses container
|
|
238
|
+
analyses_container = element.container.children.find_or_create_by(container_type: 'analyses')
|
|
239
|
+
|
|
240
|
+
# Prepare default extended_metadata for analysis
|
|
241
|
+
default_metadata = {
|
|
242
|
+
'content' => '{"ops":[{"insert":""}]}',
|
|
243
|
+
'report' => true
|
|
244
|
+
}
|
|
245
|
+
extended_metadata = default_metadata.merge(analysis_attributes[:extended_metadata] || {})
|
|
246
|
+
|
|
247
|
+
# Create the analysis container
|
|
248
|
+
analysis_container = analyses_container.children.create(
|
|
249
|
+
container_type: 'analysis',
|
|
250
|
+
name: analysis_name,
|
|
251
|
+
description: analysis_attributes[:description] || '',
|
|
252
|
+
extended_metadata: extended_metadata
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
# Create the dataset container nested under the analysis
|
|
256
|
+
dataset_container = analysis_container.children.create(
|
|
257
|
+
container_type: 'dataset',
|
|
258
|
+
name: dataset_name,
|
|
259
|
+
description: '',
|
|
260
|
+
extended_metadata: {}
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
[analysis_container, dataset_container]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
def ensure_root_container(element)
|
|
267
|
+
return if element.container.present?
|
|
268
|
+
|
|
269
|
+
element.container = Container.create_root_container
|
|
270
|
+
end
|
|
271
|
+
# def send_mtt_request(current_user, params)
|
|
272
|
+
|
|
273
|
+
# token_uri = token_url(current_user, params)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# uri = URI.parse(api_url)
|
|
277
|
+
# http = Net::HTTP.new(uri.host, uri.port)
|
|
278
|
+
# http.use_ssl = (uri.scheme == 'https')
|
|
279
|
+
# http.read_timeout = 30
|
|
280
|
+
|
|
281
|
+
# # Ensure path is not empty, default to '/' if needed
|
|
282
|
+
# path = uri.path.empty? ? '/' : uri.path
|
|
283
|
+
# request = Net::HTTP::Post.new(path, { 'Content-Type' => 'application/json' })
|
|
284
|
+
# request.body = json_data.to_json
|
|
285
|
+
|
|
286
|
+
# response = http.request(request)
|
|
287
|
+
|
|
288
|
+
# {
|
|
289
|
+
# success: response.is_a?(Net::HTTPSuccess),
|
|
290
|
+
# status: response.code,
|
|
291
|
+
# body: (JSON.parse(response.body) rescue response.body),
|
|
292
|
+
# message: response.message
|
|
293
|
+
# }
|
|
294
|
+
# rescue StandardError => e
|
|
295
|
+
# {
|
|
296
|
+
# success: false,
|
|
297
|
+
# error: e.message,
|
|
298
|
+
# backtrace: e.backtrace.first(5)
|
|
299
|
+
# }
|
|
300
|
+
# end
|
|
301
|
+
|
|
302
|
+
def extract_readout_titles(wellplate)
|
|
303
|
+
# Extract readout titles from wellplate
|
|
304
|
+
if wellplate.respond_to?(:readout_titles)
|
|
305
|
+
titles = wellplate.readout_titles
|
|
306
|
+
return titles if titles.is_a?(Array)
|
|
307
|
+
return JSON.parse(titles) if titles.is_a?(String)
|
|
308
|
+
end
|
|
309
|
+
[]
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def extract_wells(wellplate)
|
|
313
|
+
# Extract wells data from wellplate
|
|
314
|
+
wells = wellplate.wells || []
|
|
315
|
+
wells.map do |well|
|
|
316
|
+
# Position might be stored as a hash or separate fields
|
|
317
|
+
position = if well.respond_to?(:position) && well.position.is_a?(Hash)
|
|
318
|
+
well.position
|
|
319
|
+
elsif well.respond_to?(:position_x)
|
|
320
|
+
{ x: well.position_x, y: well.position_y }
|
|
321
|
+
else
|
|
322
|
+
{ x: 0, y: 0 }
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
well_data = {
|
|
326
|
+
id: well.id,
|
|
327
|
+
position: position
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
# Only include readouts if they have values
|
|
331
|
+
readouts = extract_readouts(well)
|
|
332
|
+
well_data[:readouts] = readouts if readouts.present?
|
|
333
|
+
|
|
334
|
+
# Only include sample if it exists
|
|
335
|
+
sample = extract_sample(well)
|
|
336
|
+
well_data[:sample] = sample if sample.present?
|
|
337
|
+
|
|
338
|
+
well_data
|
|
339
|
+
end
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def extract_readouts(well)
|
|
343
|
+
# Extract readouts from well
|
|
344
|
+
# Readouts are typically stored as JSON data in the well
|
|
345
|
+
readouts = if well.respond_to?(:readouts) && well.readouts.is_a?(Array)
|
|
346
|
+
well.readouts
|
|
347
|
+
elsif well.respond_to?(:readouts) && well.readouts.is_a?(String)
|
|
348
|
+
JSON.parse(well.readouts) rescue []
|
|
349
|
+
elsif well.respond_to?(:readouts) && well.readouts.is_a?(Hash)
|
|
350
|
+
well.readouts.values rescue []
|
|
351
|
+
else
|
|
352
|
+
[]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Filter out empty readouts (both unit and value are blank)
|
|
356
|
+
readouts.select do |readout|
|
|
357
|
+
readout.is_a?(Hash) &&
|
|
358
|
+
(readout['unit'].to_s.present? || readout['value'].to_s.present? ||
|
|
359
|
+
readout[:unit].to_s.present? || readout[:value].to_s.present?)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def extract_sample(well)
|
|
364
|
+
# Extract sample information from well
|
|
365
|
+
if well.respond_to?(:sample) && well.sample.present?
|
|
366
|
+
sample = well.sample
|
|
367
|
+
return {
|
|
368
|
+
id: sample.id,
|
|
369
|
+
short_label: sample.short_label,
|
|
370
|
+
conc: sample.try(:molarity_value) || 0
|
|
371
|
+
}
|
|
372
|
+
end
|
|
373
|
+
nil
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def generate_wellplates_metadata(wellplates)
|
|
377
|
+
wellplates.map do |wellplate|
|
|
378
|
+
{
|
|
379
|
+
id: wellplate.id.to_s,
|
|
380
|
+
readoutTitles: extract_readout_titles(wellplate),
|
|
381
|
+
wells: extract_wells(wellplate)
|
|
382
|
+
}
|
|
383
|
+
end
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def extract_element_properties(element)
|
|
387
|
+
props = {
|
|
388
|
+
id: element.id.to_s,
|
|
389
|
+
name: element.name
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
layers = element.properties.dig('layers', 'general_information', 'fields') ||
|
|
393
|
+
element.properties.dig(:layers, :general_information, :fields) || []
|
|
394
|
+
|
|
395
|
+
endpoint_field = layers.find { |f| f['field'] == 'Endpoint' || f[:field] == 'Endpoint' }
|
|
396
|
+
props[:endpoint] = endpoint_field['value'] || endpoint_field[:value] if endpoint_field
|
|
397
|
+
|
|
398
|
+
props
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
def generate_element_metadata(element)
|
|
403
|
+
{
|
|
404
|
+
id: wellplate.id.to_s,
|
|
405
|
+
readoutTitles: extract_readout_titles(wellplate),
|
|
406
|
+
wells: extract_wells(wellplate)
|
|
407
|
+
}
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def generate_json_data(wellplates)
|
|
412
|
+
# # Generate wellplates metadata
|
|
413
|
+
wellplates_metadata = generate_wellplates_metadata(wellplates)
|
|
414
|
+
|
|
415
|
+
# Generate JSON structure
|
|
416
|
+
json_data = {
|
|
417
|
+
id: element.id.to_s,
|
|
418
|
+
request_id: dose_resp_request.id.to_s,
|
|
419
|
+
wellplates: wellplates_metadata
|
|
420
|
+
}
|
|
421
|
+
# Save to JSON file
|
|
422
|
+
filename = "mtt_request_#{element.id}_#{dose_resp_request.id}.json"
|
|
423
|
+
filepath = Rails.root.join('tmp', filename)
|
|
424
|
+
File.write(filepath, JSON.pretty_generate(json_data))
|
|
425
|
+
json_data
|
|
426
|
+
end
|
|
427
|
+
end
|
|
428
|
+
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
|