labimotion 2.2.0.rc5 → 2.2.0.rc7

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: 5aa32269487161d9b1e5647154e1d55ec3fae34dc50082e538adae80679afdf9
4
- data.tar.gz: 5df8ec5d405a7c64ee0840cfffe1dd8a60537997447c898d2f1d8870b3910830
3
+ metadata.gz: a7b53d1daca8b636571715c7ff62caa557dc59082c1f7733cdd6e6a875795712
4
+ data.tar.gz: cab23343b2c90f335571ae7b5b7e714b63cee7e1f403b50b8b3c89e944b23e4a
5
5
  SHA512:
6
- metadata.gz: 68ebbb1d5ebbe512808bcab3f34b3452c9ee0dbeceebbed183db20904e433b0aa06cd533f66a0896e7d3824e615307d25edd669cb1de5c0f0415213c2f349e1f
7
- data.tar.gz: 14a545856a83b9d5778910eefd06c2cb4b865e696036c8e7e1901c18e2688726c6447aca3af8a65703e788d2fc95357acfe1fd5e6177706a3f0dc62b712e885d
6
+ metadata.gz: ffbd3fac0cba826e516a8dd57fed5c0ac0b29a50c15752ca27453cc9dfd28736e71ce44b76efef72690daf1aa23f42a48309228b038a883ac67852564e74c94d
7
+ data.tar.gz: 7a3da058102e765cdfa54fa5ece7e3dc022cff2a0f39e9eae1cc31e4053b8e7735b5eda17bc2c05927b1e54d81883c3a885e1367eb85624d4e8437505ee96191
@@ -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,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ class ElementVariationAPI < Grape::API
5
+ rescue_from ActiveRecord::RecordNotFound do
6
+ error!('404 Element not found', 404)
7
+ end
8
+
9
+ resource :element_variations do
10
+ params do
11
+ requires :element_id, type: Integer, desc: 'Generic element id'
12
+ end
13
+ route_param :element_id do
14
+ before do
15
+ @element = Labimotion::Element.find(params[:element_id])
16
+ error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, @element).read?
17
+ end
18
+
19
+ desc 'Return element variations for a generic element'
20
+ get do
21
+ record = Labimotion::ElementVariation.find_or_initialize_by(element_id: @element.id)
22
+ present record, with: Labimotion::ElementVariationEntity, root: 'element_variation'
23
+ end
24
+
25
+ desc 'Upsert element variations for a generic element'
26
+ params do
27
+ requires :variations, type: Hash, desc: 'Variations keyed by row uuid'
28
+ optional :layout, type: Hash, desc: 'Column layout (selected/order/units/rowOrder)'
29
+ end
30
+ put do
31
+ error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, @element).update?
32
+
33
+ record = Labimotion::ElementVariation.find_or_initialize_by(element_id: @element.id)
34
+ record.variations = params[:variations] || {}
35
+ if params.key?(:layout) && record.class.column_names.include?('layout')
36
+ record.layout = params[:layout] || {}
37
+ end
38
+ record.save!
39
+
40
+ present record, with: Labimotion::ElementVariationEntity, root: 'element_variation'
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -12,5 +12,8 @@ module Labimotion
12
12
  mount Labimotion::LabimotionHubAPI
13
13
  mount Labimotion::StandardLayerAPI
14
14
  mount Labimotion::VocabularyAPI
15
+ mount Labimotion::MttAPI
16
+ mount Labimotion::DoseRespRequestAPI
17
+ mount Labimotion::ElementVariationAPI
15
18
  end
16
19
  end
@@ -0,0 +1,238 @@
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
+
74
+ desc 'Delete one or multiple MTT requests'
75
+ params do
76
+ requires :ids, type: Array[Integer], desc: 'Array of request IDs to delete'
77
+ end
78
+ delete do
79
+ # Find requests belonging to current user
80
+ requests = Labimotion::DoseRespRequest.where(
81
+ id: params[:ids],
82
+ created_by: current_user.id
83
+ )
84
+
85
+ if requests.empty?
86
+ error!('No requests found or unauthorized', 404)
87
+ end
88
+
89
+ deleted_count = requests.count
90
+ requests.destroy_all
91
+
92
+ {
93
+ success: true,
94
+ message: "Successfully deleted #{deleted_count} request(s)",
95
+ deleted_count: deleted_count
96
+ }
97
+ rescue StandardError => e
98
+ error!("Error deleting requests: #{e.message}", 500)
99
+ end
100
+
101
+ desc 'Update MTT request'
102
+ params do
103
+ requires :id, type: Integer, desc: 'Request ID'
104
+ optional :state, type: Integer, desc: 'State (-1: error, 0: initial, 1: processing, 2: completed)', values: [-1, 0, 1, 2]
105
+ optional :resp_message, type: String, desc: 'Response message'
106
+ optional :wellplates_metadata, type: Hash, desc: 'Wellplates metadata'
107
+ optional :input_metadata, type: Hash, desc: 'Input metadata'
108
+ optional :revoked, type: Boolean, desc: 'Revoke access token'
109
+ end
110
+ patch ':id' do
111
+ # Find request belonging to current user
112
+ request = Labimotion::DoseRespRequest.find_by(
113
+ id: params[:id],
114
+ created_by: current_user.id
115
+ )
116
+
117
+ error!('Request not found or unauthorized', 404) unless request
118
+
119
+ # Prepare update attributes
120
+ update_attrs = {}
121
+ update_attrs[:state] = params[:state] if params.key?(:state)
122
+ update_attrs[:resp_message] = params[:resp_message] if params.key?(:resp_message)
123
+ update_attrs[:wellplates_metadata] = params[:wellplates_metadata] if params.key?(:wellplates_metadata)
124
+ update_attrs[:input_metadata] = params[:input_metadata] if params.key?(:input_metadata)
125
+ update_attrs[:revoked_at] = params[:revoked] ? Time.current : nil if params.key?(:revoked)
126
+
127
+ if request.update(update_attrs)
128
+ {
129
+ success: true,
130
+ message: 'Request updated successfully',
131
+ request: {
132
+ id: request.id,
133
+ request_id: request.request_id,
134
+ 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,
142
+ resp_message: request.resp_message,
143
+ revoked: request.revoked?,
144
+ updated_at: request.updated_at
145
+ }
146
+ }
147
+ else
148
+ error!("Validation error: #{request.errors.full_messages.join(', ')}", 422)
149
+ end
150
+ rescue ActiveRecord::RecordNotFound
151
+ error!('Request not found', 404)
152
+ rescue StandardError => e
153
+ error!("Error updating request: #{e.message}", 500)
154
+ end
155
+ end
156
+
157
+ namespace :outputs do
158
+ desc 'Delete one or multiple MTT outputs'
159
+ params do
160
+ requires :ids, type: Array[Integer], desc: 'Array of output IDs to delete'
161
+ end
162
+ delete do
163
+ # Find outputs where the parent request belongs to current user
164
+ outputs = Labimotion::DoseRespOutput
165
+ .joins(:dose_resp_request)
166
+ .where(
167
+ id: params[:ids],
168
+ dose_resp_requests: { created_by: current_user.id }
169
+ )
170
+
171
+ if outputs.empty?
172
+ error!('No outputs found or unauthorized', 404)
173
+ end
174
+
175
+ deleted_count = outputs.count
176
+ outputs.destroy_all
177
+
178
+ {
179
+ success: true,
180
+ message: "Successfully deleted #{deleted_count} output(s)",
181
+ deleted_count: deleted_count
182
+ }
183
+ rescue StandardError => e
184
+ error!("Error deleting outputs: #{e.message}", 500)
185
+ end
186
+ end
187
+
188
+ namespace :create_mtt_request do
189
+ desc 'Create MTT assay request'
190
+ params do
191
+ use :create_mtt_request_params
192
+ end
193
+ post do
194
+ # Find element and wellplates
195
+ element = Labimotion::Element.find_by(id: params[:id])
196
+ error!('Element not found', 404) unless element
197
+ #byebug
198
+ # Verify user has update permission
199
+ error!('Unauthorized', 403) unless ElementPolicy.new(current_user, element).update?
200
+
201
+ wellplates = Wellplate.where(id: params[:wellplate_ids])
202
+ error!('No wellplates found', 404) if wellplates.empty?
203
+
204
+ # Generate wellplates metadata
205
+ wellplates_metadata = generate_wellplates_metadata(wellplates)
206
+
207
+ # Create DoseRespRequest record with token
208
+ dose_resp_request = Labimotion::DoseRespRequest.create!(
209
+ element_id: element.id,
210
+ wellplates_metadata: { wellplates: wellplates_metadata },
211
+ input_metadata: {
212
+ wellplate_ids: params[:wellplate_ids],
213
+ element_id: element.id,
214
+ element_name: element.name,
215
+ created_at: Time.current
216
+ },
217
+ state: Labimotion::DoseRespRequest::STATE_INITIAL,
218
+ created_by: current_user&.id,
219
+ expires_at: TPA_EXPIRATION.from_now
220
+ )
221
+
222
+ # Generate external app URL with token
223
+ token_uri = token_url(dose_resp_request)
224
+ external_app_url = get_external_app_url
225
+
226
+ # Format: "#{@app.url}?url=#{CGI.escape(token_uri)}&type=ThirdPartyApp"
227
+ "#{external_app_url}?method=DoseResponse&url=#{CGI.escape(token_uri)}"
228
+ rescue ActiveRecord::RecordInvalid => e
229
+ error!("Validation error: #{e.message}", 422)
230
+ rescue ActiveRecord::RecordNotFound => e
231
+ error!("Not found: #{e.message}", 404)
232
+ rescue StandardError => e
233
+ error!("Error: #{e.message}", 500)
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -23,6 +23,7 @@ module Labimotion
23
23
  expose! :uuid
24
24
  expose! :user_labels
25
25
  expose! :preview_attachment # align with eln change
26
+ expose! :variations_count
26
27
  end
27
28
 
28
29
  with_options(anonymize_below: 10) do
@@ -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
@@ -247,21 +247,27 @@ module Labimotion
247
247
  end
248
248
 
249
249
  def list_serialized_elements(params, current_user)
250
- scope = Labimotion::Element.none
251
-
250
+ collection_id =
252
251
  if params[:collection_id]
253
- begin
254
- collection = Collection.accessible_for(current_user).find(params[:collection_id])
255
- scope = collection.elements
256
- .joins(:element_klass)
257
- .where(element_klasses: { name: params[:el_type] })
258
- .includes(:tag)
259
- rescue ActiveRecord::RecordNotFound
260
- Labimotion::Element.none
261
- end
252
+ Collection
253
+ .belongs_to_or_shared_by(current_user.id, current_user.group_ids)
254
+ .find_by(id: params[:collection_id])&.id
255
+ elsif params[:sync_collection_id]
256
+ current_user
257
+ .all_sync_in_collections_users
258
+ .find_by(id: params[:sync_collection_id])&.collection&.id
259
+ end
260
+
261
+ scope =
262
+ if collection_id
263
+ Labimotion::Element
264
+ .joins(:element_klass, :collections_elements)
265
+ .where(
266
+ element_klasses: { name: params[:el_type] },
267
+ collections_elements: { collection_id: collection_id },
268
+ ).includes(:tag, collections: :sync_collections_users)
262
269
  else
263
- # All collection of current_user
264
- scope = Labimotion::Element.for_user(current_user.id)
270
+ Labimotion::Element.none
265
271
  end
266
272
 
267
273
  ## TO DO: refactor labimotion
@@ -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
@@ -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
@@ -41,6 +41,12 @@ module Labimotion
41
41
  has_many :samples, through: :elements_samples, source: :sample
42
42
  has_one :container, :as => :containable
43
43
  has_many :elements_revisions, dependent: :destroy, class_name: 'Labimotion::ElementsRevision'
44
+ has_one :element_variation, dependent: :destroy, class_name: 'Labimotion::ElementVariation', foreign_key: :element_id
45
+
46
+ def variations_count
47
+ rows = element_variation&.variations
48
+ rows.is_a?(Hash) ? rows.size : 0
49
+ end
44
50
 
45
51
  accepts_nested_attributes_for :collections_elements
46
52
 
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ class ElementVariation < ApplicationRecord
5
+ self.table_name = :element_variations
6
+
7
+ belongs_to :element, class_name: 'Labimotion::Element'
8
+
9
+ validates :element_id, uniqueness: true
10
+
11
+ def variations_hash
12
+ variations.is_a?(Hash) ? variations : {}
13
+ end
14
+
15
+ def layout_hash
16
+ return {} unless self.class.column_names.include?('layout')
17
+
18
+ layout.is_a?(Hash) ? layout : {}
19
+ end
20
+ end
21
+ end
@@ -12,7 +12,7 @@ module Labimotion
12
12
  end
13
13
 
14
14
  def self.elements_search(params, current_user, c_id, dl)
15
- collection = Collection.accessible_for(current_user).find(c_id)
15
+ collection = Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids).find(c_id)
16
16
  element_scope = Labimotion::Element.joins(:collections_elements).where('collections_elements.collection_id = ?', collection.id).joins(:element_klass).where('element_klasses.id = elements.element_klass_id AND element_klasses.name = ?', params[:selection][:genericElName])
17
17
  element_scope = element_scope.where('elements.name like (?)', "%#{params[:selection][:searchName]}%") if params[:selection][:searchName].present?
18
18
  element_scope = element_scope.where('elements.short_label like (?)', "%#{params[:selection][:searchShowLabel]}%") if params[:selection][:searchShowLabel].present?
@@ -97,7 +97,7 @@ module Labimotion
97
97
  def self.samples_search(c_id = @c_id)
98
98
  sqls = []
99
99
  sps = params[:selection][:searchProperties]
100
- collection = Collection.accessible_for(current_user).find(c_id)
100
+ collection = Collection.belongs_to_or_shared_by(current_user.id, current_user.group_ids).find(c_id)
101
101
  element_scope = Sample.joins(:collections_samples).where('collections_samples.collection_id = ?', collection.id)
102
102
  return element_scope if sps.empty?
103
103
 
@@ -2,5 +2,5 @@
2
2
 
3
3
  ## Labimotion Version
4
4
  module Labimotion
5
- VERSION = '2.2.0.rc5'
5
+ VERSION = '2.2.0.rc7'
6
6
  end
data/lib/labimotion.rb CHANGED
@@ -27,6 +27,9 @@ 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'
32
+ autoload :ElementVariationAPI, 'labimotion/apis/element_variation_api'
30
33
 
31
34
  ######## Entities
32
35
  autoload :PropertiesEntity, 'labimotion/entities/properties_entity'
@@ -49,6 +52,7 @@ module Labimotion
49
52
  autoload :SegmentRevisionEntity, 'labimotion/entities/segment_revision_entity'
50
53
  ## autoload :DatasetRevisionEntity, 'labimotion/entities/dataset_revision_entity'
51
54
  autoload :VocabularyEntity, 'labimotion/entities/vocabulary_entity'
55
+ autoload :ElementVariationEntity, 'labimotion/entities/element_variation_entity'
52
56
 
53
57
  ######## Helpers
54
58
  autoload :GenericHelpers, 'labimotion/helpers/generic_helpers'
@@ -114,11 +118,14 @@ module Labimotion
114
118
  autoload :StdLayersRevision, 'labimotion/models/std_layers_revision'
115
119
 
116
120
  autoload :DeviceDescription, 'labimotion/models/device_description'
121
+ autoload :DoseRespRequest, 'labimotion/models/dose_resp_request'
122
+ autoload :DoseRespOutput, 'labimotion/models/dose_resp_output'
117
123
  autoload :Reaction, 'labimotion/models/reaction'
118
124
  autoload :ResearchPlan, 'labimotion/models/research_plan'
119
125
  autoload :Sample, 'labimotion/models/sample'
120
126
  autoload :Screen, 'labimotion/models/screen'
121
127
  autoload :Wellplate, 'labimotion/models/wellplate'
128
+ autoload :ElementVariation, 'labimotion/models/element_variation'
122
129
 
123
130
  ######## Models/Concerns
124
131
  autoload :GenericKlassRevisions, 'labimotion/models/concerns/generic_klass_revisions'
@@ -126,6 +133,6 @@ module Labimotion
126
133
  autoload :ElementFetchable, 'labimotion/models/concerns/element_fetchable'
127
134
  autoload :Segmentable, 'labimotion/models/concerns/segmentable'
128
135
  autoload :Datasetable, 'labimotion/models/concerns/datasetable'
129
- autoload :AttachmentConverter, 'labimotion/models/concerns/attachment_converter.rb'
136
+ autoload :AttachmentConverter, 'labimotion/models/concerns/attachment_converter'
130
137
  autoload :LinkedProperties, 'labimotion/models/concerns/linked_properties'
131
138
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: labimotion
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.2.0.rc5
4
+ version: 2.2.0.rc7
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-03-25 00:00:00.000000000 Z
12
+ date: 2026-05-04 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx
@@ -55,12 +55,15 @@ 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
59
+ - lib/labimotion/apis/element_variation_api.rb
58
60
  - lib/labimotion/apis/exporter_api.rb
59
61
  - lib/labimotion/apis/generic_dataset_api.rb
60
62
  - lib/labimotion/apis/generic_element_api.rb
61
63
  - lib/labimotion/apis/generic_klass_api.rb
62
64
  - lib/labimotion/apis/labimotion_api.rb
63
65
  - lib/labimotion/apis/labimotion_hub_api.rb
66
+ - lib/labimotion/apis/mtt_api.rb
64
67
  - lib/labimotion/apis/segment_api.rb
65
68
  - lib/labimotion/apis/standard_api.rb
66
69
  - lib/labimotion/apis/standard_layer_api.rb
@@ -75,6 +78,7 @@ files:
75
78
  - lib/labimotion/entities/element_entity.rb
76
79
  - lib/labimotion/entities/element_klass_entity.rb
77
80
  - lib/labimotion/entities/element_revision_entity.rb
81
+ - lib/labimotion/entities/element_variation_entity.rb
78
82
  - lib/labimotion/entities/eln_element_entity.rb
79
83
  - lib/labimotion/entities/generic_entity.rb
80
84
  - lib/labimotion/entities/generic_klass_entity.rb
@@ -90,6 +94,7 @@ files:
90
94
  - lib/labimotion/helpers/element_helpers.rb
91
95
  - lib/labimotion/helpers/exporter_helpers.rb
92
96
  - lib/labimotion/helpers/generic_helpers.rb
97
+ - lib/labimotion/helpers/mtt_helpers.rb
93
98
  - lib/labimotion/helpers/param_helpers.rb
94
99
  - lib/labimotion/helpers/repository_helpers.rb
95
100
  - lib/labimotion/helpers/sample_association_helpers.rb
@@ -129,9 +134,12 @@ files:
129
134
  - lib/labimotion/models/dataset_klasses_revision.rb
130
135
  - lib/labimotion/models/datasets_revision.rb
131
136
  - lib/labimotion/models/device_description.rb
137
+ - lib/labimotion/models/dose_resp_output.rb
138
+ - lib/labimotion/models/dose_resp_request.rb
132
139
  - lib/labimotion/models/element.rb
133
140
  - lib/labimotion/models/element_klass.rb
134
141
  - lib/labimotion/models/element_klasses_revision.rb
142
+ - lib/labimotion/models/element_variation.rb
135
143
  - lib/labimotion/models/elements_element.rb
136
144
  - lib/labimotion/models/elements_revision.rb
137
145
  - lib/labimotion/models/elements_sample.rb