labimotion 2.2.0.rc16 → 2.2.0.rc18

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: 53d62c152440c37c962f7a989735fc000b5f9125e1fa47722a4083965f352aee
4
- data.tar.gz: 30825b940e18998848773fec2d53c8e8239515cf8f40cc0f9ca18d77e70aecb8
3
+ metadata.gz: ac9e13a86048207de58add4ce3c2670dd3d3ece7b334103a4b8afef774309bd7
4
+ data.tar.gz: a21c3957630ea45d248eec5f49999e31424d3ea30c70b3d9b34b11dbe3105c12
5
5
  SHA512:
6
- metadata.gz: 16595166f423f444eac0ad2b8e5be9e8c8b4f3334e24d23cc7b3e6a348ea3ba32a50354bd414250505943fbbef899c4bf9dc6f059f7d9ab172e949b6c817a05b
7
- data.tar.gz: d6b47a16615e8af50a0026b1288d8f9d59369e7a4751f0d4b5f13e957e8fcbb7f92b4ee15d833e3a791b7c6ed6b2c3773e4de709b2847d920bc0126dc1ce3485
6
+ metadata.gz: 7ec1e065535fe7fbfa1c94f82f1f5f06057857aa6439b41fab7e8570b4b584276a9625fd5d806431a6abe3307b147107f6e91469c65b351bcf3b5a9f868d8d83
7
+ data.tar.gz: 4ab8e424d00107d029dbe0cdd97740a2d2470994121c1d82b40fd902c5bbd1d1712849756da75631f675666ee39f09a4d56f0839452eb0a29a9b8a78c1ef37bc
@@ -432,7 +432,6 @@ module Labimotion
432
432
  desc 'Return serialized elements of current user'
433
433
  params do
434
434
  optional :collection_id, type: Integer, desc: 'Collection id'
435
- optional :sync_collection_id, type: Integer, desc: 'SyncCollectionsUser id'
436
435
  optional :el_type, type: String, desc: 'element klass name'
437
436
  optional :from_date, type: Integer, desc: 'created_date from in ms'
438
437
  optional :to_date, type: Integer, desc: 'created_date to in ms'
@@ -14,9 +14,9 @@ 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
20
+ mount Labimotion::WellplateAPI
21
21
  end
22
22
  end
@@ -29,46 +29,19 @@ module Labimotion
29
29
 
30
30
  resource :mtt do
31
31
  namespace :requests do
32
- desc 'Get MTT requests for current user'
32
+ desc 'Get MTT requests for current user, scoped to a single element'
33
+ params do
34
+ requires :element_id, type: Integer, desc: 'Only return requests for this element'
35
+ end
33
36
  get do
34
- # Get all requests created by current user
37
+ # Get requests created by current user for the selected element only
35
38
  requests = Labimotion::DoseRespRequest
36
39
  .includes(:dose_resp_outputs)
37
- .where(created_by: current_user.id)
40
+ .where(created_by: current_user.id, element_id: params[:element_id])
38
41
  .order(created_at: :desc)
39
42
 
40
43
  # 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
44
+ requests.map { |req| mtt_request_json(req, include_outputs: true) }
72
45
  end
73
46
 
74
47
  desc 'Delete one or multiple MTT requests'
@@ -132,13 +105,7 @@ module Labimotion
132
105
  id: request.id,
133
106
  request_id: request.request_id,
134
107
  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,
108
+ state_name: mtt_state_name(request.state),
142
109
  resp_message: request.resp_message,
143
110
  revoked: request.revoked?,
144
111
  updated_at: request.updated_at
@@ -183,6 +150,34 @@ module Labimotion
183
150
  rescue StandardError => e
184
151
  error!("Error deleting outputs: #{e.message}", 500)
185
152
  end
153
+
154
+ desc 'Delete a single result (by sample name) from an output'
155
+ params do
156
+ requires :id, type: Integer, desc: 'Output ID'
157
+ requires :sample_name, type: String, desc: 'Sample name (result[].name) to remove'
158
+ end
159
+ delete ':id/results' do
160
+ # Only operate on outputs whose parent request belongs to the current user
161
+ output = Labimotion::DoseRespOutput
162
+ .joins(:dose_resp_request)
163
+ .where(dose_resp_requests: { created_by: current_user.id })
164
+ .find_by(id: params[:id])
165
+
166
+ error!('Output not found or unauthorized', 404) unless output
167
+
168
+ outcome = remove_mtt_result_by_sample_name(output, params[:sample_name])
169
+ error!('Result not found in output', 404) unless outcome[:removed]
170
+
171
+ {
172
+ success: true,
173
+ message: "Removed result '#{params[:sample_name]}' from output #{params[:id]}",
174
+ output_id: params[:id],
175
+ output_deleted: outcome[:output_deleted],
176
+ output: outcome[:output_deleted] ? nil : mtt_output_json(output)
177
+ }
178
+ rescue StandardError => e
179
+ error!("Error deleting result: #{e.message}", 500)
180
+ end
186
181
  end
187
182
 
188
183
  namespace :create_mtt_request do
@@ -194,7 +189,7 @@ module Labimotion
194
189
  # Find element and wellplates
195
190
  element = Labimotion::Element.find_by(id: params[:id])
196
191
  error!('Element not found', 404) unless element
197
- #byebug
192
+
198
193
  # Verify user has update permission
199
194
  error!('Unauthorized', 403) unless ElementPolicy.new(current_user, element).update?
200
195
 
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ # API for wellplates linked to a generic element
5
+ class WellplateAPI < Grape::API
6
+ resource :wellplates do
7
+ namespace :by_generic_element do
8
+ desc 'Get wellplates for a generic element'
9
+ params do
10
+ requires :element_id, type: Integer, desc: 'Generic element id'
11
+ end
12
+ route_param :element_id do
13
+ get do
14
+ element = Labimotion::Element.find_by(id: params[:element_id])
15
+ error!('404 Not Found', 404) unless element
16
+ error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, element).read?
17
+
18
+ wellplate_ids = Labimotion::ElementsWellplate.where(element_id: element.id).pluck(:wellplate_id)
19
+ wellplates = Wellplate.where(id: wellplate_ids)
20
+ .includes(:wells, wells: :sample)
21
+
22
+ serialized_wellplates = wellplates.map do |wellplate|
23
+ detail_levels = ElementDetailLevelCalculator.new(user: current_user, element: wellplate).detail_levels
24
+ Entities::WellplateEntity.represent(wellplate, detail_levels: detail_levels)
25
+ end
26
+
27
+ { wellplates: serialized_wellplates }
28
+ end
29
+
30
+ desc 'Update wellplates for a generic element'
31
+ params do
32
+ requires :wellplate_ids, type: Array, desc: 'Wellplate IDs'
33
+ end
34
+ put do
35
+ element = Labimotion::Element.find_by(id: params[:element_id])
36
+ error!('404 Not Found', 404) unless element
37
+ error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, element).update?
38
+
39
+ ActiveRecord::Base.transaction do
40
+ current_ids = Labimotion::ElementsWellplate.where(element_id: element.id).pluck(:wellplate_id)
41
+ new_ids = params[:wellplate_ids] || []
42
+
43
+ ids_to_remove = current_ids - new_ids
44
+ if ids_to_remove.any?
45
+ Labimotion::ElementsWellplate.where(element_id: element.id, wellplate_id: ids_to_remove).destroy_all
46
+ end
47
+
48
+ ids_to_add = new_ids - current_ids
49
+ ids_to_add.each do |wellplate_id|
50
+ Labimotion::ElementsWellplate.create!(element_id: element.id, wellplate_id: wellplate_id)
51
+ end
52
+ end
53
+
54
+ { wellplate_ids: Labimotion::ElementsWellplate.where(element_id: element.id).pluck(:wellplate_id) }
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -215,7 +215,7 @@ module Labimotion
215
215
  layer, field = params[:sort_column].split('.')
216
216
 
217
217
  element_klass = Labimotion::ElementKlass.find_by(name: params[:el_type])
218
- allowed_fields = element_klass.properties_release.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
218
+ allowed_fields = element_klass&.properties_release&.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
219
219
 
220
220
  if field.in?(allowed_fields)
221
221
  query = ActiveRecord::Base.sanitize_sql(
@@ -247,27 +247,21 @@ module Labimotion
247
247
  end
248
248
 
249
249
  def list_serialized_elements(params, current_user)
250
- collection_id =
251
- if params[:collection_id]
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
250
+ scope = Labimotion::Element.none
260
251
 
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)
252
+ 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
269
262
  else
270
- Labimotion::Element.none
263
+ # All collection of current_user
264
+ scope = Labimotion::Element.for_user(current_user.id)
271
265
  end
272
266
 
273
267
  ## TO DO: refactor labimotion
@@ -278,7 +272,7 @@ module Labimotion
278
272
  layer, field = params[:sort_column].split('.')
279
273
 
280
274
  element_klass = Labimotion::ElementKlass.find_by(name: params[:el_type])
281
- allowed_fields = element_klass.properties_release.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
275
+ allowed_fields = element_klass&.properties_release&.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
282
276
 
283
277
  if field.in?(allowed_fields)
284
278
  query = ActiveRecord::Base.sanitize_sql(
@@ -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)
@@ -9,7 +9,6 @@
9
9
  "file": "acqus",
10
10
  "parameters": [
11
11
  "DATE",
12
- "D1",
13
12
  "INSTRUM",
14
13
  "NS",
15
14
  "NUC1",
@@ -23,7 +22,10 @@
23
22
  "TD",
24
23
  "TE",
25
24
  "TITLE"
26
- ]
25
+ ],
26
+ "arrayParameters": {
27
+ "D1": { "name": "D", "index": 1 }
28
+ }
27
29
  },
28
30
  "procs": {
29
31
  "file": "pdata/1/procs",
@@ -26,9 +26,9 @@ module Labimotion
26
26
  joins(collections: :user).where(collections: { user_id: user_id })
27
27
  )
28
28
 
29
- # Shared (synced) records
29
+ # Shared records
30
30
  shared = apply_filters.call(
31
- joins(collections: :sync_collections_users).where(sync_collections_users: { user_id: user_id })
31
+ left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
32
32
  )
33
33
 
34
34
  # Combine (remove duplicates), order, and limit
@@ -141,9 +141,9 @@ module Labimotion
141
141
  joins(collections: :user).where(collections: { user_id: user_id })
142
142
  )
143
143
 
144
- # Shared (synced) elements
144
+ # Shared elements
145
145
  shared = apply_filters.call(
146
- joins(collections: :sync_collections_users).where(sync_collections_users: { user_id: user_id })
146
+ left_joins(:collection_shares).where(collection_shares: { shared_with_id: user_id })
147
147
  )
148
148
 
149
149
  # Combine (remove duplicates), order, and limit
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ class ElementsWellplate < ApplicationRecord
5
+ acts_as_paranoid
6
+ self.table_name = :elements_wellplates
7
+ belongs_to :element, class_name: 'Labimotion::Element'
8
+ belongs_to :wellplate
9
+ end
10
+ end
@@ -70,6 +70,37 @@ module Labimotion
70
70
  extracted_parameters
71
71
  end
72
72
 
73
+ # Extracts scalar values from Bruker array parameters such as
74
+ # ##$D= (0..63)
75
+ # 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.0002 ...
76
+ # where the values live on the line(s) following the header. The config maps
77
+ # each output key to an array name + index, e.g.
78
+ # { "D1" => { "name" => "D", "index" => 1 } } => D1 = the 2nd value (here 1).
79
+ def extract_array_parameters(file_content, array_parameters)
80
+ return {} if file_content.blank? || array_parameters.blank?
81
+
82
+ lines = file_content.lines
83
+ extracted = {}
84
+ lines.each_with_index do |line, idx|
85
+ header = line.match(/^\s*##\$(?<name>[A-Za-z0-9_]+)\s*=\s*\(\s*\d+\s*\.\.\s*\d+\s*\)/)
86
+ next unless header
87
+
88
+ targets = array_parameters.select { |_key, cfg| cfg['name'] == header[:name] }
89
+ next if targets.empty?
90
+
91
+ values = collect_array_values(lines, idx + 1)
92
+ targets.each do |out_key, cfg|
93
+ value = values[cfg['index'].to_i]
94
+ extracted[out_key] = clean_value(value) if value.present?
95
+ end
96
+ end
97
+ extracted.compact_blank!
98
+ extracted
99
+ rescue StandardError => e
100
+ Rails.logger.error "Error extracting array parameters: #{e.message}"
101
+ {}
102
+ end
103
+
73
104
  def format_timestamp(timestamp_str, give_format = nil)
74
105
  return nil if timestamp_str.blank?
75
106
 
@@ -118,6 +149,21 @@ module Labimotion
118
149
  value
119
150
  end
120
151
 
152
+ # Collects whitespace-separated values from the line(s) following an array
153
+ # header, stopping at the next directive (line starting with '##') or a blank line.
154
+ def collect_array_values(lines, start_index)
155
+ values = []
156
+ index = start_index
157
+ while index < lines.length
158
+ stripped = lines[index].strip
159
+ break if stripped.empty? || stripped.start_with?('##')
160
+
161
+ values.concat(stripped.split(/\s+/))
162
+ index += 1
163
+ end
164
+ values
165
+ end
166
+
121
167
  def process_zip_file(zip_file_url, source_map)
122
168
  final_parameters = {}
123
169
 
@@ -137,23 +183,28 @@ module Labimotion
137
183
 
138
184
  zip_file.each do |entry|
139
185
  if source_file?(entry, source_config)
140
- process_file_entry(entry, source_config['parameters'], final_parameters)
186
+ process_file_entry(entry, source_config, final_parameters)
141
187
  elsif bagit_metadata_file?(entry)
142
188
  return { is_bagit: true, metadata: nil }
143
189
  end
144
190
  end
145
191
  end
146
192
 
147
- def process_file_entry(entry, parameters, final_parameters)
193
+ def process_file_entry(entry, source_config, final_parameters)
148
194
  file_content = entry.get_input_stream.read.force_encoding(Constants::File::ENCODING)
149
- extracted_parameters = extract_parameters(file_content, parameters)
195
+ extracted_parameters = extract_parameters(file_content, source_config['parameters'])
150
196
  final_parameters.merge!(extracted_parameters) if extracted_parameters.present?
197
+
198
+ # Array parameters (e.g. D1 from acqus ##$D) are a fallback: they only fill a
199
+ # value that an earlier/higher-priority source (e.g. parm.txt D1) did not provide.
200
+ array_parameters = extract_array_parameters(file_content, source_config['arrayParameters'])
201
+ final_parameters.reverse_merge!(array_parameters) if array_parameters.present?
151
202
  end
152
203
 
153
204
  def invalid_source_config?(source_config)
154
205
  source_config.nil? ||
155
206
  source_config['file'].nil? ||
156
- source_config['parameters'].nil?
207
+ (source_config['parameters'].nil? && source_config['arrayParameters'].nil?)
157
208
  end
158
209
 
159
210
  def source_file?(entry, source_config)
@@ -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.belongs_to_or_shared_by(current_user.id, current_user.group_ids).find(c_id)
15
+ collection = Collection.accessible_for(current_user).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.belongs_to_or_shared_by(current_user.id, current_user.group_ids).find(c_id)
100
+ collection = Collection.accessible_for(current_user).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.rc16'
5
+ VERSION = '2.2.0.rc18'
6
6
  end
data/lib/labimotion.rb CHANGED
@@ -29,10 +29,10 @@ 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'
35
+ autoload :WellplateAPI, 'labimotion/apis/wellplate_api'
36
36
 
37
37
  ######## Entities
38
38
  autoload :PropertiesEntity, 'labimotion/entities/properties_entity'
@@ -124,6 +124,7 @@ module Labimotion
124
124
 
125
125
  autoload :ElementsSample, 'labimotion/models/elements_sample'
126
126
  autoload :ElementsElement, 'labimotion/models/elements_element'
127
+ autoload :ElementsWellplate, 'labimotion/models/elements_wellplate'
127
128
  autoload :CollectionsElement, 'labimotion/models/collections_element'
128
129
 
129
130
  autoload :StdLayer, 'labimotion/models/std_layer'
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.rc16
4
+ version: 2.2.0.rc18
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-06-22 00:00:00.000000000 Z
12
+ date: 2026-07-01 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: caxlsx
@@ -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
@@ -74,6 +73,7 @@ files:
74
73
  - lib/labimotion/apis/standard_layer_api.rb
75
74
  - lib/labimotion/apis/user_api.rb
76
75
  - lib/labimotion/apis/vocabulary_api.rb
76
+ - lib/labimotion/apis/wellplate_api.rb
77
77
  - lib/labimotion/collection/export.rb
78
78
  - lib/labimotion/collection/import.rb
79
79
  - lib/labimotion/conf.rb
@@ -151,6 +151,7 @@ files:
151
151
  - lib/labimotion/models/elements_element.rb
152
152
  - lib/labimotion/models/elements_revision.rb
153
153
  - lib/labimotion/models/elements_sample.rb
154
+ - lib/labimotion/models/elements_wellplate.rb
154
155
  - lib/labimotion/models/hub_log.rb
155
156
  - lib/labimotion/models/reaction.rb
156
157
  - lib/labimotion/models/research_plan.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