labimotion 2.2.0.rc13 → 2.2.0.rc16
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 +4 -4
- data/lib/labimotion/apis/dose_resp_request_api.rb +241 -0
- data/lib/labimotion/apis/generic_element_api.rb +1 -0
- data/lib/labimotion/apis/labimotion_api.rb +1 -0
- data/lib/labimotion/apis/labimotion_doi_api.rb +12 -12
- data/lib/labimotion/apis/labimotion_template_browse_api.rb +2 -2
- data/lib/labimotion/apis/mtt_api.rb +39 -31
- data/lib/labimotion/helpers/element_helpers.rb +21 -15
- data/lib/labimotion/helpers/mtt_helpers.rb +0 -92
- data/lib/labimotion/models/concerns/element_fetchable.rb +2 -2
- data/lib/labimotion/models/element.rb +2 -2
- data/lib/labimotion/usecases/build_template_doi_xml.rb +103 -0
- data/lib/labimotion/usecases/release_template_doi.rb +90 -0
- data/lib/labimotion/usecases/reserve_template_doi.rb +70 -0
- data/lib/labimotion/usecases/template_doi_helpers.rb +75 -0
- data/lib/labimotion/usecases/update_template_publication_metadata.rb +59 -0
- data/lib/labimotion/utils/search.rb +2 -2
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +8 -0
- metadata +7 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 53d62c152440c37c962f7a989735fc000b5f9125e1fa47722a4083965f352aee
|
|
4
|
+
data.tar.gz: 30825b940e18998848773fec2d53c8e8239515cf8f40cc0f9ca18d77e70aecb8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16595166f423f444eac0ad2b8e5be9e8c8b4f3334e24d23cc7b3e6a348ea3ba32a50354bd414250505943fbbef899c4bf9dc6f059f7d9ab172e949b6c817a05b
|
|
7
|
+
data.tar.gz: d6b47a16615e8af50a0026b1288d8f9d59369e7a4751f0d4b5f13e957e8fcbb7f92b4ee15d833e3a791b7c6ed6b2c3773e4de709b2847d920bc0126dc1ce3485
|
|
@@ -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
|
|
@@ -432,6 +432,7 @@ 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'
|
|
435
436
|
optional :el_type, type: String, desc: 'element klass name'
|
|
436
437
|
optional :from_date, type: Integer, desc: 'created_date from in ms'
|
|
437
438
|
optional :to_date, type: Integer, desc: 'created_date to in ms'
|
|
@@ -14,6 +14,7 @@ module Labimotion
|
|
|
14
14
|
mount Labimotion::VocabularyAPI
|
|
15
15
|
mount Labimotion::UserAPI
|
|
16
16
|
mount Labimotion::MttAPI
|
|
17
|
+
mount Labimotion::DoseRespRequestAPI
|
|
17
18
|
mount Labimotion::ElementVariationAPI
|
|
18
19
|
mount Labimotion::LabimotionDoiAPI
|
|
19
20
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
38
|
+
new_version_available: Labimotion::TemplateDoiHelpers.new_version_available?(record),
|
|
39
39
|
next_doi_version: ::Doi.labimotion_version_segment(record),
|
|
40
|
-
template_url:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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:
|
|
91
|
+
xml: Labimotion::BuildTemplateDoiXml.new(record, doi).call }
|
|
92
92
|
end
|
|
93
93
|
end
|
|
94
94
|
end
|
|
@@ -38,7 +38,37 @@ module Labimotion
|
|
|
38
38
|
.order(created_at: :desc)
|
|
39
39
|
|
|
40
40
|
# Return formatted response
|
|
41
|
-
requests.map
|
|
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
|
|
42
72
|
end
|
|
43
73
|
|
|
44
74
|
desc 'Delete one or multiple MTT requests'
|
|
@@ -102,7 +132,13 @@ module Labimotion
|
|
|
102
132
|
id: request.id,
|
|
103
133
|
request_id: request.request_id,
|
|
104
134
|
state: request.state,
|
|
105
|
-
state_name:
|
|
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,
|
|
106
142
|
resp_message: request.resp_message,
|
|
107
143
|
revoked: request.revoked?,
|
|
108
144
|
updated_at: request.updated_at
|
|
@@ -147,34 +183,6 @@ module Labimotion
|
|
|
147
183
|
rescue StandardError => e
|
|
148
184
|
error!("Error deleting outputs: #{e.message}", 500)
|
|
149
185
|
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
|
|
178
186
|
end
|
|
179
187
|
|
|
180
188
|
namespace :create_mtt_request do
|
|
@@ -186,7 +194,7 @@ module Labimotion
|
|
|
186
194
|
# Find element and wellplates
|
|
187
195
|
element = Labimotion::Element.find_by(id: params[:id])
|
|
188
196
|
error!('Element not found', 404) unless element
|
|
189
|
-
|
|
197
|
+
#byebug
|
|
190
198
|
# Verify user has update permission
|
|
191
199
|
error!('Unauthorized', 403) unless ElementPolicy.new(current_user, element).update?
|
|
192
200
|
|
|
@@ -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
|
|
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,21 +247,27 @@ module Labimotion
|
|
|
247
247
|
end
|
|
248
248
|
|
|
249
249
|
def list_serialized_elements(params, current_user)
|
|
250
|
-
|
|
251
|
-
|
|
250
|
+
collection_id =
|
|
252
251
|
if params[:collection_id]
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
264
|
-
scope = Labimotion::Element.for_user(current_user.id)
|
|
270
|
+
Labimotion::Element.none
|
|
265
271
|
end
|
|
266
272
|
|
|
267
273
|
## TO DO: refactor labimotion
|
|
@@ -272,7 +278,7 @@ module Labimotion
|
|
|
272
278
|
layer, field = params[:sort_column].split('.')
|
|
273
279
|
|
|
274
280
|
element_klass = Labimotion::ElementKlass.find_by(name: params[:el_type])
|
|
275
|
-
allowed_fields = element_klass
|
|
281
|
+
allowed_fields = element_klass.properties_release.dig(Labimotion::Prop::LAYERS, layer, Labimotion::Prop::FIELDS)&.pluck('field') || []
|
|
276
282
|
|
|
277
283
|
if field.in?(allowed_fields)
|
|
278
284
|
query = ActiveRecord::Base.sanitize_sql(
|
|
@@ -25,98 +25,6 @@ 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
|
-
|
|
120
28
|
def validate_token(token)
|
|
121
29
|
# Find the request by access token
|
|
122
30
|
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 records
|
|
29
|
+
# Shared (synced) records
|
|
30
30
|
shared = apply_filters.call(
|
|
31
|
-
|
|
31
|
+
joins(collections: :sync_collections_users).where(sync_collections_users: { user_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 elements
|
|
144
|
+
# Shared (synced) elements
|
|
145
145
|
shared = apply_filters.call(
|
|
146
|
-
|
|
146
|
+
joins(collections: :sync_collections_users).where(sync_collections_users: { user_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.
|
|
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.
|
|
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
|
|
data/lib/labimotion/version.rb
CHANGED
data/lib/labimotion.rb
CHANGED
|
@@ -29,6 +29,7 @@ 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'
|
|
32
33
|
autoload :ElementVariationAPI, 'labimotion/apis/element_variation_api'
|
|
33
34
|
autoload :LabimotionDoiAPI, 'labimotion/apis/labimotion_doi_api'
|
|
34
35
|
autoload :LabimotionTemplateBrowseAPI, 'labimotion/apis/labimotion_template_browse_api'
|
|
@@ -71,6 +72,13 @@ module Labimotion
|
|
|
71
72
|
autoload :RepositoryHelpers, 'labimotion/helpers/repository_helpers'
|
|
72
73
|
autoload :VocabularyHelpers, 'labimotion/helpers/vocabulary_helpers'
|
|
73
74
|
|
|
75
|
+
######## Usecases
|
|
76
|
+
autoload :TemplateDoiHelpers, 'labimotion/usecases/template_doi_helpers'
|
|
77
|
+
autoload :BuildTemplateDoiXml, 'labimotion/usecases/build_template_doi_xml'
|
|
78
|
+
autoload :ReserveTemplateDoi, 'labimotion/usecases/reserve_template_doi'
|
|
79
|
+
autoload :ReleaseTemplateDoi, 'labimotion/usecases/release_template_doi'
|
|
80
|
+
autoload :UpdateTemplatePublicationMetadata, 'labimotion/usecases/update_template_publication_metadata'
|
|
81
|
+
|
|
74
82
|
######## Libs
|
|
75
83
|
autoload :Converter, 'labimotion/libs/converter'
|
|
76
84
|
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.
|
|
4
|
+
version: 2.2.0.rc16
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chia-Lin Lin
|
|
@@ -58,6 +58,7 @@ 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
|
|
61
62
|
- lib/labimotion/apis/element_variation_api.rb
|
|
62
63
|
- lib/labimotion/apis/exporter_api.rb
|
|
63
64
|
- lib/labimotion/apis/generic_dataset_api.rb
|
|
@@ -164,6 +165,11 @@ files:
|
|
|
164
165
|
- lib/labimotion/models/template_submission.rb
|
|
165
166
|
- lib/labimotion/models/vocabulary.rb
|
|
166
167
|
- lib/labimotion/models/wellplate.rb
|
|
168
|
+
- lib/labimotion/usecases/build_template_doi_xml.rb
|
|
169
|
+
- lib/labimotion/usecases/release_template_doi.rb
|
|
170
|
+
- lib/labimotion/usecases/reserve_template_doi.rb
|
|
171
|
+
- lib/labimotion/usecases/template_doi_helpers.rb
|
|
172
|
+
- lib/labimotion/usecases/update_template_publication_metadata.rb
|
|
167
173
|
- lib/labimotion/utils/con_state.rb
|
|
168
174
|
- lib/labimotion/utils/export_utils.rb
|
|
169
175
|
- lib/labimotion/utils/field_type.rb
|