labimotion 2.2.0.rc8 → 2.2.0.rc9

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: 2342f8963ebba3b102957a308070a94ecb87e49ba05641b22c32b5d49a0ff0a6
4
- data.tar.gz: 4ed1239dc5c9ed49c97c453abe0cff9c9656b54fd1faaaf5607f9ad9fc75a148
3
+ metadata.gz: fde410e148abdf50036fd9146e2faea1ccd5078f7c93ec99dbd1c3fa6680ee96
4
+ data.tar.gz: 3a63d545e4b1c9da579e582f956ead5e2d204c4f9078f47803322a4831ce8b50
5
5
  SHA512:
6
- metadata.gz: 53c17583059e150dd89e8308062ab4b658def233285940c0e0dbcc0aee9d3e15ce7d2fd7a05619f5416a4d5fd0a6507967a0a50b6b5f1894792864ee628bd682
7
- data.tar.gz: 267f0cf9ac7c535642d8bff93fafa66cc5661d6cd7d820cf473ab41540f8b4004c9c1403f2d208187e4c199c612bf65171b6b9159fba2a49140a0b052095d16a
6
+ metadata.gz: 1c2cd08a3c54a452994ab180b014e53545ade8ff6ebf65d19a06a23fe47770b7e27bb2c4e04e8c7c05d4a3ab45875240f5cdac29e3ae1e64f49281ea3d9a6bb3
7
+ data.tar.gz: 318fa9de8609f13f1074620e09aa24d62ad135d956e5415dfa582d262787897dbb70d119144a80227c4b567138fd69e871196dba072b6fb3e9eb5276ae0239f9
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'labimotion/version'
4
+
5
+ module Labimotion
6
+ # Dose Response Request API
7
+ class DoseRespRequestAPI < Grape::API
8
+ helpers Labimotion::ParamHelpers
9
+
10
+ resource :dose_resp_requests do
11
+ desc 'Get all dose response requests for current user'
12
+ params do
13
+ optional :element_id, type: Integer, desc: 'Filter by element ID'
14
+ optional :state, type: Integer, desc: 'Filter by state (-1, 0, 1, 2)'
15
+ optional :page, type: Integer, desc: 'Page number', default: 1
16
+ optional :per_page, type: Integer, desc: 'Items per page', default: 20
17
+ end
18
+ get do
19
+ requests = Labimotion::DoseRespRequest.where(created_by: current_user.id)
20
+
21
+ # Apply filters
22
+ requests = requests.where(element_id: params[:element_id]) if params[:element_id]
23
+ requests = requests.where(state: params[:state]) if params[:state]
24
+
25
+ # Pagination
26
+ page = params[:page] || 1
27
+ per_page = [params[:per_page] || 20, 100].min # Max 100 per page
28
+
29
+ total = requests.count
30
+ requests = requests.order(created_at: :desc)
31
+ .offset((page - 1) * per_page)
32
+ .limit(per_page)
33
+
34
+ {
35
+ requests: requests.map do |req|
36
+ {
37
+ id: req.id,
38
+ request_id: req.request_id,
39
+ element_id: req.element_id,
40
+ state: req.state,
41
+ state_label: state_label(req.state),
42
+ expires_at: req.expires_at,
43
+ created_at: req.created_at,
44
+ updated_at: req.updated_at,
45
+ first_accessed_at: req.first_accessed_at,
46
+ last_accessed_at: req.last_accessed_at,
47
+ access_count: req.access_count,
48
+ resp_message: req.resp_message,
49
+ active: req.active?
50
+ }
51
+ end,
52
+ pagination: {
53
+ page: page,
54
+ per_page: per_page,
55
+ total: total,
56
+ total_pages: (total.to_f / per_page).ceil
57
+ }
58
+ }
59
+ rescue StandardError => e
60
+ error!("Error: #{e.message}", 500)
61
+ end
62
+
63
+ desc 'Get a dose response request by ID'
64
+ params do
65
+ requires :id, type: Integer, desc: 'Request ID'
66
+ end
67
+ get ':id' do
68
+ request = Labimotion::DoseRespRequest.find_by(id: params[:id])
69
+ error!('Request not found', 404) unless request
70
+
71
+ # Check authorization
72
+ error!('Unauthorized', 403) unless request.created_by == current_user.id
73
+
74
+ {
75
+ id: request.id,
76
+ request_id: request.request_id,
77
+ element_id: request.element_id,
78
+ state: request.state,
79
+ state_label: state_label(request.state),
80
+ wellplates_metadata: request.wellplates_metadata,
81
+ input_metadata: request.input_metadata,
82
+ expires_at: request.expires_at,
83
+ revoked_at: request.revoked_at,
84
+ created_at: request.created_at,
85
+ updated_at: request.updated_at,
86
+ first_accessed_at: request.first_accessed_at,
87
+ last_accessed_at: request.last_accessed_at,
88
+ access_count: request.access_count,
89
+ resp_message: request.resp_message,
90
+ active: request.active?,
91
+ expired: request.expired?,
92
+ revoked: request.revoked?
93
+ }
94
+ rescue StandardError => e
95
+ error!("Error: #{e.message}", 500)
96
+ end
97
+
98
+ desc 'Update a dose response request'
99
+ params do
100
+ requires :id, type: Integer, desc: 'Request ID'
101
+ optional :state, type: Integer, desc: 'State', values: [-1, 0, 1, 2]
102
+ optional :resp_message, type: String, desc: 'Response message'
103
+ optional :wellplates_metadata, type: Hash, desc: 'Wellplates metadata'
104
+ end
105
+ put ':id' do
106
+ request = Labimotion::DoseRespRequest.find_by(id: params[:id])
107
+ error!('Request not found', 404) unless request
108
+
109
+ # Check authorization
110
+ error!('Unauthorized', 403) unless request.created_by == current_user.id
111
+
112
+ update_params = {}
113
+ update_params[:state] = params[:state] if params[:state]
114
+ update_params[:resp_message] = params[:resp_message] if params[:resp_message]
115
+ update_params[:wellplates_metadata] = params[:wellplates_metadata] if params[:wellplates_metadata]
116
+
117
+ request.update!(update_params)
118
+
119
+ {
120
+ success: true,
121
+ message: 'Request updated successfully',
122
+ request: {
123
+ id: request.id,
124
+ request_id: request.request_id,
125
+ state: request.state,
126
+ state_label: state_label(request.state),
127
+ resp_message: request.resp_message,
128
+ updated_at: request.updated_at
129
+ }
130
+ }
131
+ rescue ActiveRecord::RecordInvalid => e
132
+ error!("Validation error: #{e.message}", 422)
133
+ rescue StandardError => e
134
+ error!("Error: #{e.message}", 500)
135
+ end
136
+
137
+ desc 'Revoke a dose response request'
138
+ params do
139
+ requires :id, type: Integer, desc: 'Request ID'
140
+ end
141
+ post ':id/revoke' do
142
+ request = Labimotion::DoseRespRequest.find_by(id: params[:id])
143
+ error!('Request not found', 404) unless request
144
+
145
+ # Check authorization
146
+ error!('Unauthorized', 403) unless request.created_by == current_user.id
147
+
148
+ request.revoke!
149
+
150
+ {
151
+ success: true,
152
+ message: 'Request revoked successfully',
153
+ request: {
154
+ id: request.id,
155
+ request_id: request.request_id,
156
+ state: request.state,
157
+ revoked_at: request.revoked_at,
158
+ active: request.active?
159
+ }
160
+ }
161
+ rescue StandardError => e
162
+ error!("Error: #{e.message}", 500)
163
+ end
164
+
165
+ desc 'Delete a dose response request'
166
+ params do
167
+ requires :id, type: Integer, desc: 'Request ID'
168
+ end
169
+ delete ':id' do
170
+ request = Labimotion::DoseRespRequest.find_by(id: params[:id])
171
+ error!('Request not found', 404) unless request
172
+
173
+ # Check authorization
174
+ error!('Unauthorized', 403) unless request.created_by == current_user.id
175
+
176
+ # Soft delete if acts_as_paranoid is enabled
177
+ request.destroy
178
+
179
+ {
180
+ success: true,
181
+ message: 'Request deleted successfully'
182
+ }
183
+ rescue StandardError => e
184
+ error!("Error: #{e.message}", 500)
185
+ end
186
+
187
+ desc 'Get dose response requests by element ID'
188
+ params do
189
+ requires :element_id, type: Integer, desc: 'Element ID'
190
+ end
191
+ get 'by_element/:element_id' do
192
+ element = Labimotion::Element.find_by(id: params[:element_id])
193
+ error!('Element not found', 404) unless element
194
+
195
+ # Check if user has access to element
196
+ policy = ElementPolicy.new(current_user, element)
197
+ error!('Unauthorized', 403) unless policy.read?
198
+
199
+ requests = Labimotion::DoseRespRequest.where(element_id: params[:element_id])
200
+ .where(created_by: current_user.id)
201
+ .order(created_at: :desc)
202
+
203
+ {
204
+ element_id: element.id,
205
+ element_name: element.name,
206
+ requests: requests.map do |req|
207
+ {
208
+ id: req.id,
209
+ request_id: req.request_id,
210
+ state: req.state,
211
+ state_label: state_label(req.state),
212
+ expires_at: req.expires_at,
213
+ created_at: req.created_at,
214
+ access_count: req.access_count,
215
+ active: req.active?
216
+ }
217
+ end
218
+ }
219
+ rescue StandardError => e
220
+ error!("Error: #{e.message}", 500)
221
+ end
222
+ end
223
+
224
+ helpers do
225
+ def state_label(state)
226
+ case state
227
+ when Labimotion::DoseRespRequest::STATE_ERROR
228
+ 'error'
229
+ when Labimotion::DoseRespRequest::STATE_INITIAL
230
+ 'initial'
231
+ when Labimotion::DoseRespRequest::STATE_PROCESSING
232
+ 'processing'
233
+ when Labimotion::DoseRespRequest::STATE_COMPLETED
234
+ 'completed'
235
+ else
236
+ 'unknown'
237
+ end
238
+ end
239
+ end
240
+ end
241
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Labimotion
4
+ class ElementVariationAPI < Grape::API
5
+ rescue_from ActiveRecord::RecordNotFound do
6
+ error!('404 Element not found', 404)
7
+ end
8
+
9
+ resource :element_variations do
10
+ params do
11
+ requires :element_id, type: Integer, desc: 'Generic element id'
12
+ end
13
+ route_param :element_id do
14
+ before do
15
+ @element = Labimotion::Element.find(params[:element_id])
16
+ error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, @element).read?
17
+ end
18
+
19
+ desc 'Return element variations for a generic element'
20
+ get do
21
+ record = Labimotion::ElementVariation.find_or_initialize_by(element_id: @element.id)
22
+ present record, with: Labimotion::ElementVariationEntity, root: 'element_variation'
23
+ end
24
+
25
+ desc 'Upsert element variations for a generic element'
26
+ params do
27
+ requires :variations, type: Hash, desc: 'Variations keyed by row uuid'
28
+ optional :layout, type: Hash, desc: 'Column layout (selected/order/units/rowOrder)'
29
+ end
30
+ put do
31
+ error!('401 Unauthorized', 401) unless ElementPolicy.new(current_user, @element).update?
32
+
33
+ record = Labimotion::ElementVariation.find_or_initialize_by(element_id: @element.id)
34
+ record.variations = params[:variations] || {}
35
+ if params.key?(:layout) && record.class.column_names.include?('layout')
36
+ record.layout = params[:layout] || {}
37
+ end
38
+ record.save!
39
+
40
+ present record, with: Labimotion::ElementVariationEntity, root: 'element_variation'
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -179,10 +179,11 @@ module Labimotion
179
179
  end
180
180
 
181
181
  namespace :klass_revisions do
182
- desc 'list Generic Element Revisions'
182
+ desc 'list Generic Klass Revisions'
183
183
  params do
184
184
  requires :id, type: Integer, desc: 'Generic Element Klass Id'
185
185
  requires :klass, type: String, desc: 'Klass', values: %w[ElementKlass SegmentKlass DatasetKlass]
186
+ optional :limit, type: Integer, default: 10, desc: 'Max revisions returned'
186
187
  end
187
188
  get do
188
189
  list = list_klass_revisions(params)
@@ -197,6 +198,7 @@ module Labimotion
197
198
  desc 'list Generic Element Revisions'
198
199
  params do
199
200
  requires :id, type: Integer, desc: 'Generic Element Id'
201
+ optional :limit, type: Integer, default: 10, desc: 'Max revisions returned'
200
202
  end
201
203
  get do
202
204
  list = element_revisions(params)
@@ -241,14 +243,15 @@ module Labimotion
241
243
  end
242
244
 
243
245
  namespace :segment_revisions do
244
- desc 'list Generic Element Revisions'
246
+ desc 'list Generic Segment Revisions'
245
247
  params do
246
248
  optional :id, type: Integer, desc: 'Generic Element Id'
249
+ optional :limit, type: Integer, default: 10, desc: 'Max revisions returned'
247
250
  end
248
251
  get do
249
252
  klass = Labimotion::Segment.find(params[:id])
250
253
  list = klass.segments_revisions unless klass.nil?
251
- present list&.order(created_at: :desc)&.limit(10), with: Labimotion::SegmentRevisionEntity, root: 'revisions'
254
+ present list&.order(created_at: :desc)&.limit(params[:limit]), with: Labimotion::SegmentRevisionEntity, root: 'revisions'
252
255
  rescue StandardError => e
253
256
  Labimotion.log_exception(e, current_user)
254
257
  []
@@ -429,6 +432,7 @@ module Labimotion
429
432
  desc 'Return serialized elements of current user'
430
433
  params do
431
434
  optional :collection_id, type: Integer, desc: 'Collection id'
435
+ optional :sync_collection_id, type: Integer, desc: 'SyncCollectionsUser id'
432
436
  optional :el_type, type: String, desc: 'element klass name'
433
437
  optional :from_date, type: Integer, desc: 'created_date from in ms'
434
438
  optional :to_date, type: Integer, desc: 'created_date to in ms'
@@ -12,5 +12,8 @@ module Labimotion
12
12
  mount Labimotion::LabimotionHubAPI
13
13
  mount Labimotion::StandardLayerAPI
14
14
  mount Labimotion::VocabularyAPI
15
+ mount Labimotion::MttAPI
16
+ mount Labimotion::DoseRespRequestAPI
17
+ mount Labimotion::ElementVariationAPI
15
18
  end
16
19
  end
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'labimotion/version'
4
+ require 'labimotion/libs/export_element'
5
+ require 'labimotion/helpers/mtt_helpers'
6
+ require 'labimotion/models/dose_resp_output'
7
+
8
+ module Labimotion
9
+ # Generic Element API
10
+ class MttAPI < Grape::API
11
+ helpers Labimotion::ParamHelpers
12
+ helpers Labimotion::MttHelpers
13
+
14
+ namespace :public do
15
+ resource :mtt_apps do
16
+ route_param :token do
17
+ desc 'Download data from MTT app (GET endpoint)'
18
+ get do
19
+ download_json_to_external_app
20
+ end
21
+
22
+ desc 'Upload modified data from MTT app (POST endpoint)'
23
+ post do
24
+ upload_json_from_external_app
25
+ end
26
+ end
27
+ end
28
+ end
29
+
30
+ resource :mtt do
31
+ namespace :requests do
32
+ desc 'Get MTT requests for current user'
33
+ get do
34
+ # Get all requests created by current user
35
+ requests = Labimotion::DoseRespRequest
36
+ .includes(:dose_resp_outputs)
37
+ .where(created_by: current_user.id)
38
+ .order(created_at: :desc)
39
+
40
+ # Return formatted response
41
+ requests.map do |req|
42
+ {
43
+ id: req.id,
44
+ request_id: req.request_id,
45
+ element_id: req.element_id,
46
+ state: req.state,
47
+ state_name: case req.state
48
+ when Labimotion::DoseRespRequest::STATE_ERROR then 'error'
49
+ when Labimotion::DoseRespRequest::STATE_INITIAL then 'initial'
50
+ when Labimotion::DoseRespRequest::STATE_PROCESSING then 'processing'
51
+ when Labimotion::DoseRespRequest::STATE_COMPLETED then 'completed'
52
+ else 'unknown'
53
+ end,
54
+ created_at: req.created_at,
55
+ expires_at: req.expires_at,
56
+ expired: req.expired?,
57
+ revoked: req.revoked?,
58
+ active: req.active?,
59
+ resp_message: req.resp_message,
60
+ last_accessed_at: req.last_accessed_at,
61
+ access_count: req.access_count || 0,
62
+ outputs: req.dose_resp_outputs.map do |output|
63
+ {
64
+ id: output.id,
65
+ output_data: output.output_data,
66
+ notes: output.notes,
67
+ created_at: output.created_at
68
+ }
69
+ end
70
+ }
71
+ end
72
+ end
73
+
74
+ desc 'Delete one or multiple MTT requests'
75
+ params do
76
+ requires :ids, type: Array[Integer], desc: 'Array of request IDs to delete'
77
+ end
78
+ delete do
79
+ # Find requests belonging to current user
80
+ requests = Labimotion::DoseRespRequest.where(
81
+ id: params[:ids],
82
+ created_by: current_user.id
83
+ )
84
+
85
+ if requests.empty?
86
+ error!('No requests found or unauthorized', 404)
87
+ end
88
+
89
+ deleted_count = requests.count
90
+ requests.destroy_all
91
+
92
+ {
93
+ success: true,
94
+ message: "Successfully deleted #{deleted_count} request(s)",
95
+ deleted_count: deleted_count
96
+ }
97
+ rescue StandardError => e
98
+ error!("Error deleting requests: #{e.message}", 500)
99
+ end
100
+
101
+ desc 'Update MTT request'
102
+ params do
103
+ requires :id, type: Integer, desc: 'Request ID'
104
+ optional :state, type: Integer, desc: 'State (-1: error, 0: initial, 1: processing, 2: completed)', values: [-1, 0, 1, 2]
105
+ optional :resp_message, type: String, desc: 'Response message'
106
+ optional :wellplates_metadata, type: Hash, desc: 'Wellplates metadata'
107
+ optional :input_metadata, type: Hash, desc: 'Input metadata'
108
+ optional :revoked, type: Boolean, desc: 'Revoke access token'
109
+ end
110
+ patch ':id' do
111
+ # Find request belonging to current user
112
+ request = Labimotion::DoseRespRequest.find_by(
113
+ id: params[:id],
114
+ created_by: current_user.id
115
+ )
116
+
117
+ error!('Request not found or unauthorized', 404) unless request
118
+
119
+ # Prepare update attributes
120
+ update_attrs = {}
121
+ update_attrs[:state] = params[:state] if params.key?(:state)
122
+ update_attrs[:resp_message] = params[:resp_message] if params.key?(:resp_message)
123
+ update_attrs[:wellplates_metadata] = params[:wellplates_metadata] if params.key?(:wellplates_metadata)
124
+ update_attrs[:input_metadata] = params[:input_metadata] if params.key?(:input_metadata)
125
+ update_attrs[:revoked_at] = params[:revoked] ? Time.current : nil if params.key?(:revoked)
126
+
127
+ if request.update(update_attrs)
128
+ {
129
+ success: true,
130
+ message: 'Request updated successfully',
131
+ request: {
132
+ id: request.id,
133
+ request_id: request.request_id,
134
+ state: request.state,
135
+ state_name: case request.state
136
+ when Labimotion::DoseRespRequest::STATE_ERROR then 'error'
137
+ when Labimotion::DoseRespRequest::STATE_INITIAL then 'initial'
138
+ when Labimotion::DoseRespRequest::STATE_PROCESSING then 'processing'
139
+ when Labimotion::DoseRespRequest::STATE_COMPLETED then 'completed'
140
+ else 'unknown'
141
+ end,
142
+ resp_message: request.resp_message,
143
+ revoked: request.revoked?,
144
+ updated_at: request.updated_at
145
+ }
146
+ }
147
+ else
148
+ error!("Validation error: #{request.errors.full_messages.join(', ')}", 422)
149
+ end
150
+ rescue ActiveRecord::RecordNotFound
151
+ error!('Request not found', 404)
152
+ rescue StandardError => e
153
+ error!("Error updating request: #{e.message}", 500)
154
+ end
155
+ end
156
+
157
+ namespace :outputs do
158
+ desc 'Delete one or multiple MTT outputs'
159
+ params do
160
+ requires :ids, type: Array[Integer], desc: 'Array of output IDs to delete'
161
+ end
162
+ delete do
163
+ # Find outputs where the parent request belongs to current user
164
+ outputs = Labimotion::DoseRespOutput
165
+ .joins(:dose_resp_request)
166
+ .where(
167
+ id: params[:ids],
168
+ dose_resp_requests: { created_by: current_user.id }
169
+ )
170
+
171
+ if outputs.empty?
172
+ error!('No outputs found or unauthorized', 404)
173
+ end
174
+
175
+ deleted_count = outputs.count
176
+ outputs.destroy_all
177
+
178
+ {
179
+ success: true,
180
+ message: "Successfully deleted #{deleted_count} output(s)",
181
+ deleted_count: deleted_count
182
+ }
183
+ rescue StandardError => e
184
+ error!("Error deleting outputs: #{e.message}", 500)
185
+ end
186
+ end
187
+
188
+ namespace :create_mtt_request do
189
+ desc 'Create MTT assay request'
190
+ params do
191
+ use :create_mtt_request_params
192
+ end
193
+ post do
194
+ # Find element and wellplates
195
+ element = Labimotion::Element.find_by(id: params[:id])
196
+ error!('Element not found', 404) unless element
197
+ #byebug
198
+ # Verify user has update permission
199
+ error!('Unauthorized', 403) unless ElementPolicy.new(current_user, element).update?
200
+
201
+ wellplates = Wellplate.where(id: params[:wellplate_ids])
202
+ error!('No wellplates found', 404) if wellplates.empty?
203
+
204
+ # Generate wellplates metadata
205
+ wellplates_metadata = generate_wellplates_metadata(wellplates)
206
+
207
+ # Create DoseRespRequest record with token
208
+ dose_resp_request = Labimotion::DoseRespRequest.create!(
209
+ element_id: element.id,
210
+ wellplates_metadata: { wellplates: wellplates_metadata },
211
+ input_metadata: {
212
+ wellplate_ids: params[:wellplate_ids],
213
+ element_id: element.id,
214
+ element_name: element.name,
215
+ created_at: Time.current
216
+ },
217
+ state: Labimotion::DoseRespRequest::STATE_INITIAL,
218
+ created_by: current_user&.id,
219
+ expires_at: TPA_EXPIRATION.from_now
220
+ )
221
+
222
+ # Generate external app URL with token
223
+ token_uri = token_url(dose_resp_request)
224
+ external_app_url = get_external_app_url
225
+
226
+ # Format: "#{@app.url}?url=#{CGI.escape(token_uri)}&type=ThirdPartyApp"
227
+ "#{external_app_url}?method=DoseResponse&url=#{CGI.escape(token_uri)}"
228
+ rescue ActiveRecord::RecordInvalid => e
229
+ error!("Validation error: #{e.message}", 422)
230
+ rescue ActiveRecord::RecordNotFound => e
231
+ error!("Not found: #{e.message}", 404)
232
+ rescue StandardError => e
233
+ error!("Error: #{e.message}", 500)
234
+ end
235
+ end
236
+ end
237
+ end
238
+ end
@@ -23,6 +23,7 @@ module Labimotion
23
23
  expose! :uuid
24
24
  expose! :user_labels
25
25
  expose! :preview_attachment # align with eln change
26
+ expose! :variations_count
26
27
  end
27
28
 
28
29
  with_options(anonymize_below: 10) do
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'labimotion/entities/application_entity'
4
+
5
+ module Labimotion
6
+ class ElementVariationEntity < Labimotion::ApplicationEntity
7
+ expose :id
8
+ expose :element_id, as: :elementId
9
+ expose :variations
10
+ expose :layout
11
+
12
+ def variations
13
+ rows = object.variations_hash
14
+ rows.transform_values do |row|
15
+ next row unless row.is_a?(Hash)
16
+
17
+ row.symbolize_keys.slice(:uuid, :name, :properties, :metadata, :segments).tap do |slim|
18
+ slim[:properties] = (slim[:properties] || {})
19
+ slim[:metadata] = (slim[:metadata] || {}).slice('notes', 'analyses', 'group', :notes, :analyses, :group)
20
+ slim[:segments] = (slim[:segments] || {})
21
+ end
22
+ end
23
+ end
24
+
25
+ def layout
26
+ object.layout_hash
27
+ end
28
+ end
29
+ end