Module: Labimotion::MttHelpers

Extended by:
Grape::API::Helpers
Defined in:
lib/labimotion/helpers/mtt_helpers.rb

Overview

MTT Helpers

Constant Summary collapse

TPA_EXPIRATION =
72.hours

Instance Method Summary collapse

Instance Method Details

#create_analysis_with_csv(element, user, csv_data, dose_resp_request, output) ⇒ Object



274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 274

def create_analysis_with_csv(element, user, csv_data, dose_resp_request, output)
  analysis, dataset = create_analysis_with_dataset(
    element: element,
    analysis_name: "MTT Analysis #{dose_resp_request.request_id}-#{output&.id}",
    dataset_name: 'new',
    analysis_attributes: {}
  )

  # Determine content type based on file extension
  content_type = case File.extname(csv_data[:filename]).downcase
                 when '.xlsx'
                   'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
                 when '.xls'
                   'application/vnd.ms-excel'
                 when '.csv'
                   'text/csv'
                 else
                   'application/octet-stream'
                 end

  # Create a temporary file for the attachment
  temp_file = Tempfile.new([File.basename(csv_data[:filename], '.*'), File.extname(csv_data[:filename])])
  begin
    temp_file.binmode
    temp_file.write(csv_data[:content])
    temp_file.rewind

    # Create attachment for CSV/Excel file
    attachment = Attachment.new(
      filename: csv_data[:filename],
      file_path: temp_file.path,
      created_by: user.id,
      created_for: user.id,
      attachable_type: 'Container',
      attachable_id: dataset.id,
      content_type: content_type
    )
    attachment.save! if attachment.valid?

    { analysis: analysis, dataset: dataset, attachment: attachment }
  ensure
    temp_file.close
    temp_file.unlink
  end
end

#create_analysis_with_dataset(element:, analysis_name: 'New Analysis', dataset_name: 'New Dataset', analysis_attributes: {}) ⇒ Object



320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 320

def create_analysis_with_dataset(
  element:,
  analysis_name: 'New Analysis',
  dataset_name: 'New Dataset',
  analysis_attributes: {}
)
  # Ensure the element has a root container
  ensure_root_container(element)

  # Get or create the analyses container
  analyses_container = element.container.children.find_or_create_by(container_type: 'analyses')

  # Prepare default extended_metadata for analysis
   = {
    'content' => '{"ops":[{"insert":""}]}',
    'report' => true
  }
   = .merge(analysis_attributes[:extended_metadata] || {})

  # Create the analysis container
  analysis_container = analyses_container.children.create(
    container_type: 'analysis',
    name: analysis_name,
    description: analysis_attributes[:description] || '',
    extended_metadata: 
  )

  # Create the dataset container nested under the analysis
  dataset_container = analysis_container.children.create(
    container_type: 'dataset',
    name: dataset_name,
    description: '',
    extended_metadata: {}
  )

  [analysis_container, dataset_container]
end

#download_json_to_external_appObject



149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 149

def download_json_to_external_app
  # Get token from route params
  token = params[:token]
  dose_resp_request = validate_token(token)

  # Validate user access
  validate_user_access(dose_resp_request)

  # Track access and update state
  # dose_resp_request.track_access!
  dose_resp_request.mark_processing! if dose_resp_request.state == Labimotion::DoseRespRequest::STATE_INITIAL
  # Return wellplates metadata as JSON
  # Access the wellplates array from the metadata structure
  wellplates_data = dose_resp_request.&.dig('wellplates') ||
                    dose_resp_request.&.dig(:wellplates) ||
                    []

  response_data = {
    id: dose_resp_request.id.to_s,
    request_id: dose_resp_request.request_id,
    element_info: extract_element_properties(dose_resp_request.element),
    wellplates: wellplates_data
  }

  status 200
  response_data
rescue => e
  error!("Error: #{e.message}", 500)
end

#ensure_root_container(element) ⇒ Object



358
359
360
361
362
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 358

def ensure_root_container(element)
  return if element.container.present?

  element.container = Container.create_root_container
end

#extract_element_properties(element) ⇒ Object



478
479
480
481
482
483
484
485
486
487
488
489
490
491
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 478

def extract_element_properties(element)
  props = {
    id: element.id.to_s,
    name: element.name
  }

  layers = element.properties.dig('layers', 'general_information', 'fields') ||
           element.properties.dig(:layers, :general_information, :fields) || []

  endpoint_field = layers.find { |f| f['field'] == 'Endpoint' || f[:field] == 'Endpoint' }
  props[:endpoint] = endpoint_field['value'] || endpoint_field[:value] if endpoint_field

  props
end

#extract_readout_titles(wellplate) ⇒ Object

{

  success: response.is_a?(Net::HTTPSuccess),
  status: response.code,
  body: (JSON.parse(response.body) rescue response.body),
  message: response.message
}

rescue StandardError => e

{
  success: false,
  error: e.message,
  backtrace: e.backtrace.first(5)
}

end



394
395
396
397
398
399
400
401
402
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 394

def extract_readout_titles(wellplate)
  # Extract readout titles from wellplate
  if wellplate.respond_to?(:readout_titles)
    titles = wellplate.readout_titles
    return titles if titles.is_a?(Array)
    return JSON.parse(titles) if titles.is_a?(String)
  end
  []
end

#extract_readouts(well) ⇒ Object



434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 434

def extract_readouts(well)
  # Extract readouts from well
  # Readouts are typically stored as JSON data in the well
  readouts = if well.respond_to?(:readouts) && well.readouts.is_a?(Array)
               well.readouts
             elsif well.respond_to?(:readouts) && well.readouts.is_a?(String)
               JSON.parse(well.readouts) rescue []
             elsif well.respond_to?(:readouts) && well.readouts.is_a?(Hash)
               well.readouts.values rescue []
             else
               []
             end

  # Filter out empty readouts (both unit and value are blank)
  readouts.select do |readout|
    readout.is_a?(Hash) &&
    (readout['unit'].to_s.present? || readout['value'].to_s.present? ||
     readout[:unit].to_s.present? || readout[:value].to_s.present?)
  end
end

#extract_sample(well) ⇒ Object



455
456
457
458
459
460
461
462
463
464
465
466
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 455

def extract_sample(well)
  # Extract sample information from well
  if well.respond_to?(:sample) && well.sample.present?
    sample = well.sample
    return {
      id: sample.id,
      short_label: sample.short_label,
      conc: sample.try(:molarity_value) || 0
    }
  end
  nil
end

#extract_wells(wellplate) ⇒ Object



404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 404

def extract_wells(wellplate)
  # Extract wells data from wellplate
  wells = wellplate.wells || []
  wells.map do |well|
    # Position might be stored as a hash or separate fields
    position = if well.respond_to?(:position) && well.position.is_a?(Hash)
                 well.position
               elsif well.respond_to?(:position_x)
                 { x: well.position_x, y: well.position_y }
               else
                 { x: 0, y: 0 }
               end

    well_data = {
      id: well.id,
      position: position
    }

    # Only include readouts if they have values
    readouts = extract_readouts(well)
    well_data[:readouts] = readouts if readouts.present?

    # Only include sample if it exists
    sample = extract_sample(well)
    well_data[:sample] = sample if sample.present?

    well_data
  end
end

#generate_element_metadata(element) ⇒ Object



494
495
496
497
498
499
500
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 494

def (element)
  {
    id: wellplate.id.to_s,
    readoutTitles: extract_readout_titles(wellplate),
    wells: extract_wells(wellplate)
  }
end

#generate_json_data(wellplates) ⇒ Object



503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 503

def generate_json_data(wellplates)
  # # Generate wellplates metadata
   = (wellplates)

  # Generate JSON structure
  json_data = {
    id: element.id.to_s,
    request_id: dose_resp_request.id.to_s,
    wellplates: 
  }
  # Save to JSON file
  filename = "mtt_request_#{element.id}_#{dose_resp_request.id}.json"
  filepath = Rails.root.join('tmp', filename)
  File.write(filepath, JSON.pretty_generate(json_data))
  json_data
end

#generate_wellplates_metadata(wellplates) ⇒ Object



468
469
470
471
472
473
474
475
476
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 468

def (wellplates)
  wellplates.map do |wellplate|
    {
      id: wellplate.id.to_s,
      readoutTitles: extract_readout_titles(wellplate),
      wells: extract_wells(wellplate)
    }
  end
end

#get_external_app_urlObject



24
25
26
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 24

def get_external_app_url
  ENV['MTT_EXTERNAL_APP_URL'] || 'http://localhost:4050'
end

#mtt_output_json(output) ⇒ Object



40
41
42
43
44
45
46
47
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 40

def mtt_output_json(output)
  {
    id: output.id,
    output_data: output.output_data,
    notes: output.notes,
    created_at: output.created_at
  }
end

#mtt_request_json(req, include_outputs: false) ⇒ Object



49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 49

def mtt_request_json(req, include_outputs: false)
  json = {
    id: req.id,
    request_id: req.request_id,
    element_id: req.element_id,
    state: req.state,
    state_name: mtt_state_name(req.state),
    created_at: req.created_at,
    expires_at: req.expires_at,
    expired: req.expired?,
    revoked: req.revoked?,
    active: req.active?,
    resp_message: req.resp_message,
    last_accessed_at: req.last_accessed_at,
    access_count: req.access_count || 0
  }
  json[:outputs] = req.dose_resp_outputs.map { |output| mtt_output_json(output) } if include_outputs
  json
end

#mtt_result_name(node) ⇒ Object

The sample name of a result node, i.e. result.name. Used to match a single result row across both output_data shapes (see below). JSONB columns deserialize with string keys; symbol keys are tolerated defensively.



72
73
74
75
76
77
78
79
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 72

def mtt_result_name(node)
  return nil unless node.is_a?(Hash)

  result = node['result'] || node[:result]
  return nil unless result.is_a?(Array) && result.first.is_a?(Hash)

  result.first['name'] || result.first[:name]
end

#mtt_state_name(state) ⇒ Object

— Serialization helpers (shared across the MTT request endpoints) —



30
31
32
33
34
35
36
37
38
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 30

def mtt_state_name(state)
  case state
  when Labimotion::DoseRespRequest::STATE_ERROR then 'error'
  when Labimotion::DoseRespRequest::STATE_INITIAL then 'initial'
  when Labimotion::DoseRespRequest::STATE_PROCESSING then 'processing'
  when Labimotion::DoseRespRequest::STATE_COMPLETED then 'completed'
  else 'unknown'
  end
end

#process_zip_file(tempfile) ⇒ Object



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 249

def process_zip_file(tempfile)
  wellplates_data = nil
  csv_data = nil

  Zip::File.open(tempfile.path) do |zip_file|
    zip_file.each do |entry|
      if entry.name.end_with?('.json')
        # Read JSON file
        json_content = entry.get_input_stream.read
        json_data = JSON.parse(json_content).with_indifferent_access
        wellplates_data = json_data[:Output]
      elsif entry.name.end_with?('.xls', '.xlsx', '.csv')
        # Read CSV/Excel file - extract just the basename without path
        csv_content = entry.get_input_stream.read
        csv_data = {
          filename: File.basename(entry.name),
          content: csv_content
        }
      end
    end
  end

  [wellplates_data, csv_data]
end

#remove_mtt_result_by_sample_name(output, sample_name) ⇒ Object

Remove a single result row (matched by sample name) from an output’s output_data JSON, supporting both the new (Output[].items) and the legacy (Output[].result) shapes. If the output has no results left afterwards the record is soft-deleted (acts_as_paranoid), consistent with the bulk delete.

Returns { removed:, output_deleted: }.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 87

def remove_mtt_result_by_sample_name(output, sample_name)
  data = output.output_data || {}
  groups = data['Output'] || data[:Output]
  return { removed: false, output_deleted: false } unless groups.is_a?(Array)

  removed = false
  new_groups = groups.map do |group|
    items = group['items'] || group[:items]
    if items.is_a?(Array)
      # New structure: drop the matching item(s) from the group.
      kept = items.reject { |item| mtt_result_name(item) == sample_name }
      removed ||= kept.length != items.length
      kept.empty? ? nil : group.merge('items' => kept)
    elsif mtt_result_name(group) == sample_name
      # Legacy structure: drop the whole group.
      removed = true
      nil
    else
      group
    end
  end.compact

  return { removed: false, output_deleted: false } unless removed

  if new_groups.empty?
    output.destroy
    { removed: true, output_deleted: true }
  else
    output.update!(output_data: data.merge('Output' => new_groups))
    { removed: true, output_deleted: false }
  end
end

#token_url(dose_resp_request) ⇒ Object



15
16
17
18
19
20
21
22
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 15

def token_url(dose_resp_request)
  # Build the callback URL with token in path
  api_base_url = ENV['PUBLIC_URL'] || 'http://172.28.156.100:3000'
  callback_path = "/api/v1/public/mtt_apps/#{dose_resp_request.access_token}"
  callback_url = "#{api_base_url}#{callback_path}"

  callback_url
end

#upload_json_from_external_appObject



179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 179

def upload_json_from_external_app
  # Get token from route params
  token = params[:token]
  dose_resp_request = validate_token(token)

  # Validate user access
  access_info = validate_user_access(dose_resp_request)
  user = access_info[:user]
  element = access_info[:element]
  # Handle file upload
  file_param = params['file'] || params[:file]
  error!('No file uploaded', 400) unless file_param && file_param.is_a?(Hash) && file_param['tempfile']

  tempfile = file_param['tempfile']
  filename = file_param['filename'] || 'upload'
  # Check if it's a zip file
  if filename.end_with?('.zip')
    # Process zip file
    wellplates_data, csv_data = process_zip_file(tempfile)

    error!('Missing JSON data in zip file', 400) unless wellplates_data

    # Create analysis container and dataset with CSV if present
    # if csv_data
    #   create_analysis_with_csv(element, user, csv_data, dose_resp_request, wellplates_data)
    # end
  # else
  #   # Handle single JSON file
  #   file_content = tempfile.read
  #   tempfile.rewind
  #   json_data = JSON.parse(file_content).with_indifferent_access
  #   wellplates_data = json_data[:Output]

  #   error!('Missing wellplates data', 400) unless wellplates_data
  end

  dose_resp_request.track_access!

  # Save output data to dose_resp_outputs table
  output = dose_resp_request.dose_resp_outputs.create!(
    output_data: { Output: wellplates_data }
  )
  if csv_data
    create_analysis_with_csv(element, user, csv_data, dose_resp_request, output)
  end

  dose_resp_request.update!(
    wellplates_metadata: { wellplates: wellplates_data },
    resp_message: 'Data updated successfully'
  )

  # Mark as completed
  dose_resp_request.mark_completed!

  status 200
  {
    success: true,
    message: 'Data updated successfully',
    request_id: dose_resp_request.id
  }
rescue JSON::ParserError => e
  error!("Invalid JSON: #{e.message}", 400)
rescue ActiveRecord::RecordInvalid => e
  dose_resp_request.mark_error!(e.message) if dose_resp_request
  error!("Validation error: #{e.message}", 422)
rescue => e
  dose_resp_request.mark_error!(e.message) if dose_resp_request
  error!("Error: #{e.message}", 500)
end

#validate_token(token) ⇒ Object



120
121
122
123
124
125
126
127
128
129
130
131
132
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 120

def validate_token(token)
  # Find the request by access token
  request = Labimotion::DoseRespRequest.find_by(access_token: token)
  error!('Token not found', 404) unless request

  # Check expiration
  error!('Token expired', 403) if request.expired?

  # Check revocation
  error!('Token revoked', 403) if request.revoked?

  request
end

#validate_user_access(dose_resp_request) ⇒ Object



134
135
136
137
138
139
140
141
142
143
144
145
146
147
# File 'lib/labimotion/helpers/mtt_helpers.rb', line 134

def validate_user_access(dose_resp_request)
  # Get element and user
  element = dose_resp_request.element
  error!('Element not found', 404) unless element

  user = dose_resp_request.creator
  error!('User not found', 404) unless user

  # Check user has update permission on element using ElementPolicy
  policy = ElementPolicy.new(user, element)
  error!('Unauthorized', 403) unless policy.update?

  { element: element, user: user }
end