labimotion 2.2.0.rc12 → 2.2.0.rc14

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: 407197c03e88402882b081d31dc92999629ec43c25b8c056c6f6ba3ca40135fc
4
- data.tar.gz: d331368ce985ae7ff32a98f0747e9b06233eb5ac3b9b1a0c17e76a3ecbeeab7a
3
+ metadata.gz: 92bb0da102b4ac516f423c274a21bb59a08fffecb2d451d0f1d5caee8631d5af
4
+ data.tar.gz: e25335102afc9aa97669c3c09f671f64fa2102344fa2764817b6fe9556638359
5
5
  SHA512:
6
- metadata.gz: 275089d4170573e9961387dfe2274e9c5c8d1f9ccfe02b854aec3b1a5bfb237f5535915fd73205fc1efa564102ef33605a4f5de148fec8244b76ee2ca9fa5b48
7
- data.tar.gz: 75b3433050aab3b9a76c9442643b0c5dcb87b508672d4b3b1c57b8ca52bbb75e1045cf429c25c4cfa64616df2482baa15062ccc45f515173012f31ca6cbcacef
6
+ metadata.gz: f9f574b0057178c25cb0624bd11370757adf1992e8f5734a7492ac380dadd779d06c83accabe1e59c07e189ce316258da0a8177b2c42fa028f4aaf418245fbda
7
+ data.tar.gz: f9d629342ffead4be91ce5728d2048a56b9de3da660df32b962d549756391ca34758edf7e1392e3aefbd117d185ff179cda085e59f4d5ad4b20e802fbdcbf060
@@ -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,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
@@ -6,11 +6,11 @@ module Labimotion
6
6
  class LabimotionDoiAPI < Grape::API
7
7
  # rubocop:disable Metrics/BlockLength
8
8
  # Grape route and helper DSL blocks are necessarily large.
9
- KLASS_TYPES = ::Usecases::Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE.keys.freeze
9
+ KLASS_TYPES = Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE.keys.freeze
10
10
 
11
11
  helpers do
12
12
  def resolve_klass!(type, id)
13
- model = ::Usecases::Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE[type]
13
+ model = Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE[type]
14
14
  error!('unknown template type', 404) unless model
15
15
 
16
16
  record = model.find_by(id: id)
@@ -20,7 +20,7 @@ module Labimotion
20
20
  end
21
21
 
22
22
  def render_template_doi(record)
23
- type = ::Usecases::Labimotion::TemplateDoiHelpers.klass_type_for(record)
23
+ type = Labimotion::TemplateDoiHelpers.klass_type_for(record)
24
24
  dois = ::Doi.labimotion_dois(record)
25
25
  # `doi` is the current (most recently reserved) DOI; `released_doi` is the
26
26
  # latest released (minted) one. They differ once a new version has been
@@ -35,9 +35,9 @@ module Labimotion
35
35
  publication: (record.properties_template || {})['publication'],
36
36
  released_at: record.released_at,
37
37
  released_by: record.released_by,
38
- new_version_available: ::Usecases::Labimotion::TemplateDoiHelpers.new_version_available?(record),
38
+ new_version_available: Labimotion::TemplateDoiHelpers.new_version_available?(record),
39
39
  next_doi_version: ::Doi.labimotion_version_segment(record),
40
- template_url: ::Usecases::Labimotion::TemplateDoiHelpers.template_url(record)
40
+ template_url: Labimotion::TemplateDoiHelpers.template_url(record)
41
41
  )
42
42
  end
43
43
 
@@ -67,7 +67,7 @@ module Labimotion
67
67
  label: record.try(:label),
68
68
  template_version: record.try(:version),
69
69
  next_doi_version: ::Doi.labimotion_version_segment(record),
70
- template_url: ::Usecases::Labimotion::TemplateDoiHelpers.template_url(record),
70
+ template_url: Labimotion::TemplateDoiHelpers.template_url(record),
71
71
  publication: publication,
72
72
  versions: versions
73
73
  )
@@ -82,7 +82,7 @@ module Labimotion
82
82
  route_param :type, type: String, values: KLASS_TYPES do
83
83
  desc 'Released DOIs + publication metadata for every released template of a type'
84
84
  get :released do
85
- model = ::Usecases::Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE[params[:type]]
85
+ model = Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE[params[:type]]
86
86
  grouped = ::Doi.where(doiable_type: model.name).where.not(minted_at: nil)
87
87
  .order(:id).group_by(&:doiable_id)
88
88
  records = model.where(id: grouped.keys).index_by(&:id)
@@ -98,7 +98,7 @@ module Labimotion
98
98
  route_param :type, type: String, values: KLASS_TYPES do
99
99
  desc "DOI state (reserved / released) per template of a type, for the designer grid's DOI icon"
100
100
  get :states do
101
- model = ::Usecases::Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE[params[:type]]
101
+ model = Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE[params[:type]]
102
102
  grouped = ::Doi.where(doiable_type: model.name).order(:id).group_by(&:doiable_id)
103
103
  states = grouped.transform_values do |list|
104
104
  released = list.last.minted == true
@@ -125,7 +125,7 @@ module Labimotion
125
125
  version: ::Doi.labimotion_doi_version(doi),
126
126
  full_doi: doi.full_doi,
127
127
  minted: doi.minted == true,
128
- xml: ::Usecases::Labimotion::BuildTemplateDoiXml.new(record, doi, current_user).call
128
+ xml: Labimotion::BuildTemplateDoiXml.new(record, doi, current_user).call
129
129
  }
130
130
  end
131
131
  { versions: versions }
@@ -137,7 +137,7 @@ module Labimotion
137
137
  end
138
138
  put :publication_metadata do
139
139
  record = resolve_klass!(params[:type], params[:id])
140
- uc = ::Usecases::Labimotion::UpdateTemplatePublicationMetadata.new(
140
+ uc = Labimotion::UpdateTemplatePublicationMetadata.new(
141
141
  record, current_user, params[:publication]
142
142
  ).call
143
143
  error!(uc.error, 422) unless uc.success?
@@ -148,7 +148,7 @@ module Labimotion
148
148
  desc 'Reserves a DataCite DOI for a template'
149
149
  post :reserve_doi do
150
150
  record = resolve_klass!(params[:type], params[:id])
151
- uc = ::Usecases::Labimotion::ReserveTemplateDoi.new(record, current_user).call
151
+ uc = Labimotion::ReserveTemplateDoi.new(record, current_user).call
152
152
  error!(uc.error, 422) unless uc.success?
153
153
 
154
154
  render_template_doi(record.reload)
@@ -157,7 +157,7 @@ module Labimotion
157
157
  desc 'Releases (mints) the reserved DOI for a template'
158
158
  post :release_doi do
159
159
  record = resolve_klass!(params[:type], params[:id])
160
- uc = ::Usecases::Labimotion::ReleaseTemplateDoi.new(record, current_user).call
160
+ uc = Labimotion::ReleaseTemplateDoi.new(record, current_user).call
161
161
  error!(uc.error, 422) unless uc.success?
162
162
 
163
163
  render_template_doi(record.reload)
@@ -7,7 +7,7 @@ module Labimotion
7
7
  # properties_release so the frontend can render the example inline.
8
8
  # Whitelisted in RepoAPI::PUBLIC_URLS so anonymous hub visitors can use it.
9
9
  class LabimotionTemplateBrowseAPI < Grape::API
10
- KLASS_BY_TYPE = ::Usecases::Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE
10
+ KLASS_BY_TYPE = Labimotion::TemplateDoiHelpers::KLASS_BY_TYPE
11
11
  KLASS_TYPES = KLASS_BY_TYPE.keys.freeze
12
12
  # type => [revision model, foreign key to the klass]
13
13
  REVISION_BY_TYPE = {
@@ -88,7 +88,7 @@ module Labimotion
88
88
  full = doi.full_doi
89
89
  { version: ::Doi.labimotion_doi_version(doi), doi: full,
90
90
  doi_url: "https://dx.doi.org/#{full}", minted_at: doi.minted_at,
91
- xml: ::Usecases::Labimotion::BuildTemplateDoiXml.new(record, doi).call }
91
+ xml: Labimotion::BuildTemplateDoiXml.new(record, doi).call }
92
92
  end
93
93
  end
94
94
  end
@@ -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
 
@@ -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)
@@ -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,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'erb'
4
+
5
+ module Labimotion
6
+ # Builds the DataCite (kernel-4) metadata XML for a LabIMotion template DOI.
7
+ # The same XML is uploaded to DataCite at release time and shown in the UI
8
+ # preview, so what users see is exactly what gets sent. A template has a
9
+ # single DOI; the ERB template binds to this instance, so the methods below
10
+ # are its accessors.
11
+ class BuildTemplateDoiXml
12
+ include ::DataCitePublisher
13
+
14
+ TEMPLATE_PATH = Rails.root.join('app/publish/datacite_metadata_labimotion_template.html.erb')
15
+
16
+ RESOURCE_LABEL = {
17
+ 'element' => 'LabIMotion Element Template',
18
+ 'segment' => 'LabIMotion Segment Template',
19
+ 'dataset' => 'LabIMotion Dataset Template'
20
+ }.freeze
21
+
22
+ def initialize(record, doi = nil, current_user = nil)
23
+ @record = record
24
+ @doi = doi || ::Doi.labimotion_latest_doi(record)
25
+ @current_user = current_user
26
+ end
27
+
28
+ # Returns the XML string, or nil when no DOI has been reserved yet.
29
+ def call
30
+ return nil if @doi.nil?
31
+
32
+ ERB.new(File.read(TEMPLATE_PATH), trim_mode: '-').result(binding)
33
+ end
34
+
35
+ private
36
+
37
+ def full_doi
38
+ @doi.full_doi
39
+ end
40
+
41
+ def title
42
+ h(publication['title'].presence || @record.label.presence || @record.try(:name))
43
+ end
44
+
45
+ def description
46
+ h(publication['description'])
47
+ end
48
+
49
+ # The DOI version this DOI was reserved for (kept on the DOI), so each
50
+ # DOI's XML keeps showing its own version even after the template moves on.
51
+ def version
52
+ h(::Doi.labimotion_doi_version(@doi).presence || ::Doi.labimotion_version_segment(@record))
53
+ end
54
+
55
+ def license
56
+ h(publication['license'].presence || 'CC-BY-4.0')
57
+ end
58
+
59
+ def resource_label
60
+ RESOURCE_LABEL[TemplateDoiHelpers.klass_type_for(@record)]
61
+ end
62
+
63
+ def publication_year
64
+ (@doi.minted_at || Time.current).strftime('%Y')
65
+ end
66
+
67
+ def creators
68
+ Array(publication['authors']).filter_map do |author|
69
+ next unless author.is_a?(Hash)
70
+
71
+ given = author['givenName'].to_s.strip
72
+ family = author['familyName'].to_s.strip
73
+ next if given.empty? && family.empty?
74
+
75
+ { 'givenName' => h(given), 'familyName' => h(family),
76
+ 'orcid' => h(author['orcid'].to_s.strip), 'affiliation' => h(author['affiliation'].to_s.strip) }
77
+ end
78
+ end
79
+
80
+ # The acting user, rendered as a DataCite "Researcher" contributor. Nil
81
+ # (no contributors block) when there is no user or they have no name.
82
+ def contributor
83
+ user = @current_user
84
+ return nil unless user
85
+
86
+ given = user.try(:first_name).to_s.strip
87
+ family = user.try(:last_name).to_s.strip
88
+ return nil if given.empty? && family.empty?
89
+
90
+ orcid = user.respond_to?(:orcid) ? user.orcid.to_s.strip : ''
91
+ { 'givenName' => h(given), 'familyName' => h(family), 'orcid' => h(orcid) }
92
+ end
93
+
94
+ def publication
95
+ @publication ||=
96
+ (@record.properties_template.is_a?(Hash) ? @record.properties_template['publication'] : nil) || {}
97
+ end
98
+
99
+ def h(value)
100
+ ERB::Util.html_escape(value.to_s)
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ # Mints (publishes) a previously reserved DOI for a LabIMotion template
5
+ # via DataCite MDS. On success flips Doi#minted=true and stamps
6
+ # released_by/released_at on the template record.
7
+ class ReleaseTemplateDoi
8
+ include TemplateDoiHelpers
9
+
10
+ attr_reader :record, :current_user, :doi, :error
11
+
12
+ def initialize(record, current_user)
13
+ @record = record
14
+ @current_user = current_user
15
+ end
16
+
17
+ def call
18
+ return failure('record is required') if record.nil?
19
+ return failure('unauthorized') unless TemplateDoiHelpers.authorized?(record, current_user)
20
+
21
+ # Release the template's current (highest-sequence) DOI — the one a fresh
22
+ # reserve created or returned.
23
+ @doi = ::Doi.labimotion_latest_doi(record)
24
+ return failure('doi not reserved') if @doi.nil?
25
+ return self if @doi.minted
26
+
27
+ return failure('publication metadata is incomplete') unless metadata_ready?
28
+
29
+ publish_at_datacite
30
+ self
31
+ end
32
+
33
+ def success?
34
+ @error.nil? && @doi&.minted
35
+ end
36
+
37
+ private
38
+
39
+ def publish_at_datacite
40
+ if send_to_datacite?
41
+ mds = ::Repo::Datacite::Mds.new
42
+ # DataCite requires the metadata to exist before the DOI can be minted.
43
+ metadata = mds.upload_metadata(BuildTemplateDoiXml.new(record, doi, current_user).call)
44
+ return failure(datacite_error('metadata upload', metadata)) unless metadata.is_a?(Net::HTTPSuccess)
45
+
46
+ response = mds.mint(doi.full_doi, TemplateDoiHelpers.template_url(record))
47
+ return failure(datacite_error('mint', response)) unless response.is_a?(Net::HTTPSuccess)
48
+ end
49
+
50
+ ActiveRecord::Base.transaction do
51
+ doi.update!(minted: true, minted_at: Time.current)
52
+ stamp_release_on_record
53
+ end
54
+ end
55
+
56
+ def datacite_error(step, response)
57
+ status = response.respond_to?(:code) ? response.code : 'unknown'
58
+ "datacite #{step} failed (status #{status})"
59
+ end
60
+
61
+ # Mirrors Publication#transition_from_doi_registering_to_registered!: only
62
+ # hit DataCite for real on test DOIs or in production publishing. In any
63
+ # other mode (e.g. dev/staging) skip the network call and just mark the
64
+ # DOI released locally.
65
+ def send_to_datacite?
66
+ ENV['DATACITE_MODE'] == 'test' || ENV['PUBLISH_MODE'] == 'production'
67
+ end
68
+
69
+ def stamp_release_on_record
70
+ attrs = {}
71
+ attrs[:released_at] = Time.current if record.respond_to?(:released_at)
72
+ attrs[:released_by] = current_user.id if record.respond_to?(:released_by) && current_user
73
+ record.update!(attrs) if attrs.any?
74
+ end
75
+
76
+ def metadata_ready?
77
+ publication = record.properties_template.is_a?(Hash) ? record.properties_template['publication'] : nil
78
+ return false unless publication.is_a?(Hash)
79
+
80
+ publication['title'].to_s.strip.present? &&
81
+ publication['description'].to_s.strip.present? &&
82
+ Array(publication['authors']).any? { |a| a.is_a?(Hash) && a['familyName'].to_s.strip.present? }
83
+ end
84
+
85
+ def failure(message)
86
+ @error = message
87
+ self
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ # Reserves a DataCite DOI for a LabIMotion template (ElementKlass /
5
+ # SegmentKlass / DatasetKlass). A DOI is published per major release
6
+ # (template X.0 -> DOI vX; later minor revisions X.y roll into v(X+1));
7
+ # sub-1.0 and unversioned templates map to the first DOI (v1). The existing
8
+ # DOI is returned unless the latest one has been released AND the template
9
+ # has since moved to a version that maps to a new DOI version.
10
+ class ReserveTemplateDoi
11
+ include TemplateDoiHelpers
12
+
13
+ attr_reader :record, :current_user, :doi, :error
14
+
15
+ def initialize(record, current_user)
16
+ @record = record
17
+ @current_user = current_user
18
+ end
19
+
20
+ def call
21
+ return failure('record is required') if record.nil?
22
+ return failure('unauthorized') unless TemplateDoiHelpers.authorized?(record, current_user)
23
+
24
+ @doi = reserve
25
+ self
26
+ end
27
+
28
+ def success?
29
+ @error.nil? && @doi.present?
30
+ end
31
+
32
+ private
33
+
34
+ # Returns the template's current DOI, or reserves one for a new version.
35
+ # A new DOI is only created when there is none yet, or once the latest DOI
36
+ # has been released and the template has moved to a new version. Otherwise
37
+ # the existing DOI is returned, so repeated reserves are idempotent and a
38
+ # released version stays read-only until the template is revised.
39
+ def reserve
40
+ latest = ::Doi.labimotion_latest_doi(record)
41
+ return latest if latest && !TemplateDoiHelpers.new_version_available?(record)
42
+
43
+ create_doi
44
+ end
45
+
46
+ def create_doi
47
+ suffix = ::Doi.build_labimotion_suffix(record)
48
+ return failure('could not build a DOI suffix for this template') if suffix.blank?
49
+
50
+ # Set inchikey == suffix and version_count: 0 so Doi#align_suffix keeps
51
+ # the deterministic suffix verbatim (no /concept, no rebuild).
52
+ ::Doi.create!(
53
+ doiable_id: record.id,
54
+ doiable_type: record.class.name,
55
+ inchikey: suffix,
56
+ suffix: suffix,
57
+ version_count: 0,
58
+ metadata: ::Doi.labimotion_doi_metadata(record)
59
+ )
60
+ rescue ActiveRecord::RecordInvalid => e
61
+ failure(e.message)
62
+ nil
63
+ end
64
+
65
+ def failure(message)
66
+ @error = message
67
+ self
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ # Shared helpers for LabIMotion template DOIs: the type <-> klass mapping,
5
+ # authorization, the public Template Hub URL and "is a new version reservable"
6
+ # check. Used by the template DOI usecases and APIs.
7
+ module TemplateDoiHelpers
8
+ KLASS_BY_TYPE = {
9
+ 'element' => ::Labimotion::ElementKlass,
10
+ 'segment' => ::Labimotion::SegmentKlass,
11
+ 'dataset' => ::Labimotion::DatasetKlass
12
+ }.freeze
13
+
14
+ # Maps a template record to the `generic_admin` profile key that grants
15
+ # admin rights over it. Keys are plural, matching the LabIMotion gem's
16
+ # `authenticate_admin!` and `User#generic_admin`.
17
+ GENERIC_ADMIN_KEY = {
18
+ ::Labimotion::ElementKlass => 'elements',
19
+ ::Labimotion::SegmentKlass => 'segments',
20
+ ::Labimotion::DatasetKlass => 'datasets'
21
+ }.freeze
22
+
23
+ module_function
24
+
25
+ # True once the template's latest DOI has been released (minted) and the
26
+ # template has since moved to a new major version — i.e. a DOI for the new
27
+ # version may now be reserved. False while there is no DOI, the latest is
28
+ # still unreleased, or the DOI version is unchanged (released = read-only,
29
+ # and minor template revisions keep the same DOI).
30
+ def new_version_available?(record)
31
+ latest = ::Doi.labimotion_latest_doi(record)
32
+ return false unless latest&.minted
33
+
34
+ ::Doi.labimotion_doi_version(latest) != ::Doi.labimotion_version_segment(record)
35
+ end
36
+
37
+ def klass_type_for(record)
38
+ case record
39
+ when ::Labimotion::ElementKlass then 'element'
40
+ when ::Labimotion::SegmentKlass then 'segment'
41
+ when ::Labimotion::DatasetKlass then 'dataset'
42
+ end
43
+ end
44
+
45
+ # Public URL that opens the template in the LabIMotion Template Hub's
46
+ # "Find a Template" page (the Finder), preselecting the template type, the
47
+ # template and its release version:
48
+ # /home/genericHub?type=finder&kind=<type>&id=<identifier|uuid>&version=<v>
49
+ # Used as the DOI's DataCite landing page and surfaced in the UI.
50
+ def template_url(record)
51
+ type = klass_type_for(record)
52
+ host = ENV['REPO_HOST'].presence || ENV['DOI_DOMAIN_TEST'].presence ||
53
+ ENV['DOI_DOMAIN'].presence || 'https://www.chemotion-repository.net'
54
+ record_id = record.try(:identifier).presence || record.try(:uuid).presence || record.id
55
+ query = "type=finder&kind=#{type}&id=#{record_id}"
56
+ version = record.try(:version)
57
+ query += "&version=#{version}" if version.present?
58
+ "#{host.chomp('/')}/home/genericHub?#{query}"
59
+ end
60
+
61
+ # A user may manage a template's DOI iff they hold the LabIMotion
62
+ # `generic_admin` right for that template kind — the same gate the gem's
63
+ # `authenticate_admin!` and the admin UI use. `admin_ids`/`created_by` do
64
+ # NOT apply: segment templates carry neither, so they were always denied.
65
+ def authorized?(record, user)
66
+ return false unless record && user
67
+
68
+ key = GENERIC_ADMIN_KEY[record.class]
69
+ return false unless key
70
+
71
+ admin = user.respond_to?(:generic_admin) ? user.generic_admin : nil
72
+ admin.is_a?(Hash) && !!admin[key]
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ # Merges a normalized "publication" sub-object into the template's
5
+ # properties_template jsonb. Keeps the rest of the jsonb untouched so the
6
+ # labimotion gem's runtime shape stays intact.
7
+ class UpdateTemplatePublicationMetadata
8
+ include TemplateDoiHelpers
9
+
10
+ ALLOWED_KEYS = %w[title description authors license].freeze
11
+
12
+ attr_reader :record, :current_user, :publication, :error
13
+
14
+ def initialize(record, current_user, publication)
15
+ @record = record
16
+ @current_user = current_user
17
+ @publication = publication
18
+ end
19
+
20
+ def call
21
+ return failure('record is required') if record.nil?
22
+ return failure('unauthorized') unless TemplateDoiHelpers.authorized?(record, current_user)
23
+ return failure('publication payload must be a hash') unless publication.is_a?(Hash)
24
+
25
+ merged = normalized_publication
26
+ props = (record.properties_template || {}).dup
27
+ props['publication'] = merged
28
+ record.update!(properties_template: props)
29
+ @publication = merged
30
+ self
31
+ end
32
+
33
+ def success?
34
+ @error.nil?
35
+ end
36
+
37
+ private
38
+
39
+ def normalized_publication
40
+ slice = publication.stringify_keys.slice(*ALLOWED_KEYS)
41
+ slice['authors'] = Array(slice['authors']).filter_map { |a| normalize_author(a) }
42
+ slice
43
+ end
44
+
45
+ def normalize_author(author)
46
+ return nil unless author.is_a?(Hash)
47
+
48
+ h = author.stringify_keys.slice('givenName', 'familyName', 'orcid', 'affiliation', 'affiliation_id')
49
+ return nil if h.values_at('givenName', 'familyName').all? { |v| v.to_s.strip.empty? }
50
+
51
+ h
52
+ end
53
+
54
+ def failure(message)
55
+ @error = message
56
+ self
57
+ end
58
+ end
59
+ 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.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.rc12'
5
+ VERSION = '2.2.0.rc14'
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'
@@ -72,6 +71,13 @@ module Labimotion
72
71
  autoload :RepositoryHelpers, 'labimotion/helpers/repository_helpers'
73
72
  autoload :VocabularyHelpers, 'labimotion/helpers/vocabulary_helpers'
74
73
 
74
+ ######## Usecases
75
+ autoload :TemplateDoiHelpers, 'labimotion/usecases/template_doi_helpers'
76
+ autoload :BuildTemplateDoiXml, 'labimotion/usecases/build_template_doi_xml'
77
+ autoload :ReserveTemplateDoi, 'labimotion/usecases/reserve_template_doi'
78
+ autoload :ReleaseTemplateDoi, 'labimotion/usecases/release_template_doi'
79
+ autoload :UpdateTemplatePublicationMetadata, 'labimotion/usecases/update_template_publication_metadata'
80
+
75
81
  ######## Libs
76
82
  autoload :Converter, 'labimotion/libs/converter'
77
83
  autoload :DatasetBuilder, 'labimotion/libs/dataset_builder'
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.rc12
4
+ version: 2.2.0.rc14
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
@@ -165,6 +164,11 @@ files:
165
164
  - lib/labimotion/models/template_submission.rb
166
165
  - lib/labimotion/models/vocabulary.rb
167
166
  - lib/labimotion/models/wellplate.rb
167
+ - lib/labimotion/usecases/build_template_doi_xml.rb
168
+ - lib/labimotion/usecases/release_template_doi.rb
169
+ - lib/labimotion/usecases/reserve_template_doi.rb
170
+ - lib/labimotion/usecases/template_doi_helpers.rb
171
+ - lib/labimotion/usecases/update_template_publication_metadata.rb
168
172
  - lib/labimotion/utils/con_state.rb
169
173
  - lib/labimotion/utils/export_utils.rb
170
174
  - lib/labimotion/utils/field_type.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