labimotion 2.1.0.rc3 → 2.1.0.rc5
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/exporter_api.rb +269 -0
- data/lib/labimotion/apis/generic_dataset_api.rb +19 -2
- data/lib/labimotion/apis/generic_element_api.rb +82 -1
- data/lib/labimotion/apis/labimotion_api.rb +1 -0
- data/lib/labimotion/helpers/dataset_helpers.rb +25 -2
- data/lib/labimotion/helpers/exporter_helpers.rb +139 -0
- data/lib/labimotion/helpers/param_helpers.rb +6 -0
- data/lib/labimotion/libs/dataset_builder.rb +2 -1
- data/lib/labimotion/libs/sample_association.rb +4 -3
- data/lib/labimotion/libs/template_matcher.rb +39 -0
- data/lib/labimotion/libs/xlsx_exporter.rb +285 -0
- data/lib/labimotion/models/concerns/element_fetchable.rb +53 -0
- data/lib/labimotion/models/dataset_klass.rb +18 -1
- data/lib/labimotion/models/device_description.rb +36 -0
- data/lib/labimotion/models/element.rb +30 -0
- data/lib/labimotion/models/reaction.rb +36 -0
- data/lib/labimotion/models/research_plan.rb +40 -0
- data/lib/labimotion/models/sample.rb +36 -0
- data/lib/labimotion/models/screen.rb +36 -0
- data/lib/labimotion/models/wellplate.rb +40 -0
- data/lib/labimotion/utils/utils.rb +42 -0
- data/lib/labimotion/version.rb +1 -1
- data/lib/labimotion.rb +12 -0
- metadata +38 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e23b274f29ceb300bbb97f91830dbe2fdc454966d229e88608cd434b6586a3b3
|
|
4
|
+
data.tar.gz: da620e95cb04ce10a69cba25d8cb63f22cb1e21142a1a6a3c93d9fecc33840aa
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: db1827e8ad8c56f05c55215f3cd5e3830174f937875c2af49e7899dc4875180364111b1788eecc430b6f3ca8b39e3e0234eb21a3c1036c3b7ab064676f8bb4d1
|
|
7
|
+
data.tar.gz: 3f1667aa18030745ab178330cac70f234d4aa3eab46e3622546aeb4268aaf379e9d8dd0369060324327dd3db67efa2da6e94b6568feb33a88e987bec8030a737
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Labimotion
|
|
4
|
+
# ExporterAPI handles exporting data to various formats (e.g., XLSX)
|
|
5
|
+
# The class length is justified due to the comprehensive helper methods needed for export functionality
|
|
6
|
+
class ExporterAPI < Grape::API
|
|
7
|
+
include Grape::Kaminari
|
|
8
|
+
|
|
9
|
+
helpers Labimotion::ParamHelpers
|
|
10
|
+
helpers Labimotion::ExporterHelpers
|
|
11
|
+
|
|
12
|
+
resource :exporter do
|
|
13
|
+
resource :table_xlsx do
|
|
14
|
+
desc 'Export single table field to XLSX format'
|
|
15
|
+
params do
|
|
16
|
+
use :table_xlsx_params
|
|
17
|
+
end
|
|
18
|
+
route_param :id do
|
|
19
|
+
before do
|
|
20
|
+
validate_and_authorize_request!
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
get do
|
|
24
|
+
layer = fetch_layer
|
|
25
|
+
field = fetch_field(layer)
|
|
26
|
+
validate_table_field!(field)
|
|
27
|
+
|
|
28
|
+
headers, expanded_sub_fields = build_table_headers(field)
|
|
29
|
+
rows = build_table_rows(field, expanded_sub_fields)
|
|
30
|
+
|
|
31
|
+
export_to_xlsx(layer, field, headers, rows)
|
|
32
|
+
rescue StandardError => e
|
|
33
|
+
Labimotion.log_exception(e, current_user)
|
|
34
|
+
error!("500 Internal Server Error: #{e.message}", 500)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# rubocop:disable Metrics/BlockLength
|
|
41
|
+
# The helpers block is necessarily large due to the many specialized helper methods
|
|
42
|
+
# for data fetching, validation, formatting, and export functionality
|
|
43
|
+
helpers do
|
|
44
|
+
# Validate and authorize the request, sets @element instance variable
|
|
45
|
+
def validate_and_authorize_request!
|
|
46
|
+
klass = fetch_klass
|
|
47
|
+
@element = klass.find_by(id: params[:id])
|
|
48
|
+
|
|
49
|
+
error!('404 Not Found', 404) if @element.nil?
|
|
50
|
+
|
|
51
|
+
authorize_element_access!
|
|
52
|
+
rescue ActiveRecord::RecordNotFound
|
|
53
|
+
error!('404 Not Found', 404)
|
|
54
|
+
rescue NameError => e
|
|
55
|
+
Labimotion.log_exception(e, current_user)
|
|
56
|
+
error!('400 Bad Request - Invalid type', 400)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Fetch the element or segment class based on params
|
|
60
|
+
def fetch_klass
|
|
61
|
+
Labimotion::Utils.resolve_class(params[:klass])
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Authorize element access based on user permissions
|
|
65
|
+
def authorize_element_access!
|
|
66
|
+
element_policy = if params[:klass] == 'Element'
|
|
67
|
+
ElementPolicy.new(current_user,
|
|
68
|
+
@element)
|
|
69
|
+
else
|
|
70
|
+
ElementPolicy.new(current_user,
|
|
71
|
+
@element.element)
|
|
72
|
+
end
|
|
73
|
+
matrix_name = params[:klass] == 'Element' ? 'genericElement' : 'segment'
|
|
74
|
+
|
|
75
|
+
return if current_user.matrix_check_by_name(matrix_name) && element_policy.read?
|
|
76
|
+
|
|
77
|
+
error!('401 Unauthorized', 401)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Fetch layer from element properties
|
|
81
|
+
def fetch_layer
|
|
82
|
+
properties = @element.properties
|
|
83
|
+
layers = properties[Labimotion::Prop::LAYERS] || {}
|
|
84
|
+
layer_key = params[:layer_id].to_s
|
|
85
|
+
layer = layers[layer_key]
|
|
86
|
+
|
|
87
|
+
error!('404 Layer Not Found', 404) if layer.nil?
|
|
88
|
+
layer
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Fetch field from layer
|
|
92
|
+
def fetch_field(layer)
|
|
93
|
+
fields = layer[Labimotion::Prop::FIELDS] || []
|
|
94
|
+
field = fields.find { |f| f['field'] == params[:field_id].to_s }
|
|
95
|
+
|
|
96
|
+
error!('404 Field Not Found', 404) if field.nil?
|
|
97
|
+
field
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Validate that the field is a table type
|
|
101
|
+
def validate_table_field!(field)
|
|
102
|
+
return if field['type'] == Labimotion::FieldType::TABLE
|
|
103
|
+
|
|
104
|
+
error!('400 Bad Request - Field is not a table type', 400)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Build table headers from field definition, expands DRAG_SAMPLE and DRAG_MOLECULE columns
|
|
108
|
+
def build_table_headers(field)
|
|
109
|
+
sub_fields = field.fetch('sub_fields', [])
|
|
110
|
+
headers = []
|
|
111
|
+
expanded_sub_fields = []
|
|
112
|
+
|
|
113
|
+
sub_fields.each do |sf|
|
|
114
|
+
base_col_name = normalize_column_name(sf['col_name'])
|
|
115
|
+
|
|
116
|
+
if expandable_field?(sf['type'])
|
|
117
|
+
headers, expanded_sub_fields = expand_field_columns(sf, base_col_name, headers, expanded_sub_fields)
|
|
118
|
+
else
|
|
119
|
+
headers << base_col_name
|
|
120
|
+
expanded_sub_fields << sf
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
[headers, expanded_sub_fields]
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Expand field columns for DRAG_SAMPLE and DRAG_MOLECULE with sub-headers
|
|
128
|
+
def expand_field_columns(sub_field, base_col_name, headers, expanded_sub_fields)
|
|
129
|
+
sub_headers = sub_field['value'].to_s.split(';').reject(&:empty?)
|
|
130
|
+
|
|
131
|
+
# Always add the base column first (for SVG image link)
|
|
132
|
+
headers << base_col_name
|
|
133
|
+
expanded_sub_fields << sub_field
|
|
134
|
+
|
|
135
|
+
# For DRAG_SAMPLE, add a second base column for short_label
|
|
136
|
+
if sub_field['type'] == Labimotion::FieldType::DRAG_SAMPLE
|
|
137
|
+
headers << 'short_label'
|
|
138
|
+
expanded_sub_fields << {
|
|
139
|
+
'id' => sub_field['id'],
|
|
140
|
+
'type' => sub_field['type'],
|
|
141
|
+
'sub_header' => 'short_label',
|
|
142
|
+
'original' => sub_field,
|
|
143
|
+
'is_base_column' => true
|
|
144
|
+
}
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Add extra columns for each sub-header
|
|
148
|
+
sub_headers.each do |sub_header|
|
|
149
|
+
headers << sub_header
|
|
150
|
+
expanded_sub_fields << {
|
|
151
|
+
'id' => sub_field['id'],
|
|
152
|
+
'type' => sub_field['type'],
|
|
153
|
+
'sub_header' => sub_header,
|
|
154
|
+
'original' => sub_field
|
|
155
|
+
}
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
[headers, expanded_sub_fields]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Build table rows from field data
|
|
162
|
+
def build_table_rows(field, expanded_sub_fields)
|
|
163
|
+
sub_values = field.fetch('sub_values', [])
|
|
164
|
+
|
|
165
|
+
sub_values.map do |sub_val|
|
|
166
|
+
expanded_sub_fields.map do |exp_field|
|
|
167
|
+
if exp_field.is_a?(Hash) && exp_field['sub_header']
|
|
168
|
+
format_expanded_cell(sub_val, exp_field)
|
|
169
|
+
else
|
|
170
|
+
format_table_cell(sub_val, exp_field)
|
|
171
|
+
end
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Export data to XLSX format
|
|
177
|
+
def export_to_xlsx(layer, field, headers, rows)
|
|
178
|
+
element_name = @element.is_a?(Labimotion::Segment) ? @element.element.name : @element.name
|
|
179
|
+
segment_name = @element.is_a?(Labimotion::Segment) ? @element.segment_klass.label : ''
|
|
180
|
+
selected_layer = "#{layer['label'] || ExporterHelpers::CONST_UNNAMED} (#{layer['key']})"
|
|
181
|
+
selected_field = field['label'] || ExporterHelpers::CONST_UNNAMED
|
|
182
|
+
|
|
183
|
+
filename = generate_filename(element_name, segment_name, selected_layer, selected_field)
|
|
184
|
+
exporter = Labimotion::XlsxExporter.new(filename)
|
|
185
|
+
|
|
186
|
+
worksheet_params = {
|
|
187
|
+
exporter: exporter,
|
|
188
|
+
element_name: element_name,
|
|
189
|
+
layer_name: selected_layer,
|
|
190
|
+
field_name: selected_field,
|
|
191
|
+
headers: headers,
|
|
192
|
+
rows: rows
|
|
193
|
+
}
|
|
194
|
+
worksheet_params[:segment_name] = segment_name unless segment_name.empty?
|
|
195
|
+
populate_worksheet(worksheet_params)
|
|
196
|
+
|
|
197
|
+
# Set response headers and format
|
|
198
|
+
configure_xlsx_response(exporter)
|
|
199
|
+
|
|
200
|
+
exporter.read
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Build metadata array for worksheet
|
|
204
|
+
def build_metadata(params)
|
|
205
|
+
metadata = [['Element:', params[:element_name]]]
|
|
206
|
+
metadata << ['Segment:', params[:segment_name]] if params[:segment_name].present?
|
|
207
|
+
metadata.push(
|
|
208
|
+
['Layer:', params[:layer_name]],
|
|
209
|
+
['Field:', params[:field_name]],
|
|
210
|
+
['Exported at:', Time.zone.now.strftime('%Y-%m-%d %H:%M:%S')],
|
|
211
|
+
['Exported by:', current_user.name]
|
|
212
|
+
)
|
|
213
|
+
metadata
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Populate worksheet with data
|
|
217
|
+
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
218
|
+
def populate_worksheet(params)
|
|
219
|
+
# Store params in local variables for use in the instance_eval block
|
|
220
|
+
table_headers = params[:headers]
|
|
221
|
+
table_rows = params[:rows]
|
|
222
|
+
metadata = build_metadata(params)
|
|
223
|
+
|
|
224
|
+
params[:exporter].add_worksheet('Table Data') do |sheet|
|
|
225
|
+
# Add metadata section
|
|
226
|
+
sheet.add_section('Table Information', metadata)
|
|
227
|
+
|
|
228
|
+
# Add table data with headers
|
|
229
|
+
header_row_index = sheet.add_header(table_headers)
|
|
230
|
+
|
|
231
|
+
# Add data rows with hyperlink support
|
|
232
|
+
table_rows.each do |row_data|
|
|
233
|
+
# Process row data to handle hyperlinks
|
|
234
|
+
processed_row = row_data.map do |cell_value|
|
|
235
|
+
cell_value.is_a?(Hash) && cell_value[:hyperlink] ? cell_value[:text] : cell_value
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Add the row
|
|
239
|
+
row_index = sheet.add_row(processed_row)
|
|
240
|
+
|
|
241
|
+
# Add hyperlinks to cells that need them
|
|
242
|
+
row_data.each_with_index do |cell_value, col_index|
|
|
243
|
+
next unless cell_value.is_a?(Hash) && cell_value[:hyperlink]
|
|
244
|
+
|
|
245
|
+
sheet.add_hyperlink(row_index, col_index, cell_value[:hyperlink])
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Auto-fit columns and freeze panes
|
|
250
|
+
sheet.auto_fit_columns
|
|
251
|
+
sheet.freeze_panes(header_row_index + 1, 0) if table_headers.any?
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
255
|
+
|
|
256
|
+
# Set XLSX response headers with proper encoding
|
|
257
|
+
def configure_xlsx_response(exporter)
|
|
258
|
+
env['api.format'] = :binary
|
|
259
|
+
content_type 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
|
260
|
+
|
|
261
|
+
# Use both filename and filename* for better browser compatibility
|
|
262
|
+
filename = exporter.filename
|
|
263
|
+
encoded_filename = URI.encode_www_form_component(filename)
|
|
264
|
+
header('Content-Disposition', "attachment; filename=\"#{filename}\"; filename*=UTF-8''#{encoded_filename}")
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
# rubocop:enable Metrics/BlockLength
|
|
268
|
+
end
|
|
269
|
+
end
|
|
@@ -4,6 +4,7 @@ module Labimotion
|
|
|
4
4
|
## Generic Dataset API
|
|
5
5
|
class GenericDatasetAPI < Grape::API
|
|
6
6
|
include Grape::Kaminari
|
|
7
|
+
|
|
7
8
|
helpers Labimotion::GenericHelpers
|
|
8
9
|
helpers Labimotion::DatasetHelpers
|
|
9
10
|
|
|
@@ -24,7 +25,8 @@ module Labimotion
|
|
|
24
25
|
end
|
|
25
26
|
get do
|
|
26
27
|
list = klass_list(params[:is_active], params[:displayed_in_list])
|
|
27
|
-
serialized_data = Labimotion::DatasetKlassEntity.represent(list,
|
|
28
|
+
serialized_data = Labimotion::DatasetKlassEntity.represent(list,
|
|
29
|
+
displayed_in_list: params[:displayed_in_list])
|
|
28
30
|
{ mc: 'ss00', data: serialized_data }
|
|
29
31
|
rescue StandardError => e
|
|
30
32
|
Labimotion.log_exception(e, current_user)
|
|
@@ -32,7 +34,8 @@ module Labimotion
|
|
|
32
34
|
end
|
|
33
35
|
end
|
|
34
36
|
|
|
35
|
-
# Deprecated: This namespace is no longer used, but kept for backward compatibility.
|
|
37
|
+
# Deprecated: This namespace is no longer used, but kept for backward compatibility.
|
|
38
|
+
# It is replaced by `list_klass`.
|
|
36
39
|
namespace :list_dataset_klass do
|
|
37
40
|
desc 'list Generic Dataset Klass'
|
|
38
41
|
params do
|
|
@@ -65,6 +68,20 @@ module Labimotion
|
|
|
65
68
|
{ error: e.message }
|
|
66
69
|
end
|
|
67
70
|
end
|
|
71
|
+
|
|
72
|
+
namespace :find_template do
|
|
73
|
+
desc 'Find best matching template for given OLS term ID'
|
|
74
|
+
params do
|
|
75
|
+
requires :ols_term_id, type: String, desc: 'OLS Term ID (e.g., CHMO:0000470)'
|
|
76
|
+
end
|
|
77
|
+
get do
|
|
78
|
+
result = find_best_match_template(params[:ols_term_id])
|
|
79
|
+
result
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
Labimotion.log_exception(e, current_user)
|
|
82
|
+
raise e
|
|
83
|
+
end
|
|
84
|
+
end
|
|
68
85
|
end
|
|
69
86
|
end
|
|
70
87
|
end
|
|
@@ -59,6 +59,65 @@ module Labimotion
|
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
+
namespace :search_by_like do
|
|
63
|
+
desc 'Search elements by name (case-insensitive like search)'
|
|
64
|
+
params do
|
|
65
|
+
requires :name, type: String, desc: 'Search query for element name'
|
|
66
|
+
requires :short_label, type: String, desc: 'Search query for element short label'
|
|
67
|
+
requires :klass_id, type: Integer, desc: 'Filter by element klass id'
|
|
68
|
+
optional :limit, type: Integer, desc: 'Maximum number of results', default: 20
|
|
69
|
+
end
|
|
70
|
+
get do
|
|
71
|
+
scope = Labimotion::Element.fetch_for_user(
|
|
72
|
+
current_user.id,
|
|
73
|
+
name: params[:name],
|
|
74
|
+
short_label: params[:short_label],
|
|
75
|
+
klass_id: params[:klass_id],
|
|
76
|
+
limit: params[:limit]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
results = scope.map do |element|
|
|
80
|
+
Labimotion::ElementLookupEntity.represent(element)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
{ elements: results, total_count: results.count }
|
|
84
|
+
rescue StandardError => e
|
|
85
|
+
Labimotion.log_exception(e, current_user)
|
|
86
|
+
{ elements: [], total_count: 0, error: e.message }
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
namespace :search_basic_by_like do
|
|
91
|
+
desc 'Search basic elements by name and short label (case-insensitive like search)'
|
|
92
|
+
params do
|
|
93
|
+
requires :klass_name, type: String, desc: 'Class name (device_description or wellplate or ...etc.)', default: 'device_description'
|
|
94
|
+
requires :name, type: String, desc: 'Search query for basic element name'
|
|
95
|
+
requires :short_label, type: String, desc: 'Search query for basic element short label'
|
|
96
|
+
optional :limit, type: Integer, desc: 'Maximum number of results', default: 20
|
|
97
|
+
end
|
|
98
|
+
get do
|
|
99
|
+
# Convert snake_case to PascalCase (e.g. device_description -> DeviceDescription)
|
|
100
|
+
klass_name = params[:klass_name].camelize
|
|
101
|
+
klass = "Labimotion::#{klass_name}".constantize
|
|
102
|
+
|
|
103
|
+
scope = klass.fetch_for_user(
|
|
104
|
+
current_user.id,
|
|
105
|
+
name: params[:name],
|
|
106
|
+
short_label: params[:short_label],
|
|
107
|
+
limit: params[:limit]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
results = scope.map do |record|
|
|
111
|
+
Labimotion::ElementLookupEntity.represent(record)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
{ elements: results, total_count: results.count }
|
|
115
|
+
rescue StandardError => e
|
|
116
|
+
Labimotion.log_exception(e, current_user)
|
|
117
|
+
{ elements: [], total_count: 0, error: e.message }
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
62
121
|
namespace :export do
|
|
63
122
|
desc 'export element'
|
|
64
123
|
params do
|
|
@@ -225,12 +284,13 @@ module Labimotion
|
|
|
225
284
|
desc 'list Generic Element Klass'
|
|
226
285
|
params do
|
|
227
286
|
optional :is_generic, type: Boolean, desc: 'Is Generic or Non-Generic Element'
|
|
228
|
-
optional :is_active, type: Boolean, desc: 'Active or Inactive
|
|
287
|
+
optional :is_active, type: Boolean, desc: 'Active or Inactive'
|
|
229
288
|
optional :displayed_in_list, type: Boolean, desc: 'Display in list format', default: true
|
|
230
289
|
end
|
|
231
290
|
get do
|
|
232
291
|
scope = params[:displayed_in_list] ? Labimotion::ElementKlass.for_list_display : Labimotion::ElementKlass.all
|
|
233
292
|
scope = scope.where(is_generic: params[:is_generic]) if params.key?(:is_generic)
|
|
293
|
+
scope = scope.where(is_active: params[:is_active]) if params.key?(:is_active)
|
|
234
294
|
|
|
235
295
|
list = scope.sort_by(&:place)
|
|
236
296
|
present list, with: Labimotion::ElementKlassEntity, root: 'klass', displayed_in_list: params[:displayed_in_list]
|
|
@@ -465,4 +525,25 @@ module Labimotion
|
|
|
465
525
|
end
|
|
466
526
|
end
|
|
467
527
|
end
|
|
528
|
+
|
|
529
|
+
# Entity for element lookup by name response
|
|
530
|
+
class ElementLookupEntity < Grape::Entity
|
|
531
|
+
expose :id
|
|
532
|
+
expose :name
|
|
533
|
+
expose :short_label do |element|
|
|
534
|
+
element.respond_to?(:short_label) ? element.short_label : nil
|
|
535
|
+
end
|
|
536
|
+
expose :element_klass_id, as: :element_klass_id do |element|
|
|
537
|
+
element.element_klass&.id
|
|
538
|
+
end
|
|
539
|
+
expose :klass_label, as: :klass_label do |element|
|
|
540
|
+
element.element_klass&.label
|
|
541
|
+
end
|
|
542
|
+
expose :klass_name, as: :klass_name do |element|
|
|
543
|
+
element.element_klass&.name
|
|
544
|
+
end
|
|
545
|
+
expose :klass_icon, as: :klass_icon do |element|
|
|
546
|
+
element.element_klass&.icon_name
|
|
547
|
+
end
|
|
548
|
+
end
|
|
468
549
|
end
|
|
@@ -33,18 +33,41 @@ module Labimotion
|
|
|
33
33
|
ds = Labimotion::DatasetKlass.find_by(ols_term_id: attributes['ols_term_id'])
|
|
34
34
|
ds.update!(attributes)
|
|
35
35
|
ds.create_klasses_revision(current_user)
|
|
36
|
-
{ status: 'success',
|
|
36
|
+
{ status: 'success',
|
|
37
|
+
message: "This dataset: [#{attributes['label']}] has been upgraded to the version: #{attributes['version']}!" }
|
|
37
38
|
end
|
|
38
39
|
else
|
|
39
40
|
attributes['created_by'] = current_user.id
|
|
40
41
|
ds = Labimotion::DatasetKlass.create!(attributes)
|
|
41
42
|
ds.create_klasses_revision(current_user)
|
|
42
|
-
{ status: 'success',
|
|
43
|
+
{ status: 'success',
|
|
44
|
+
message: "The dataset: #{attributes['label']} has been created using version: #{attributes['version']}!" }
|
|
43
45
|
end
|
|
44
46
|
rescue StandardError => e
|
|
45
47
|
Labimotion.log_exception(e, current_user)
|
|
46
48
|
# { error: e.message }
|
|
47
49
|
raise e
|
|
48
50
|
end
|
|
51
|
+
|
|
52
|
+
def find_best_match_template(ols_term_id)
|
|
53
|
+
result = Labimotion::TemplateMatcher.find_best_match(ols_term_id)
|
|
54
|
+
if result[:template]
|
|
55
|
+
build_template_response(result[:template], result[:match_type], result[:info_messages])
|
|
56
|
+
else
|
|
57
|
+
{ error: '', data: {}, info: "No matching template found for term id [#{ols_term_id}]" }
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private
|
|
62
|
+
|
|
63
|
+
def build_template_response(template, match_type, info_messages)
|
|
64
|
+
response = {
|
|
65
|
+
error: '',
|
|
66
|
+
data: Labimotion::DatasetKlassEntity.represent(template, displayed_in_list: false),
|
|
67
|
+
match_type: match_type
|
|
68
|
+
}
|
|
69
|
+
response[:info] = info_messages.join('; ') if info_messages.any?
|
|
70
|
+
response
|
|
71
|
+
end
|
|
49
72
|
end
|
|
50
73
|
end
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Labimotion
|
|
4
|
+
# ExporterHelpers provides utility methods for formatting and processing export data
|
|
5
|
+
module ExporterHelpers
|
|
6
|
+
# Constants
|
|
7
|
+
CONST_UNNAMED = '(Unnamed)'
|
|
8
|
+
|
|
9
|
+
# Normalize column name, returns CONST_UNNAMED for blank values
|
|
10
|
+
def normalize_column_name(col_name)
|
|
11
|
+
return CONST_UNNAMED if col_name.nil? || col_name.to_s.strip.empty?
|
|
12
|
+
|
|
13
|
+
col_name
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Check if field type should be expanded (DRAG_SAMPLE or DRAG_MOLECULE)
|
|
17
|
+
def expandable_field?(field_type)
|
|
18
|
+
[Labimotion::FieldType::DRAG_SAMPLE, Labimotion::FieldType::DRAG_MOLECULE].include?(field_type)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Generate sanitized filename for export
|
|
22
|
+
def generate_filename(element_name, segment_name, layer_name, field_name)
|
|
23
|
+
timestamp = Time.zone.now.strftime('%Y%m%d_%H%M%S')
|
|
24
|
+
parts = [element_name]
|
|
25
|
+
parts << segment_name unless segment_name.empty?
|
|
26
|
+
parts.push(layer_name, field_name, timestamp)
|
|
27
|
+
parts.join('_').gsub(/\s+/, '_').gsub(/[()]/, '(' => '[', ')' => ']')
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Format table cell value based on field type
|
|
31
|
+
def format_table_cell(sub_val, sub_field)
|
|
32
|
+
return '' if sub_field.fetch('id', nil).nil? || sub_val[sub_field['id']].nil?
|
|
33
|
+
|
|
34
|
+
case sub_field['type']
|
|
35
|
+
when Labimotion::FieldType::DRAG_SAMPLE, Labimotion::FieldType::DRAG_MOLECULE
|
|
36
|
+
format_drag_field_cell(sub_val, sub_field)
|
|
37
|
+
when Labimotion::FieldType::SELECT
|
|
38
|
+
format_select_cell(sub_val, sub_field)
|
|
39
|
+
when Labimotion::FieldType::SYSTEM_DEFINED
|
|
40
|
+
format_system_defined_cell(sub_val, sub_field)
|
|
41
|
+
else
|
|
42
|
+
format_default_cell(sub_val, sub_field)
|
|
43
|
+
end
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
Labimotion.log_exception(e)
|
|
46
|
+
''
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Format expanded cell for DRAG_SAMPLE or DRAG_MOLECULE with sub-headers
|
|
50
|
+
def format_expanded_cell(sub_val, exp_field)
|
|
51
|
+
field_id = exp_field['id']
|
|
52
|
+
sub_header = exp_field['sub_header']
|
|
53
|
+
|
|
54
|
+
return '' if field_id.nil? || sub_val[field_id].nil?
|
|
55
|
+
|
|
56
|
+
val = sub_val[field_id]['value'] || {}
|
|
57
|
+
return '' if val.blank? || !val.is_a?(Hash)
|
|
58
|
+
|
|
59
|
+
property_key = map_sub_header_to_property(sub_header)
|
|
60
|
+
val[property_key].to_s
|
|
61
|
+
rescue StandardError => e
|
|
62
|
+
Labimotion.log_exception(e)
|
|
63
|
+
''
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Map sub-header to property key
|
|
67
|
+
def map_sub_header_to_property(sub_header)
|
|
68
|
+
property_map = {
|
|
69
|
+
'name' => 'el_name',
|
|
70
|
+
'label' => 'el_label',
|
|
71
|
+
'short_label' => 'el_short_label',
|
|
72
|
+
'external_label' => 'el_external_label',
|
|
73
|
+
'molecular_weight' => 'el_molecular_weight',
|
|
74
|
+
'smiles' => 'el_smiles',
|
|
75
|
+
'inchikey' => 'el_inchikey',
|
|
76
|
+
'iupac' => 'el_iupac',
|
|
77
|
+
'sum_formula' => 'el_sum_formula',
|
|
78
|
+
'decoupled' => 'el_decoupled'
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
property_map[sub_header] || "el_#{sub_header}"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Format drag field cell (DRAG_SAMPLE or DRAG_MOLECULE), returns hyperlink or label
|
|
85
|
+
def format_drag_field_cell(sub_val, sub_field)
|
|
86
|
+
val = sub_val[sub_field['id']]['value'] || {}
|
|
87
|
+
return '' if val.blank?
|
|
88
|
+
|
|
89
|
+
svg_url = val['el_svg'].to_s
|
|
90
|
+
if svg_url.present?
|
|
91
|
+
full_url = URI.join(Rails.application.config.root_url, svg_url).to_s
|
|
92
|
+
{ hyperlink: full_url, text: '[image link]' }
|
|
93
|
+
else
|
|
94
|
+
val['el_label'].to_s
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Format select cell
|
|
99
|
+
def format_select_cell(sub_val, sub_field)
|
|
100
|
+
sub_val[sub_field['id']]['value'].to_s
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Format system-defined cell with unit
|
|
104
|
+
def format_system_defined_cell(sub_val, sub_field)
|
|
105
|
+
value = sub_val[sub_field['id']]['value'].to_s
|
|
106
|
+
unit = find_unit_label(sub_val, sub_field)
|
|
107
|
+
|
|
108
|
+
unit.present? ? "#{value} #{unit}" : value
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Find unit label for system-defined field
|
|
112
|
+
def find_unit_label(sub_val, sub_field)
|
|
113
|
+
value_system = extract_value_system(sub_val, sub_field)
|
|
114
|
+
find_unit_by_key(sub_field['option_layers'], value_system)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Extract value_system from sub_val or sub_field
|
|
118
|
+
def extract_value_system(sub_val, sub_field)
|
|
119
|
+
field_data = sub_val[sub_field['id']]
|
|
120
|
+
field_data.is_a?(Hash) ? field_data['value_system'] : sub_field['value_system']
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Find unit label by field and key
|
|
124
|
+
def find_unit_by_key(option_layers, value_system)
|
|
125
|
+
field_config = Labimotion::Units::FIELDS.find { |o| o[:field] == option_layers }
|
|
126
|
+
return '' unless field_config
|
|
127
|
+
|
|
128
|
+
units = field_config.fetch(:units, [])
|
|
129
|
+
unit = units.find { |u| u[:key] == value_system }
|
|
130
|
+
unit ? unit.fetch(:label, '') : ''
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Format default cell
|
|
134
|
+
def format_default_cell(sub_val, sub_field)
|
|
135
|
+
cell_data = sub_val[sub_field['id']]
|
|
136
|
+
cell_data.is_a?(Hash) ? cell_data['value'].to_s : cell_data.to_s
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
@@ -134,5 +134,11 @@ module Labimotion
|
|
|
134
134
|
optional :select_options, type: Hash, desc: 'selections'
|
|
135
135
|
optional :option_layers, type: String, desc: 'option'
|
|
136
136
|
end
|
|
137
|
+
|
|
138
|
+
params :table_xlsx_params do
|
|
139
|
+
requires :klass, type: String, desc: 'Generic Type', values: %w[Element Segment]
|
|
140
|
+
requires :layer_id, type: String, desc: 'layer identifier'
|
|
141
|
+
requires :field_id, type: String, desc: 'field identifier'
|
|
142
|
+
end
|
|
137
143
|
end
|
|
138
144
|
end
|
|
@@ -29,7 +29,8 @@ module Labimotion
|
|
|
29
29
|
end
|
|
30
30
|
|
|
31
31
|
def self.find_dataset_klass(ols_term_id)
|
|
32
|
-
Labimotion::
|
|
32
|
+
result = Labimotion::TemplateMatcher.find_best_match(ols_term_id)
|
|
33
|
+
result[:template]
|
|
33
34
|
end
|
|
34
35
|
|
|
35
36
|
def self.create_dataset(container, klass)
|
|
@@ -113,11 +113,12 @@ module Labimotion
|
|
|
113
113
|
Labimotion::ElementsElement.find_or_create_by(parent_id: element.id, element_id: el.id)
|
|
114
114
|
els << el.id
|
|
115
115
|
end
|
|
116
|
-
|
|
117
116
|
end
|
|
118
117
|
if element.present?
|
|
119
|
-
|
|
120
|
-
|
|
118
|
+
sds = sds.flatten.uniq
|
|
119
|
+
els = els.flatten.uniq
|
|
120
|
+
es_list = sds.present? ? Labimotion::ElementsSample.where(element_id: element.id).where.not(sample_id: sds) : Labimotion::ElementsSample.where(element_id: element.id)
|
|
121
|
+
ee_list = els.present? ? Labimotion::ElementsElement.where(parent_id: element.id).where.not(element_id: els) : Labimotion::ElementsElement.where(parent_id: element.id)
|
|
121
122
|
es_list.destroy_all if es_list.present?
|
|
122
123
|
ee_list.destroy_all if ee_list.present?
|
|
123
124
|
end
|