labimotion 2.2.0.rc11 → 2.2.0.rc13

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 491f3e3075c83954b5e49caeb8b091d1aba68ddf0135fe82ddbaf6aeb4b244e9
4
- data.tar.gz: ae5dc4a93ecb31559feff1060f8fb449aa3e618f86890ea25324410226f4d166
3
+ metadata.gz: 0ec9d0808468d0b4447f577e3932d0e421c248020df87eeee277a5242fc3a0e6
4
+ data.tar.gz: de0e10512eb0ce1e0bb72fc60983119637c869e43816aed20c0f88419a474d37
5
5
  SHA512:
6
- metadata.gz: ce1f326748ee1ae7c9a71f97f43b2eef7d3273c28c9d7d7d3e87516cfcef58002927563dedbaad66ca87f6c396fd462cab6f827eb95d052cdf4f2703bfe8e307
7
- data.tar.gz: 958083e170c64e0c378b38ecf9996343a88dfa6756617d0a7d6d9e548d3dbefc3b53c00eda2ec8412fd6da59ca3b959b7d0fa20f67e32a559fa74da9d01e695b
6
+ metadata.gz: eb60f1b75b504783636051ee83553e44b8edd71c03cf42abdfd8187405bd53ab150d5a136c7321a4a29b3a6a947aa397fba77b2ee8df180674a61f22b3c08ed5
7
+ data.tar.gz: 8cbad024d4e4b12275eb2fe938fcfcbf8540ab85f1d03cd6d86059ed4aa51bc5c2c555aa1c14e8a823031d74c6107f133fe302c5cea522ef901b8a1e305a97e9
@@ -14,7 +14,6 @@ module Labimotion
14
14
  mount Labimotion::VocabularyAPI
15
15
  mount Labimotion::UserAPI
16
16
  mount Labimotion::MttAPI
17
- mount Labimotion::DoseRespRequestAPI
18
17
  mount Labimotion::ElementVariationAPI
19
18
  mount Labimotion::LabimotionDoiAPI
20
19
  mount Labimotion::LabimotionTemplateBrowseAPI
@@ -38,37 +38,7 @@ module Labimotion
38
38
  .order(created_at: :desc)
39
39
 
40
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
41
+ requests.map { |req| mtt_request_json(req, include_outputs: true) }
72
42
  end
73
43
 
74
44
  desc 'Delete one or multiple MTT requests'
@@ -132,13 +102,7 @@ module Labimotion
132
102
  id: request.id,
133
103
  request_id: request.request_id,
134
104
  state: request.state,
135
- state_name: case request.state
136
- when Labimotion::DoseRespRequest::STATE_ERROR then 'error'
137
- when Labimotion::DoseRespRequest::STATE_INITIAL then 'initial'
138
- when Labimotion::DoseRespRequest::STATE_PROCESSING then 'processing'
139
- when Labimotion::DoseRespRequest::STATE_COMPLETED then 'completed'
140
- else 'unknown'
141
- end,
105
+ state_name: mtt_state_name(request.state),
142
106
  resp_message: request.resp_message,
143
107
  revoked: request.revoked?,
144
108
  updated_at: request.updated_at
@@ -183,6 +147,34 @@ module Labimotion
183
147
  rescue StandardError => e
184
148
  error!("Error deleting outputs: #{e.message}", 500)
185
149
  end
150
+
151
+ desc 'Delete a single result (by sample name) from an output'
152
+ params do
153
+ requires :id, type: Integer, desc: 'Output ID'
154
+ requires :sample_name, type: String, desc: 'Sample name (result[].name) to remove'
155
+ end
156
+ delete ':id/results' do
157
+ # Only operate on outputs whose parent request belongs to the current user
158
+ output = Labimotion::DoseRespOutput
159
+ .joins(:dose_resp_request)
160
+ .where(dose_resp_requests: { created_by: current_user.id })
161
+ .find_by(id: params[:id])
162
+
163
+ error!('Output not found or unauthorized', 404) unless output
164
+
165
+ outcome = remove_mtt_result_by_sample_name(output, params[:sample_name])
166
+ error!('Result not found in output', 404) unless outcome[:removed]
167
+
168
+ {
169
+ success: true,
170
+ message: "Removed result '#{params[:sample_name]}' from output #{params[:id]}",
171
+ output_id: params[:id],
172
+ output_deleted: outcome[:output_deleted],
173
+ output: outcome[:output_deleted] ? nil : mtt_output_json(output)
174
+ }
175
+ rescue StandardError => e
176
+ error!("Error deleting result: #{e.message}", 500)
177
+ end
186
178
  end
187
179
 
188
180
  namespace :create_mtt_request do
@@ -194,7 +186,7 @@ module Labimotion
194
186
  # Find element and wellplates
195
187
  element = Labimotion::Element.find_by(id: params[:id])
196
188
  error!('Element not found', 404) unless element
197
- #byebug
189
+
198
190
  # Verify user has update permission
199
191
  error!('Unauthorized', 403) unless ElementPolicy.new(current_user, element).update?
200
192
 
@@ -25,6 +25,98 @@ module Labimotion
25
25
  ENV['MTT_EXTERNAL_APP_URL'] || 'http://localhost:4050'
26
26
  end
27
27
 
28
+ # --- Serialization helpers (shared across the MTT request endpoints) ---
29
+
30
+ def mtt_state_name(state)
31
+ case state
32
+ when Labimotion::DoseRespRequest::STATE_ERROR then 'error'
33
+ when Labimotion::DoseRespRequest::STATE_INITIAL then 'initial'
34
+ when Labimotion::DoseRespRequest::STATE_PROCESSING then 'processing'
35
+ when Labimotion::DoseRespRequest::STATE_COMPLETED then 'completed'
36
+ else 'unknown'
37
+ end
38
+ end
39
+
40
+ def mtt_output_json(output)
41
+ {
42
+ id: output.id,
43
+ output_data: output.output_data,
44
+ notes: output.notes,
45
+ created_at: output.created_at
46
+ }
47
+ end
48
+
49
+ def mtt_request_json(req, include_outputs: false)
50
+ json = {
51
+ id: req.id,
52
+ request_id: req.request_id,
53
+ element_id: req.element_id,
54
+ state: req.state,
55
+ state_name: mtt_state_name(req.state),
56
+ created_at: req.created_at,
57
+ expires_at: req.expires_at,
58
+ expired: req.expired?,
59
+ revoked: req.revoked?,
60
+ active: req.active?,
61
+ resp_message: req.resp_message,
62
+ last_accessed_at: req.last_accessed_at,
63
+ access_count: req.access_count || 0
64
+ }
65
+ json[:outputs] = req.dose_resp_outputs.map { |output| mtt_output_json(output) } if include_outputs
66
+ json
67
+ end
68
+
69
+ # The sample name of a result node, i.e. result[0].name. Used to match a
70
+ # single result row across both output_data shapes (see below). JSONB columns
71
+ # deserialize with string keys; symbol keys are tolerated defensively.
72
+ def mtt_result_name(node)
73
+ return nil unless node.is_a?(Hash)
74
+
75
+ result = node['result'] || node[:result]
76
+ return nil unless result.is_a?(Array) && result.first.is_a?(Hash)
77
+
78
+ result.first['name'] || result.first[:name]
79
+ end
80
+
81
+ # Remove a single result row (matched by sample name) from an output's
82
+ # output_data JSON, supporting both the new (Output[].items[]) and the legacy
83
+ # (Output[].result[]) shapes. If the output has no results left afterwards the
84
+ # record is soft-deleted (acts_as_paranoid), consistent with the bulk delete.
85
+ #
86
+ # Returns { removed:, output_deleted: }.
87
+ def remove_mtt_result_by_sample_name(output, sample_name)
88
+ data = output.output_data || {}
89
+ groups = data['Output'] || data[:Output]
90
+ return { removed: false, output_deleted: false } unless groups.is_a?(Array)
91
+
92
+ removed = false
93
+ new_groups = groups.map do |group|
94
+ items = group['items'] || group[:items]
95
+ if items.is_a?(Array)
96
+ # New structure: drop the matching item(s) from the group.
97
+ kept = items.reject { |item| mtt_result_name(item) == sample_name }
98
+ removed ||= kept.length != items.length
99
+ kept.empty? ? nil : group.merge('items' => kept)
100
+ elsif mtt_result_name(group) == sample_name
101
+ # Legacy structure: drop the whole group.
102
+ removed = true
103
+ nil
104
+ else
105
+ group
106
+ end
107
+ end.compact
108
+
109
+ return { removed: false, output_deleted: false } unless removed
110
+
111
+ if new_groups.empty?
112
+ output.destroy
113
+ { removed: true, output_deleted: true }
114
+ else
115
+ output.update!(output_data: data.merge('Output' => new_groups))
116
+ { removed: true, output_deleted: false }
117
+ end
118
+ end
119
+
28
120
  def validate_token(token)
29
121
  # Find the request by access token
30
122
  request = Labimotion::DoseRespRequest.find_by(access_token: token)
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.2.0.rc11'
5
+ VERSION = '2.2.0.rc13'
6
6
  end
data/lib/labimotion.rb CHANGED
@@ -29,7 +29,6 @@ module Labimotion
29
29
  autoload :VocabularyAPI, 'labimotion/apis/vocabulary_api'
30
30
  autoload :UserAPI, 'labimotion/apis/user_api'
31
31
  autoload :MttAPI, 'labimotion/apis/mtt_api'
32
- autoload :DoseRespRequestAPI, 'labimotion/apis/dose_resp_request_api'
33
32
  autoload :ElementVariationAPI, 'labimotion/apis/element_variation_api'
34
33
  autoload :LabimotionDoiAPI, 'labimotion/apis/labimotion_doi_api'
35
34
  autoload :LabimotionTemplateBrowseAPI, 'labimotion/apis/labimotion_template_browse_api'
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.rc11
4
+ version: 2.2.0.rc13
5
5
  platform: ruby
6
6
  authors:
7
7
  - Chia-Lin Lin
@@ -58,7 +58,6 @@ files:
58
58
  - README.md
59
59
  - lib/labimotion.rb
60
60
  - lib/labimotion/apis/converter_api.rb
61
- - lib/labimotion/apis/dose_resp_request_api.rb
62
61
  - lib/labimotion/apis/element_variation_api.rb
63
62
  - lib/labimotion/apis/exporter_api.rb
64
63
  - lib/labimotion/apis/generic_dataset_api.rb
@@ -1,241 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'labimotion/version'
4
-
5
- module Labimotion
6
- # Dose Response Request API
7
- class DoseRespRequestAPI < Grape::API
8
- helpers Labimotion::ParamHelpers
9
-
10
- resource :dose_resp_requests do
11
- desc 'Get all dose response requests for current user'
12
- params do
13
- optional :element_id, type: Integer, desc: 'Filter by element ID'
14
- optional :state, type: Integer, desc: 'Filter by state (-1, 0, 1, 2)'
15
- optional :page, type: Integer, desc: 'Page number', default: 1
16
- optional :per_page, type: Integer, desc: 'Items per page', default: 20
17
- end
18
- get do
19
- requests = Labimotion::DoseRespRequest.where(created_by: current_user.id)
20
-
21
- # Apply filters
22
- requests = requests.where(element_id: params[:element_id]) if params[:element_id]
23
- requests = requests.where(state: params[:state]) if params[:state]
24
-
25
- # Pagination
26
- page = params[:page] || 1
27
- per_page = [params[:per_page] || 20, 100].min # Max 100 per page
28
-
29
- total = requests.count
30
- requests = requests.order(created_at: :desc)
31
- .offset((page - 1) * per_page)
32
- .limit(per_page)
33
-
34
- {
35
- requests: requests.map do |req|
36
- {
37
- id: req.id,
38
- request_id: req.request_id,
39
- element_id: req.element_id,
40
- state: req.state,
41
- state_label: state_label(req.state),
42
- expires_at: req.expires_at,
43
- created_at: req.created_at,
44
- updated_at: req.updated_at,
45
- first_accessed_at: req.first_accessed_at,
46
- last_accessed_at: req.last_accessed_at,
47
- access_count: req.access_count,
48
- resp_message: req.resp_message,
49
- active: req.active?
50
- }
51
- end,
52
- pagination: {
53
- page: page,
54
- per_page: per_page,
55
- total: total,
56
- total_pages: (total.to_f / per_page).ceil
57
- }
58
- }
59
- rescue StandardError => e
60
- error!("Error: #{e.message}", 500)
61
- end
62
-
63
- desc 'Get a dose response request by ID'
64
- params do
65
- requires :id, type: Integer, desc: 'Request ID'
66
- end
67
- get ':id' do
68
- request = Labimotion::DoseRespRequest.find_by(id: params[:id])
69
- error!('Request not found', 404) unless request
70
-
71
- # Check authorization
72
- error!('Unauthorized', 403) unless request.created_by == current_user.id
73
-
74
- {
75
- id: request.id,
76
- request_id: request.request_id,
77
- element_id: request.element_id,
78
- state: request.state,
79
- state_label: state_label(request.state),
80
- wellplates_metadata: request.wellplates_metadata,
81
- input_metadata: request.input_metadata,
82
- expires_at: request.expires_at,
83
- revoked_at: request.revoked_at,
84
- created_at: request.created_at,
85
- updated_at: request.updated_at,
86
- first_accessed_at: request.first_accessed_at,
87
- last_accessed_at: request.last_accessed_at,
88
- access_count: request.access_count,
89
- resp_message: request.resp_message,
90
- active: request.active?,
91
- expired: request.expired?,
92
- revoked: request.revoked?
93
- }
94
- rescue StandardError => e
95
- error!("Error: #{e.message}", 500)
96
- end
97
-
98
- desc 'Update a dose response request'
99
- params do
100
- requires :id, type: Integer, desc: 'Request ID'
101
- optional :state, type: Integer, desc: 'State', values: [-1, 0, 1, 2]
102
- optional :resp_message, type: String, desc: 'Response message'
103
- optional :wellplates_metadata, type: Hash, desc: 'Wellplates metadata'
104
- end
105
- put ':id' do
106
- request = Labimotion::DoseRespRequest.find_by(id: params[:id])
107
- error!('Request not found', 404) unless request
108
-
109
- # Check authorization
110
- error!('Unauthorized', 403) unless request.created_by == current_user.id
111
-
112
- update_params = {}
113
- update_params[:state] = params[:state] if params[:state]
114
- update_params[:resp_message] = params[:resp_message] if params[:resp_message]
115
- update_params[:wellplates_metadata] = params[:wellplates_metadata] if params[:wellplates_metadata]
116
-
117
- request.update!(update_params)
118
-
119
- {
120
- success: true,
121
- message: 'Request updated successfully',
122
- request: {
123
- id: request.id,
124
- request_id: request.request_id,
125
- state: request.state,
126
- state_label: state_label(request.state),
127
- resp_message: request.resp_message,
128
- updated_at: request.updated_at
129
- }
130
- }
131
- rescue ActiveRecord::RecordInvalid => e
132
- error!("Validation error: #{e.message}", 422)
133
- rescue StandardError => e
134
- error!("Error: #{e.message}", 500)
135
- end
136
-
137
- desc 'Revoke a dose response request'
138
- params do
139
- requires :id, type: Integer, desc: 'Request ID'
140
- end
141
- post ':id/revoke' do
142
- request = Labimotion::DoseRespRequest.find_by(id: params[:id])
143
- error!('Request not found', 404) unless request
144
-
145
- # Check authorization
146
- error!('Unauthorized', 403) unless request.created_by == current_user.id
147
-
148
- request.revoke!
149
-
150
- {
151
- success: true,
152
- message: 'Request revoked successfully',
153
- request: {
154
- id: request.id,
155
- request_id: request.request_id,
156
- state: request.state,
157
- revoked_at: request.revoked_at,
158
- active: request.active?
159
- }
160
- }
161
- rescue StandardError => e
162
- error!("Error: #{e.message}", 500)
163
- end
164
-
165
- desc 'Delete a dose response request'
166
- params do
167
- requires :id, type: Integer, desc: 'Request ID'
168
- end
169
- delete ':id' do
170
- request = Labimotion::DoseRespRequest.find_by(id: params[:id])
171
- error!('Request not found', 404) unless request
172
-
173
- # Check authorization
174
- error!('Unauthorized', 403) unless request.created_by == current_user.id
175
-
176
- # Soft delete if acts_as_paranoid is enabled
177
- request.destroy
178
-
179
- {
180
- success: true,
181
- message: 'Request deleted successfully'
182
- }
183
- rescue StandardError => e
184
- error!("Error: #{e.message}", 500)
185
- end
186
-
187
- desc 'Get dose response requests by element ID'
188
- params do
189
- requires :element_id, type: Integer, desc: 'Element ID'
190
- end
191
- get 'by_element/:element_id' do
192
- element = Labimotion::Element.find_by(id: params[:element_id])
193
- error!('Element not found', 404) unless element
194
-
195
- # Check if user has access to element
196
- policy = ElementPolicy.new(current_user, element)
197
- error!('Unauthorized', 403) unless policy.read?
198
-
199
- requests = Labimotion::DoseRespRequest.where(element_id: params[:element_id])
200
- .where(created_by: current_user.id)
201
- .order(created_at: :desc)
202
-
203
- {
204
- element_id: element.id,
205
- element_name: element.name,
206
- requests: requests.map do |req|
207
- {
208
- id: req.id,
209
- request_id: req.request_id,
210
- state: req.state,
211
- state_label: state_label(req.state),
212
- expires_at: req.expires_at,
213
- created_at: req.created_at,
214
- access_count: req.access_count,
215
- active: req.active?
216
- }
217
- end
218
- }
219
- rescue StandardError => e
220
- error!("Error: #{e.message}", 500)
221
- end
222
- end
223
-
224
- helpers do
225
- def state_label(state)
226
- case state
227
- when Labimotion::DoseRespRequest::STATE_ERROR
228
- 'error'
229
- when Labimotion::DoseRespRequest::STATE_INITIAL
230
- 'initial'
231
- when Labimotion::DoseRespRequest::STATE_PROCESSING
232
- 'processing'
233
- when Labimotion::DoseRespRequest::STATE_COMPLETED
234
- 'completed'
235
- else
236
- 'unknown'
237
- end
238
- end
239
- end
240
- end
241
- end