etna 0.1.18 → 0.1.23
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/bin/etna +6 -51
- data/etna.completion +225 -88
- data/lib/commands.rb +90 -19
- data/lib/etna.rb +1 -0
- data/lib/etna/clients/magma/formatting/models_csv.rb +280 -271
- data/lib/etna/clients/magma/models.rb +51 -0
- data/lib/etna/clients/magma/workflows/add_project_models_workflow.rb +8 -19
- data/lib/etna/clients/magma/workflows/create_project_workflow.rb +20 -14
- data/lib/etna/clients/magma/workflows/crud_workflow.rb +2 -2
- data/lib/etna/clients/magma/workflows/json_validators.rb +8 -3
- data/lib/etna/clients/magma/workflows/materialize_magma_record_files_workflow.rb +0 -0
- data/lib/etna/clients/magma/workflows/model_synchronization_workflow.rb +1 -1
- data/lib/etna/clients/magma/workflows/update_attributes_from_csv_workflow.rb +75 -7
- data/lib/etna/csvs.rb +160 -0
- data/lib/etna/errors.rb +6 -0
- data/lib/etna/generate_autocompletion_script.rb +2 -1
- data/lib/etna/logger.rb +9 -1
- data/lib/etna/spec/vcr.rb +8 -0
- data/lib/helpers.rb +20 -2
- metadata +6 -3
data/lib/commands.rb
CHANGED
@@ -5,10 +5,6 @@ require 'tempfile'
|
|
5
5
|
require_relative 'helpers'
|
6
6
|
require 'yaml'
|
7
7
|
|
8
|
-
# /bin/etna will confirm execution before running any command that includes this module.
|
9
|
-
module RequireConfirmation
|
10
|
-
end
|
11
|
-
|
12
8
|
class EtnaApp
|
13
9
|
def self.config_file_path
|
14
10
|
File.join(Dir.home, 'etna.yml')
|
@@ -27,9 +23,9 @@ class EtnaApp
|
|
27
23
|
elsif @config && @config.is_a?(Hash) && @config.keys.length == 1
|
28
24
|
@config.keys.last.to_sym
|
29
25
|
elsif @config && @config.is_a?(Hash) && @config.keys.length > 1
|
30
|
-
|
26
|
+
:many
|
31
27
|
else
|
32
|
-
|
28
|
+
:none
|
33
29
|
end
|
34
30
|
end
|
35
31
|
|
@@ -41,11 +37,8 @@ class EtnaApp
|
|
41
37
|
include Etna::CommandExecutor
|
42
38
|
|
43
39
|
class Show < Etna::Command
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
def execute(all: false)
|
48
|
-
if all
|
40
|
+
def execute
|
41
|
+
if EtnaApp.instance.environment == :many
|
49
42
|
File.open(EtnaApp.config_file_path, 'r') { |f| puts f.read }
|
50
43
|
else
|
51
44
|
puts "Current environment: #{EtnaApp.instance.environment}"
|
@@ -61,15 +54,16 @@ class EtnaApp
|
|
61
54
|
boolean_flags << '--ignore-ssl'
|
62
55
|
|
63
56
|
def execute(host, ignore_ssl: false)
|
64
|
-
polyphemus_client
|
57
|
+
polyphemus_client = Etna::Clients::Polyphemus.new(
|
65
58
|
host: host,
|
66
|
-
token: token,
|
59
|
+
token: token(ignore_environment: true),
|
60
|
+
persistent: false,
|
67
61
|
ignore_ssl: ignore_ssl)
|
68
62
|
workflow = Etna::Clients::Polyphemus::SetConfigurationWorkflow.new(
|
69
63
|
polyphemus_client: polyphemus_client,
|
70
64
|
config_file: EtnaApp.config_file_path)
|
71
65
|
config = workflow.update_configuration_file(ignore_ssl: ignore_ssl)
|
72
|
-
logger
|
66
|
+
logger&.info("Updated #{config.environment} configuration from #{host}.")
|
73
67
|
end
|
74
68
|
|
75
69
|
def setup(config)
|
@@ -150,7 +144,7 @@ class EtnaApp
|
|
150
144
|
tf = Tempfile.new
|
151
145
|
|
152
146
|
begin
|
153
|
-
File.open(tf.path, 'wb') { |f| workflow.write_models_template_csv(
|
147
|
+
File.open(tf.path, 'wb') { |f| workflow.write_models_template_csv(project_name, target_model, io: f) }
|
154
148
|
FileUtils.cp(tf.path, file)
|
155
149
|
ensure
|
156
150
|
tf.close!
|
@@ -233,7 +227,7 @@ class EtnaApp
|
|
233
227
|
|
234
228
|
@last_load = File.stat(file).mtime
|
235
229
|
@changeset = File.open(file, 'r') do |f|
|
236
|
-
workflow.prepare_changeset_from_csv(f) do |err|
|
230
|
+
workflow.prepare_changeset_from_csv(io: f) do |err|
|
237
231
|
@errors << err
|
238
232
|
end
|
239
233
|
end
|
@@ -253,7 +247,9 @@ class EtnaApp
|
|
253
247
|
class UpdateFromCsv < Etna::Command
|
254
248
|
include WithEtnaClients
|
255
249
|
include WithLogger
|
256
|
-
|
250
|
+
|
251
|
+
boolean_flags << '--json-values'
|
252
|
+
string_flags << '--hole-value'
|
257
253
|
|
258
254
|
def magma_crud
|
259
255
|
@magma_crud ||= Etna::Clients::Magma::MagmaCrudWorkflow.new(
|
@@ -261,17 +257,92 @@ class EtnaApp
|
|
261
257
|
project_name: @project_name)
|
262
258
|
end
|
263
259
|
|
264
|
-
def execute(project_name, model_name, filepath)
|
260
|
+
def execute(project_name, model_name, filepath, hole_value: '_', json_values: false)
|
265
261
|
@project_name = project_name
|
266
262
|
|
267
263
|
update_attributes_workflow = Etna::Clients::Magma::UpdateAttributesFromCsvWorkflowSingleModel.new(
|
268
264
|
magma_crud: magma_crud,
|
269
265
|
project_name: project_name,
|
270
266
|
model_name: model_name,
|
271
|
-
filepath: filepath
|
267
|
+
filepath: filepath,
|
268
|
+
hole_value: hole_value,
|
269
|
+
json_values: json_values)
|
272
270
|
update_attributes_workflow.update_attributes
|
273
271
|
end
|
274
272
|
end
|
273
|
+
|
274
|
+
class CreateFileLinkingCsv < Etna::Command
|
275
|
+
include WithEtnaClients
|
276
|
+
|
277
|
+
string_flags << '--file'
|
278
|
+
string_flags << '--regex'
|
279
|
+
string_flags << '--folder'
|
280
|
+
boolean_flags << '--collection'
|
281
|
+
|
282
|
+
def execute(project_name, bucket_name, attribute_name, extension, collection: false, regex: "**/*/(?<identifier>.+)\\.#{extension}$", folder: "", file: "#{project_name}_#{attribute_name}.csv")
|
283
|
+
if folder.start_with?("/")
|
284
|
+
folder = folder.slice(1..-1)
|
285
|
+
end
|
286
|
+
|
287
|
+
regex = Regexp.new(regex)
|
288
|
+
|
289
|
+
workflow = Etna::Clients::Magma::SimpleFileLinkingWorkflow.new(
|
290
|
+
metis_client: metis_client,
|
291
|
+
project_name: project_name,
|
292
|
+
bucket_name: bucket_name,
|
293
|
+
folder: folder,
|
294
|
+
extension: extension,
|
295
|
+
attribute_name: attribute_name,
|
296
|
+
regex: regex,
|
297
|
+
file_collection: collection,
|
298
|
+
)
|
299
|
+
|
300
|
+
workflow.write_csv_io(filename: file)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
class LoadTableFromCsv < Etna::Command
|
305
|
+
include WithEtnaClients
|
306
|
+
|
307
|
+
boolean_flags << '--execute'
|
308
|
+
|
309
|
+
def execute(project_name, model_name, file_path, execute: false)
|
310
|
+
request = Etna::Clients::Magma::RetrievalRequest.new(project_name: project_name)
|
311
|
+
request.model_name = model_name
|
312
|
+
request.attribute_names = 'all'
|
313
|
+
request.record_names = 'all'
|
314
|
+
model = magma_client.retrieve(request).models.model(model_name)
|
315
|
+
model_parent_name = model.template.attributes.all.select do |attribute|
|
316
|
+
attribute.attribute_type == Etna::Clients::Magma::AttributeType::PARENT
|
317
|
+
end.first.name
|
318
|
+
|
319
|
+
other_attribute_names = model.template.attributes.all.reject do |attribute|
|
320
|
+
attribute.attribute_type == Etna::Clients::Magma::AttributeType::PARENT
|
321
|
+
end.map do |attribute|
|
322
|
+
attribute.name
|
323
|
+
end
|
324
|
+
|
325
|
+
# NOTE: This does not call ensure_parent currently because of MVIR1 consent--
|
326
|
+
# if the timepoint doesn't exist, the patient may be no study? (one example, at least)
|
327
|
+
update_request = Etna::Clients::Magma::UpdateRequest.new(project_name: project_name)
|
328
|
+
|
329
|
+
data = CSV.parse(File.read(file_path), headers: true)
|
330
|
+
|
331
|
+
data.by_row.each do |row|
|
332
|
+
revision = {}
|
333
|
+
other_attribute_names.each do |attribute_name|
|
334
|
+
revision[attribute_name] = row[attribute_name] unless row[attribute_name].nil?
|
335
|
+
end
|
336
|
+
update_request.append_table(model_parent_name, row[model_parent_name], model_name, revision)
|
337
|
+
end
|
338
|
+
|
339
|
+
puts update_request
|
340
|
+
|
341
|
+
if execute
|
342
|
+
magma_client.update_json(update_request)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
275
346
|
end
|
276
347
|
end
|
277
348
|
end
|
data/lib/etna.rb
CHANGED
@@ -1,342 +1,351 @@
|
|
1
1
|
require 'ostruct'
|
2
|
+
require_relative '../../../csvs'
|
2
3
|
|
3
4
|
module Etna
|
4
5
|
module Clients
|
5
6
|
class Magma
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
:new_attribute_name,
|
13
|
-
:attribute_type,
|
14
|
-
:link_model_name,
|
15
|
-
:description,
|
16
|
-
:display_name,
|
17
|
-
:format_hint,
|
18
|
-
:restricted,
|
19
|
-
:read_only,
|
20
|
-
:options,
|
21
|
-
:match,
|
22
|
-
:attribute_group,
|
23
|
-
:hidden,
|
24
|
-
:unique,
|
25
|
-
:matrix_constant,
|
26
|
-
]
|
27
|
-
|
28
|
-
COLUMN_AS_BOOLEAN = -> (s) { ['true', 't', 'y', 'yes'].include?(s.downcase) }
|
29
|
-
|
30
|
-
COLUMNS_TO_ATTRIBUTES = {
|
31
|
-
attribute_name: :attribute_name,
|
32
|
-
attribute_type: [:attribute_type, -> (s) { AttributeType.new(s) }],
|
33
|
-
link_model_name: :link_model_name,
|
34
|
-
description: :desc,
|
35
|
-
display_name: :display_name,
|
36
|
-
format_hint: :format_hint,
|
37
|
-
restricted: [:restricted, COLUMN_AS_BOOLEAN],
|
38
|
-
read_only: [:read_only, COLUMN_AS_BOOLEAN],
|
39
|
-
options: [:validation, -> (s) { {"type" => "Array", "value" => s.split(',').map(&:strip)} }],
|
40
|
-
match: [:validation, -> (s) { {"type" => "Regexp", "value" => Regexp.new(s).source} }],
|
41
|
-
attribute_group: :attribute_group,
|
42
|
-
hidden: [:hidden, COLUMN_AS_BOOLEAN],
|
43
|
-
unique: [:unique, COLUMN_AS_BOOLEAN],
|
44
|
-
}
|
45
|
-
|
46
|
-
def self.apply_csv_row(changeset = ModelsChangeset.new, row = {}, &err_block)
|
47
|
-
changeset.tap do
|
48
|
-
models = changeset.models
|
7
|
+
module ModelsCsv
|
8
|
+
module Prettify
|
9
|
+
def prettify(name)
|
10
|
+
name.split('_').map(&:capitalize).join(' ')
|
11
|
+
end
|
12
|
+
end
|
49
13
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
14
|
+
class Exporter < Etna::CsvExporter
|
15
|
+
include Prettify
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
super([
|
19
|
+
:comments,
|
20
|
+
:model_name, :identifier, :parent_model_name, :parent_link_type,
|
21
|
+
:attribute_name,
|
22
|
+
:new_attribute_name,
|
23
|
+
:attribute_type,
|
24
|
+
:link_model_name,
|
25
|
+
:description,
|
26
|
+
:display_name,
|
27
|
+
:format_hint,
|
28
|
+
:restricted,
|
29
|
+
:read_only,
|
30
|
+
:options,
|
31
|
+
:match,
|
32
|
+
:attribute_group,
|
33
|
+
:hidden,
|
34
|
+
:unique,
|
35
|
+
:matrix_constant,
|
36
|
+
])
|
37
|
+
end
|
58
38
|
|
59
|
-
|
60
|
-
|
61
|
-
models
|
62
|
-
else
|
63
|
-
last_model = changeset.last_model_key
|
64
|
-
if last_model.nil?
|
65
|
-
nil
|
66
|
-
else
|
67
|
-
models.model(last_model).build_template
|
68
|
-
end
|
39
|
+
def write_models(models, model_keys = models.model_keys.sort, output_io: nil, filename: nil)
|
40
|
+
with_row_writeable(filename: filename, output_io: output_io) do |row_writeable|
|
41
|
+
write_model_rows(models, model_keys, row_writeable)
|
69
42
|
end
|
43
|
+
end
|
70
44
|
|
71
|
-
|
72
|
-
|
73
|
-
yield "Found identifier #{identifier} but no model_name had been given!"
|
74
|
-
next
|
75
|
-
end
|
76
|
-
template.identifier = identifier
|
77
|
-
end
|
45
|
+
def write_model_rows(models, model_keys, row_writeable)
|
46
|
+
ensure_parents(models, model_keys, row_writeable)
|
78
47
|
|
79
|
-
|
80
|
-
|
81
|
-
end
|
48
|
+
matrix_constants = {}
|
49
|
+
model_keys.each { |model_name| each_model_row(models, model_name, matrix_constants, row_writeable) }
|
82
50
|
|
83
|
-
|
84
|
-
|
51
|
+
matrix_constants.each do |digest, options|
|
52
|
+
row_writeable.write
|
53
|
+
row_writeable.write(matrix_constant: COPY_OPTIONS_SENTINEL + digest)
|
54
|
+
options.each do |option|
|
55
|
+
row_writeable.write(matrix_constant: option)
|
56
|
+
end
|
85
57
|
end
|
58
|
+
end
|
86
59
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
60
|
+
def ensure_parents(models, model_keys, row_writeable)
|
61
|
+
q = model_keys.dup
|
62
|
+
seen = Set.new
|
63
|
+
|
64
|
+
until q.empty?
|
65
|
+
model_key = q.shift
|
66
|
+
next if model_key.nil?
|
67
|
+
next if seen.include?(model_key)
|
68
|
+
seen.add(model_key)
|
69
|
+
|
70
|
+
# For models that are only part of the trunk, but not of the tree of model_keys,
|
71
|
+
# we still need their basic information (identifier / parent) for validation and for
|
72
|
+
# potentially creating the required tree dependencies to connect it to a remote tree.
|
73
|
+
unless model_keys.include?(model_key)
|
74
|
+
each_model_trunk_row(models, model_key, row_writeable)
|
91
75
|
end
|
76
|
+
|
77
|
+
q.push(*models.model(model_key).template.all_linked_model_names)
|
92
78
|
end
|
93
79
|
end
|
94
|
-
end
|
95
80
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
81
|
+
def each_model_trunk_row(models, model_name, row_writeable)
|
82
|
+
return unless (model = models.model(model_name))
|
83
|
+
|
84
|
+
# Empty link for better visual separation
|
85
|
+
row_writeable.write
|
86
|
+
row_writeable.write(model_name: model_name)
|
87
|
+
|
88
|
+
if !model.template.parent.nil?
|
89
|
+
parent_model = models.model(model.template.parent)
|
90
|
+
reciprocal = models.find_reciprocal(model: model, link_model: parent_model)
|
91
|
+
|
92
|
+
row_writeable.write(
|
93
|
+
identifier: model.template.identifier,
|
94
|
+
parent_model_name: model.template.parent,
|
95
|
+
parent_link_type: reciprocal.attribute_type.to_s
|
96
|
+
)
|
97
|
+
else
|
98
|
+
row_writeable.write(
|
99
|
+
identifier: model.template.identifier,
|
100
|
+
)
|
101
101
|
end
|
102
102
|
end
|
103
103
|
|
104
|
-
|
105
|
-
|
104
|
+
def each_model_row(models, model_name, matrix_constants, row_writeable)
|
105
|
+
return unless (model = models.model(model_name))
|
106
106
|
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
107
|
+
each_model_trunk_row(models, model_name, row_writeable)
|
108
|
+
model.template.attributes.all.each do |attribute|
|
109
|
+
each_attribute_row(models, model, attribute, matrix_constants, row_writeable)
|
110
|
+
end
|
111
111
|
end
|
112
112
|
|
113
|
-
|
114
|
-
|
115
|
-
|
113
|
+
def each_attribute_row(models, model, attribute, matrix_constants, row_writeable)
|
114
|
+
if attribute.attribute_type == Etna::Clients::Magma::AttributeType::IDENTIFIER
|
115
|
+
# Identifiers for models whose parent link type ends up being a table are non configurable, so we don't
|
116
|
+
# want to include them in the CSV.
|
117
|
+
if models.find_reciprocal(model: model, link_attribute_name: model.template.parent)&.attribute_type == Etna::Clients::Magma::AttributeType::TABLE
|
118
|
+
return
|
119
|
+
end
|
120
|
+
else
|
121
|
+
return unless Etna::Clients::Magma::AttributeValidator.valid_add_row_attribute_types.include?(attribute.attribute_type)
|
122
|
+
end
|
116
123
|
|
117
|
-
|
124
|
+
options = attribute.options&.join(', ')
|
125
|
+
if attribute.attribute_type == Etna::Clients::Magma::AttributeType::MATRIX
|
126
|
+
# Matrix attribute validations are massive, and functional. For now, we don't support showing and editing
|
127
|
+
# them inline with this spreadsheet. In the future, I think we should possibly introduce the concept of
|
128
|
+
# CONSTANTS or Matrix Types that are managed separately.
|
129
|
+
options = options || ''
|
130
|
+
digest = Digest::MD5.hexdigest(options)
|
131
|
+
matrix_constants[digest] ||= options.split(',').map(&:strip)
|
118
132
|
|
119
|
-
|
120
|
-
|
121
|
-
parent_att.attribute_type = AttributeType::PARENT
|
122
|
-
parent_att.link_model_name = parent_model_name
|
123
|
-
parent_att.desc = self.prettify(parent_model_name)
|
124
|
-
parent_att.display_name = self.prettify(parent_model_name)
|
125
|
-
end
|
126
|
-
end
|
133
|
+
options = COPY_OPTIONS_SENTINEL + digest
|
134
|
+
end
|
127
135
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
136
|
+
row_writeable.write(
|
137
|
+
attribute_name: attribute.name,
|
138
|
+
attribute_type: attribute.attribute_type,
|
139
|
+
link_model_name: attribute.link_model_name,
|
140
|
+
reciprocal_link_type: models.find_reciprocal(model: model, attribute: attribute)&.attribute_type,
|
141
|
+
description: attribute.desc,
|
142
|
+
display_name: attribute.display_name,
|
143
|
+
match: attribute.match,
|
144
|
+
format_hint: attribute.format_hint,
|
145
|
+
restricted: attribute.restricted,
|
146
|
+
read_only: attribute.read_only,
|
147
|
+
options: options,
|
148
|
+
attribute_group: attribute.attribute_group,
|
149
|
+
hidden: attribute.hidden,
|
150
|
+
unique: attribute.unique,
|
151
|
+
)
|
132
152
|
end
|
153
|
+
end
|
133
154
|
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
end
|
155
|
+
class ModelsChangeset < Struct.new(:models, :renames, :matrix_constants, keyword_init: true)
|
156
|
+
def initialize(*args)
|
157
|
+
super
|
138
158
|
|
139
|
-
|
140
|
-
|
141
|
-
|
159
|
+
self.models ||= Models.new
|
160
|
+
self.renames ||= {}
|
161
|
+
self.matrix_constants ||= {}
|
142
162
|
end
|
143
163
|
|
144
|
-
|
145
|
-
|
164
|
+
def build_renames(model_name)
|
165
|
+
renames[model_name] ||= {}
|
146
166
|
end
|
167
|
+
end
|
147
168
|
|
148
|
-
|
149
|
-
parent_model_attribute_by_model_name = parent_model.build_template.build_attributes.attribute(template.name)
|
150
|
-
if parent_model_attribute_by_model_name && !reciprocal
|
151
|
-
yield "Model #{template.parent} is linked as a parent to #{template.name}, but it already has an attribute named #{template.name} #{parent_model_attribute_by_model_name.raw}." if block_given?
|
152
|
-
end
|
169
|
+
COPY_OPTIONS_SENTINEL = '$copy_from_original$'
|
153
170
|
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
171
|
+
class Importer < Etna::CsvImporter
|
172
|
+
include Prettify
|
173
|
+
|
174
|
+
# Columns of the row, _after format_row is called_, that should be applied to an attribute.
|
175
|
+
# This should line up with the attribute names _on the model itself_.
|
176
|
+
ATTRIBUTE_ROW_ENTRIES = [
|
177
|
+
:attribute_type,
|
178
|
+
:link_model_name, :desc,
|
179
|
+
:display_name, :format_hint,
|
180
|
+
:restricted, :read_only,
|
181
|
+
:validation, :attribute_group,
|
182
|
+
:hidden, :unique,
|
183
|
+
]
|
184
|
+
|
185
|
+
def initialize
|
186
|
+
super(&method(:format_row))
|
161
187
|
end
|
162
|
-
end
|
163
188
|
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
189
|
+
def format_row(row)
|
190
|
+
replace_row_column(row, :attribute_type) { |s| AttributeType.new(s) }
|
191
|
+
replace_row_column(row, :desc) { row.delete(:description) }
|
192
|
+
replace_row_column(row, :restricted, &COLUMN_AS_BOOLEAN)
|
193
|
+
replace_row_column(row, :read_only, &COLUMN_AS_BOOLEAN)
|
194
|
+
replace_row_column(row, :options) { |s| {"type" => "Array", "value" => s.split(',').map(&:strip)} }
|
195
|
+
replace_row_column(row, :match) { |s| {"type" => "Regexp", "value" => Regexp.new(s).source} }
|
196
|
+
replace_row_column(row, :validation) { row[:options] || row[:match] }
|
197
|
+
replace_row_column(row, :hidden, &COLUMN_AS_BOOLEAN)
|
198
|
+
replace_row_column(row, :unique, &COLUMN_AS_BOOLEAN)
|
168
199
|
end
|
169
200
|
|
170
|
-
|
171
|
-
|
172
|
-
if existing_attribute
|
173
|
-
if existing_attribute.attribute_type != AttributeType::COLLECTION
|
174
|
-
yield "Attribute #{attribute_name} of model #{template.name} has duplicate definitions!" if block_given?
|
175
|
-
end
|
201
|
+
def process_row(row_processor, changeset)
|
202
|
+
models = changeset.models
|
176
203
|
|
177
|
-
|
204
|
+
process_matrix_constants(changeset, row_processor)
|
205
|
+
process_model_config(models, row_processor)
|
206
|
+
process_parent_config(models, row_processor)
|
207
|
+
process_attribute_properties(changeset, row_processor)
|
178
208
|
end
|
179
209
|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
210
|
+
def prepare_changeset(filename: nil, input_io: nil, &validation_err_block)
|
211
|
+
ModelsChangeset.new.tap do |changeset|
|
212
|
+
context = {}
|
213
|
+
each_csv_row(filename: filename, input_io: input_io) do |row, lineno|
|
214
|
+
p = NestedRowProcessor.new(row, lineno, context)
|
215
|
+
process_row(p, changeset)
|
186
216
|
end
|
187
217
|
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
end
|
194
|
-
|
195
|
-
if att.attribute_type == AttributeType::LINK && models.find_reciprocal(model_name: template.name, attribute: att).nil?
|
196
|
-
models.build_model(att.link_model_name).build_template.build_attributes.build_attribute(template.name).tap do |rec_att|
|
197
|
-
rec_att.attribute_name = template.name
|
198
|
-
rec_att.display_name = self.prettify(template.name)
|
199
|
-
rec_att.desc = self.prettify(template.name)
|
200
|
-
rec_att.attribute_type = AttributeType::COLLECTION
|
201
|
-
rec_att.link_model_name = template.name
|
218
|
+
# After configuring attributes, set defaults for certain attribute properties.
|
219
|
+
changeset.models.all.each do |model|
|
220
|
+
model.template.attributes.all.each do |attribute|
|
221
|
+
attribute.set_field_defaults!
|
222
|
+
end
|
202
223
|
end
|
203
224
|
end
|
204
|
-
|
205
|
-
|
225
|
+
rescue ImportError => e
|
226
|
+
validation_err_block.call(e.message)
|
206
227
|
end
|
207
|
-
end
|
208
228
|
|
209
|
-
def self.prettify(name)
|
210
|
-
name.split('_').map(&:capitalize).join(' ')
|
211
|
-
end
|
212
229
|
|
213
|
-
|
214
|
-
c = row[col]&.chomp
|
215
|
-
return nil if c&.empty?
|
216
|
-
c
|
217
|
-
end
|
218
|
-
|
219
|
-
def self.each_csv_row(models = Models.new, model_keys = models.model_keys.sort, &block)
|
220
|
-
yield COLUMNS.map(&:to_s)
|
221
|
-
|
222
|
-
self.ensure_parents(models, model_keys, &block)
|
223
|
-
matrix_constants = {}
|
224
|
-
model_keys.each { |model_name| self.each_model_row(models, model_name, matrix_constants, &block) }
|
230
|
+
private
|
225
231
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
options.each do |option|
|
230
|
-
yield row_from_columns(matrix_constant: option)
|
232
|
+
def process_model_config(models, row_processor)
|
233
|
+
row_processor.process(:model_name) do |model_name|
|
234
|
+
models.build_model(model_name).build_template.tap { |t| t.name = model_name }
|
231
235
|
end
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
def self.ensure_parents(models, model_keys, &block)
|
236
|
-
q = model_keys.dup
|
237
|
-
seen = Set.new
|
238
|
-
|
239
|
-
until q.empty?
|
240
|
-
model_key = q.shift
|
241
|
-
next if model_key.nil?
|
242
|
-
next if seen.include?(model_key)
|
243
|
-
seen.add(model_key)
|
244
|
-
|
245
|
-
# For models that are only part of the trunk, but not of the tree of model_keys,
|
246
|
-
# we still need their basic information (identifier / parent) for validation and for
|
247
|
-
# potentially creating the required tree dependencies to connect it to a remote tree.
|
248
|
-
unless model_keys.include?(model_key)
|
249
|
-
self.each_model_trunk_row(models, model_key, &block)
|
236
|
+
row_processor.process(:identifier, :model_name) do |identifier, template|
|
237
|
+
template.identifier = identifier
|
250
238
|
end
|
251
|
-
|
252
|
-
q.push(*models.model(model_key).template.all_linked_model_names)
|
253
239
|
end
|
254
|
-
end
|
255
240
|
|
256
|
-
|
257
|
-
|
241
|
+
def process_parent_config(models, row_processor)
|
242
|
+
row_processor.process(:parent_model_name, :model_name) do |parent_model_name, template|
|
243
|
+
if template.parent && !template.parent.empty? && template.parent != parent_model_name
|
244
|
+
raise ImportError.new("Model #{template.name} was provided multiple parent_model_names: #{template.parent} and #{parent_model_name}")
|
245
|
+
end
|
258
246
|
|
259
|
-
|
260
|
-
yield row_from_columns
|
261
|
-
yield row_from_columns(model_name: model_name)
|
247
|
+
template.parent = parent_model_name
|
262
248
|
|
263
|
-
|
264
|
-
|
265
|
-
|
249
|
+
template.build_attributes.build_attribute(template.parent).tap do |parent_att|
|
250
|
+
parent_att.name = parent_att.attribute_name = parent_model_name
|
251
|
+
parent_att.attribute_type = Etna::Clients::Magma::AttributeType::PARENT
|
252
|
+
parent_att.link_model_name = parent_model_name
|
253
|
+
parent_att.desc = prettify(parent_model_name)
|
254
|
+
parent_att.display_name = prettify(parent_model_name)
|
255
|
+
end
|
256
|
+
end
|
266
257
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
else
|
273
|
-
yield row_from_columns(
|
274
|
-
identifier: model.template.identifier,
|
275
|
-
)
|
276
|
-
end
|
277
|
-
end
|
258
|
+
row_processor.process(:parent_link_type, :model_name, :parent_model_name) do |parent_link_type, template|
|
259
|
+
reciprocal = models.find_reciprocal(model_name: template.name, link_attribute_name: template.parent)
|
260
|
+
if reciprocal && reciprocal.attribute_type.to_s != parent_link_type
|
261
|
+
raise ImportError.new("Model #{template.name} was provided multiple parent_link_types: #{reciprocal.attribute_type} and #{parent_link_type}")
|
262
|
+
end
|
278
263
|
|
279
|
-
|
280
|
-
|
264
|
+
if reciprocal && reciprocal.attribute_name != template.name
|
265
|
+
raise ImportError.new("Model #{template.name} is linked to #{template.parent}, but the reciprocal link is misnamed as '#{reciprocal.attribute_name}'.")
|
266
|
+
end
|
281
267
|
|
282
|
-
|
283
|
-
|
284
|
-
|
268
|
+
models.build_model(template.parent).tap do |parent_model|
|
269
|
+
parent_model_attribute_by_model_name = parent_model.build_template.build_attributes.attribute(template.name)
|
270
|
+
if parent_model_attribute_by_model_name && !reciprocal
|
271
|
+
raise ImportError.new("Model #{template.parent} is linked as a parent to #{template.name}, but it already has an attribute named #{template.name} #{parent_model_attribute_by_model_name.raw}.")
|
272
|
+
end
|
273
|
+
|
274
|
+
parent_model.build_template.build_attributes.build_attribute(template.name).tap do |attr|
|
275
|
+
attr.attribute_name = attr.name = template.name
|
276
|
+
attr.attribute_type = parent_link_type
|
277
|
+
attr.link_model_name = template.name
|
278
|
+
attr.desc = prettify(template.name)
|
279
|
+
attr.display_name = prettify(template.name)
|
280
|
+
end
|
281
|
+
end
|
282
|
+
end
|
285
283
|
end
|
286
|
-
end
|
287
284
|
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
285
|
+
def process_matrix_constants(changeset, row_processor)
|
286
|
+
row_processor.process(:matrix_constant) do |matrix_constant|
|
287
|
+
# Lines that start with the sentinel are a distinct 'group'
|
288
|
+
if matrix_constant.start_with?(COPY_OPTIONS_SENTINEL)
|
289
|
+
matrix_constant = matrix_constant.slice((COPY_OPTIONS_SENTINEL.length)..-1)
|
290
|
+
changeset.matrix_constants[matrix_constant] = []
|
291
|
+
else
|
292
|
+
# Until the sentinel is seen again, all further entries belong to the set.
|
293
|
+
changeset.matrix_constants[changeset.matrix_constants.keys.last] << matrix_constant
|
294
|
+
end
|
295
|
+
|
296
|
+
matrix_constant
|
294
297
|
end
|
295
|
-
else
|
296
|
-
return unless AttributeValidator.valid_add_row_attribute_types.include?(attribute.attribute_type)
|
297
298
|
end
|
298
299
|
|
299
|
-
|
300
|
-
|
301
|
-
# Matrix attribute validations are massive, and functional. For now, we don't support showing and editing
|
302
|
-
# them inline with this spreadsheet. In the future, I think we should possibly introduce the concept of
|
303
|
-
# CONSTANTS or Matrix Types that are managed separately.
|
304
|
-
options = options || ''
|
305
|
-
digest = Digest::MD5.hexdigest(options)
|
306
|
-
matrix_constants[digest] ||= COLUMNS_TO_ATTRIBUTES[:options][1].call(options)["value"]
|
300
|
+
def process_attribute_properties(changeset, row_processor)
|
301
|
+
models = changeset.models
|
307
302
|
|
308
|
-
|
309
|
-
|
303
|
+
row_processor.process(:attribute_name, :model_name) do |attribute_name, template|
|
304
|
+
attributes = template.build_attributes
|
310
305
|
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
description: attribute.desc,
|
317
|
-
display_name: attribute.display_name,
|
318
|
-
match: attribute.match,
|
319
|
-
format_hint: attribute.format_hint,
|
320
|
-
restricted: attribute.restricted,
|
321
|
-
read_only: attribute.read_only,
|
322
|
-
options: options,
|
323
|
-
attribute_group: attribute.attribute_group,
|
324
|
-
hidden: attribute.hidden,
|
325
|
-
unique: attribute.unique,
|
326
|
-
)
|
327
|
-
end
|
306
|
+
existing_attribute = attributes.attribute(attribute_name)
|
307
|
+
if existing_attribute
|
308
|
+
if existing_attribute.attribute_type != Etna::Clients::Magma::AttributeType::COLLECTION
|
309
|
+
raise ImportError.new("Attribute #{attribute_name} of model #{template.name} has duplicate definitions!")
|
310
|
+
end
|
328
311
|
|
329
|
-
|
330
|
-
|
331
|
-
|
312
|
+
# Clear its definition; implicit attributes are overwritten by explicit definitions if they exist.
|
313
|
+
attributes.raw.delete(attribute_name)
|
314
|
+
end
|
332
315
|
|
333
|
-
|
334
|
-
|
335
|
-
super
|
316
|
+
attributes.build_attribute(attribute_name).tap { |att| att.name = att.attribute_name = attribute_name }
|
317
|
+
end
|
336
318
|
|
337
|
-
|
338
|
-
|
339
|
-
|
319
|
+
row_processor.process(:new_attribute_name, :attribute_name, :model_name) do |new_attribute_name, att, template|
|
320
|
+
renames = changeset.build_renames(template.name)
|
321
|
+
if renames.include?(att.name) && renames[att.name] != new_attribute_name
|
322
|
+
raise ImportError.new("Found multiple new_attribute_name values for #{template.name}'s #{att.name}': #{new_attribute_name} or #{renames[att.name]}?")
|
323
|
+
end
|
324
|
+
|
325
|
+
renames[att.attribute_name] = new_attribute_name
|
326
|
+
end
|
327
|
+
|
328
|
+
ATTRIBUTE_ROW_ENTRIES.each do |prop_name|
|
329
|
+
row_processor.process(prop_name, :model_name, :attribute_name) do |value, template, att|
|
330
|
+
if att.raw.include?(prop_name.to_s)
|
331
|
+
raise ImportError.new("Value for #{prop_name} on attribute #{att.attribute_name} has duplicate definitions!")
|
332
|
+
end
|
333
|
+
|
334
|
+
att.send(:"#{prop_name}=", value)
|
335
|
+
|
336
|
+
if att.attribute_type && att.link_model_name
|
337
|
+
if att.attribute_type == Etna::Clients::Magma::AttributeType::LINK && models.find_reciprocal(model_name: template.name, attribute: att).nil?
|
338
|
+
models.build_model(att.link_model_name).build_template.build_attributes.build_attribute(template.name).tap do |rec_att|
|
339
|
+
rec_att.attribute_name = rec_att.name = template.name
|
340
|
+
rec_att.display_name = prettify(template.name)
|
341
|
+
rec_att.desc = prettify(template.name)
|
342
|
+
rec_att.attribute_type = Etna::Clients::Magma::AttributeType::COLLECTION
|
343
|
+
rec_att.link_model_name = template.name
|
344
|
+
end
|
345
|
+
end
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|
340
349
|
end
|
341
350
|
end
|
342
351
|
end
|