duty_free 1.0.0 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/duty_free/column.rb +4 -4
- data/lib/duty_free/extensions.rb +254 -153
- data/lib/duty_free/suggest_template.rb +4 -4
- data/lib/duty_free/util.rb +2 -2
- data/lib/duty_free/version_number.rb +1 -1
- data/lib/generators/duty_free/USAGE +1 -1
- data/lib/generators/duty_free/install_generator.rb +8 -10
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 87d07568c8c7a1692981435e098bd8426d17f30cd733ffff01a7b5a4e4fee4a9
|
4
|
+
data.tar.gz: 1d2ef595a271f40e5489cac67e576b4f85338d43d46bff26c6e5a6d706766a00
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4bdd6c53f65cb096de1c2da724a735ecee7ceaed464627f69adbb816b3afa20c85a202cce201a7ee5d35e52ae31663c7f8a9c02d47bc11ea7ead86cebfb0972f
|
7
|
+
data.tar.gz: 4664e2b6e724f3bba469ef1907b82fe25d90761251ff5cabb204bd75e1a64b522ba3a438438a76e5d58e80952870334abb8194d9d0199b6e2addb2143120b5fa
|
data/lib/duty_free/column.rb
CHANGED
@@ -5,15 +5,15 @@ require 'duty_free/util'
|
|
5
5
|
module DutyFree
|
6
6
|
# Holds detail about each column as we recursively explore the scope of what to import
|
7
7
|
class Column
|
8
|
-
attr_accessor :name, :pre_prefix, :prefix, :prefix_assocs, :
|
8
|
+
attr_accessor :name, :pre_prefix, :prefix, :prefix_assocs, :import_template_as
|
9
9
|
attr_writer :obj_class
|
10
10
|
|
11
|
-
def initialize(name, pre_prefix, prefix, prefix_assocs, obj_class,
|
11
|
+
def initialize(name, pre_prefix, prefix, prefix_assocs, obj_class, import_template_as)
|
12
12
|
self.name = name
|
13
13
|
self.pre_prefix = pre_prefix
|
14
14
|
self.prefix = prefix
|
15
15
|
self.prefix_assocs = prefix_assocs
|
16
|
-
self.
|
16
|
+
self.import_template_as = import_template_as
|
17
17
|
self.obj_class = obj_class
|
18
18
|
end
|
19
19
|
|
@@ -48,7 +48,7 @@ module DutyFree
|
|
48
48
|
# The snake-cased column name to be used for building the full list of template_columns
|
49
49
|
def sym_string
|
50
50
|
@sym_string ||= ::DutyFree::Util._prefix_join(
|
51
|
-
[pre_prefix, prefix, ::DutyFree::Util._clean_name(name,
|
51
|
+
[pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
|
52
52
|
'_'
|
53
53
|
).tr('.', '_')
|
54
54
|
end
|
data/lib/duty_free/extensions.rb
CHANGED
@@ -2,10 +2,7 @@
|
|
2
2
|
|
3
3
|
require 'duty_free/column'
|
4
4
|
require 'duty_free/suggest_template'
|
5
|
-
# require 'duty_free/attribute_serializers/object_attribute'
|
6
|
-
# require 'duty_free/attribute_serializers/object_changes_attribute'
|
7
5
|
# require 'duty_free/model_config'
|
8
|
-
# require 'duty_free/record_trail'
|
9
6
|
|
10
7
|
# :nodoc:
|
11
8
|
module DutyFree
|
@@ -21,22 +18,40 @@ module DutyFree
|
|
21
18
|
# end
|
22
19
|
|
23
20
|
# Export at least column header, and optionally include all existing data as well
|
24
|
-
def df_export(is_with_data = true,
|
21
|
+
def df_export(is_with_data = true, import_template = nil)
|
25
22
|
# In case they are only supplying the columns hash
|
26
|
-
if is_with_data.is_a?(Hash) && !
|
27
|
-
|
23
|
+
if is_with_data.is_a?(Hash) && !import_template
|
24
|
+
import_template = is_with_data
|
28
25
|
is_with_data = true
|
29
26
|
end
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
27
|
+
import_template ||= if constants.include?(:IMPORT_TEMPLATE)
|
28
|
+
self::IMPORT_TEMPLATE
|
29
|
+
else
|
30
|
+
suggest_template(0, false, false)
|
31
|
+
end
|
32
|
+
|
33
|
+
# Friendly column names that end up in the first row of the CSV
|
34
|
+
# Required columns get prefixed with a *
|
35
|
+
requireds = (import_template[:required] || [])
|
36
|
+
rows = ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
|
37
|
+
is_required = requireds.include?(col)
|
38
|
+
col = col.to_s.titleize
|
39
|
+
# Alias-ify the full column names
|
40
|
+
aliases = (import_template[:as] || [])
|
41
|
+
aliases.each do |k, v|
|
42
|
+
if col.start_with?(v)
|
43
|
+
col = k + col[v.length..-1]
|
44
|
+
break
|
45
|
+
end
|
46
|
+
end
|
47
|
+
(is_required ? '* ' : '') + col
|
48
|
+
end
|
49
|
+
rows = Array(rows)
|
50
|
+
|
36
51
|
if is_with_data
|
37
52
|
# Automatically create a JOINs strategy and select list to get back all related rows
|
38
|
-
template_cols, template_joins =
|
39
|
-
relation =
|
53
|
+
template_cols, template_joins = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template)
|
54
|
+
relation = left_joins(template_joins)
|
40
55
|
|
41
56
|
# So we can properly create the SELECT list, create a mapping between our
|
42
57
|
# column alias prefixes and the aliases AREL creates.
|
@@ -46,7 +61,7 @@ module DutyFree
|
|
46
61
|
mapping = our_names.zip(arel_alias_names).to_h
|
47
62
|
|
48
63
|
relation.select(template_cols.map { |x| x.to_s(mapping) }).each do |result|
|
49
|
-
rows <<
|
64
|
+
rows << ::DutyFree::Extensions._template_columns(self, import_template).map do |col|
|
50
65
|
value = result.send(col)
|
51
66
|
case value
|
52
67
|
when true
|
@@ -63,12 +78,16 @@ module DutyFree
|
|
63
78
|
end
|
64
79
|
|
65
80
|
# With an array of incoming data, the first row having column names, perform the import
|
66
|
-
def df_import(data,
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
81
|
+
def df_import(data, import_template = nil)
|
82
|
+
self.instance_variable_set(:@defined_uniques, nil)
|
83
|
+
self.instance_variable_set(:@valid_uniques, nil)
|
84
|
+
|
85
|
+
import_template ||= if constants.include?(:IMPORT_TEMPLATE)
|
86
|
+
self::IMPORT_TEMPLATE
|
87
|
+
else
|
88
|
+
suggest_template(0, false, false)
|
89
|
+
end
|
90
|
+
puts "Chose #{import_template}"
|
72
91
|
inserts = []
|
73
92
|
updates = []
|
74
93
|
counts = Hash.new { |h, k| h[k] = [] }
|
@@ -79,17 +98,18 @@ module DutyFree
|
|
79
98
|
cols = nil
|
80
99
|
starred = []
|
81
100
|
partials = []
|
82
|
-
all =
|
101
|
+
all = import_template[:all]
|
83
102
|
keepers = {}
|
84
103
|
valid_unique = nil
|
85
104
|
existing = {}
|
86
105
|
devise_class = ''
|
106
|
+
ret = nil
|
87
107
|
|
88
108
|
reference_models = if Object.const_defined?('Apartment')
|
89
109
|
Apartment.excluded_models
|
90
110
|
else
|
91
111
|
[]
|
92
|
-
|
112
|
+
end
|
93
113
|
|
94
114
|
if Object.const_defined?('Devise')
|
95
115
|
Object.const_get('Devise') # If this fails, devise_class will remain a blank string.
|
@@ -123,13 +143,14 @@ module DutyFree
|
|
123
143
|
# Will show as just one transaction when using auditing solutions such as PaperTrail
|
124
144
|
ActiveRecord::Base.transaction do
|
125
145
|
# Check to see if they want to do anything before the whole import
|
126
|
-
if before_import ||= (
|
146
|
+
if before_import ||= (import_template[:before_import]) # || some generic before_import)
|
127
147
|
before_import.call(data)
|
128
148
|
end
|
149
|
+
col_list = nil
|
129
150
|
data.each_with_index do |row, row_num|
|
130
151
|
row_errors = {}
|
131
152
|
if is_first # Anticipate that first row has column names
|
132
|
-
uniques =
|
153
|
+
uniques = import_template[:uniques]
|
133
154
|
|
134
155
|
# Look for UTF-8 BOM in very first cell
|
135
156
|
row[0] = row[0][3..-1] if row[0].start_with?([239, 187, 191].pack('U*'))
|
@@ -154,10 +175,13 @@ module DutyFree
|
|
154
175
|
col.strip!
|
155
176
|
end
|
156
177
|
end
|
157
|
-
|
158
|
-
|
178
|
+
# %%% Will the uniques saved into @defined_uniques here just get redefined later
|
179
|
+
# after the next line, the map! with clean to change out the alias names? So we can't yet set
|
180
|
+
# col_list?
|
181
|
+
defined_uniques(uniques, cols, cols.join('|'), starred)
|
182
|
+
cols.map! { |col| ::DutyFree::Util._clean_name(col, import_template[:as]) } # %%%
|
159
183
|
# Make sure that at least half of them match what we know as being good column names
|
160
|
-
template_column_objects =
|
184
|
+
template_column_objects = ::DutyFree::Extensions._recurse_def(self, import_template[:all], import_template).first
|
161
185
|
cols.each_with_index do |col, idx|
|
162
186
|
# prefixes = col_detail.pre_prefix + (col_detail.prefix.blank? ? [] : [col_detail.prefix])
|
163
187
|
keepers[idx] = template_column_objects.find { |col_obj| col_obj.titleize == col }
|
@@ -168,18 +192,25 @@ module DutyFree
|
|
168
192
|
end
|
169
193
|
|
170
194
|
# Returns just the first valid unique lookup set if there are multiple
|
171
|
-
valid_unique =
|
195
|
+
valid_unique = find_existing(uniques, cols, starred, import_template, keepers, false)
|
172
196
|
# Make a lookup from unique values to specific IDs
|
173
|
-
existing = pluck(*([:id] + valid_unique.keys)).each_with_object(existing)
|
197
|
+
existing = pluck(*([:id] + valid_unique.keys)).each_with_object(existing) do |v, s|
|
198
|
+
s[v[1..-1].map(&:to_s)] = v.first
|
199
|
+
s
|
200
|
+
end
|
174
201
|
is_first = false
|
175
202
|
else # Normal row of data
|
176
203
|
is_insert = false
|
177
204
|
is_do_save = true
|
178
205
|
existing_unique = valid_unique.inject([]) do |s, v|
|
179
|
-
s <<
|
206
|
+
s << if v.last.is_a?(Array)
|
207
|
+
v.last[0].where(v.last[1] => row[v.last[2]]).limit(1).pluck('MAX(id)').first.to_s
|
208
|
+
else
|
209
|
+
row[v.last].to_s
|
210
|
+
end
|
180
211
|
end
|
181
212
|
# Check to see if they want to preprocess anything
|
182
|
-
if @before_process ||=
|
213
|
+
if @before_process ||= import_template[:before_process]
|
183
214
|
existing_unique = @before_process.call(valid_unique, existing_unique)
|
184
215
|
end
|
185
216
|
obj = if existing.include?(existing_unique)
|
@@ -200,17 +231,22 @@ module DutyFree
|
|
200
231
|
|
201
232
|
# Not the same as the last path?
|
202
233
|
if this_path && v.path != this_path.split(',').map(&:to_sym) && !is_has_one
|
203
|
-
|
204
|
-
|
205
|
-
if
|
206
|
-
|
207
|
-
|
208
|
-
|
234
|
+
# puts sub_obj.class.name
|
235
|
+
if respond_to?(:around_import_save)
|
236
|
+
# Send them the sub_obj even if it might be invalid so they can choose
|
237
|
+
# to make it valid if they wish.
|
238
|
+
# binding.pry
|
239
|
+
around_import_save(sub_obj) do |modded_obj = nil|
|
240
|
+
modded_obj = (modded_obj || sub_obj)
|
241
|
+
modded_obj.save if sub_obj&.valid?
|
209
242
|
end
|
243
|
+
elsif sub_obj&.valid?
|
244
|
+
# binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Andrew'
|
245
|
+
sub_obj.save
|
210
246
|
end
|
211
247
|
end
|
212
248
|
sub_obj = obj
|
213
|
-
this_path = ''
|
249
|
+
this_path = +''
|
214
250
|
v.path.each_with_index do |path_part, idx|
|
215
251
|
this_path << (this_path.blank? ? path_part.to_s : ",#{path_part}")
|
216
252
|
unless (sub_next = sub_objects[this_path])
|
@@ -240,20 +276,36 @@ module DutyFree
|
|
240
276
|
klass.new
|
241
277
|
else
|
242
278
|
# Try to find a unique item if one is referenced
|
243
|
-
|
279
|
+
sub_bt = nil
|
244
280
|
begin
|
245
|
-
|
281
|
+
# Goofs up if trim_prefix isn't the same name as the class, or if it's
|
282
|
+
# a self-join? (like when trim_prefix == 'Reports To')
|
283
|
+
# %%% Need to test this more when the self-join is more than one hop away,
|
284
|
+
# such as importing orders and having employees come along :)
|
285
|
+
# if sub_obj.class == klass
|
286
|
+
# trim_prefix = ''
|
287
|
+
# # binding.pry
|
288
|
+
# end
|
289
|
+
# %%% Maybe instead of passing in "klass" we can give the belongs_to association and build through that instead,
|
290
|
+
# allowing us to nix the klass.new(criteria) line below.
|
291
|
+
trim_prefix = v.titleize[0..-(v.name.length + 2)]
|
292
|
+
trim_prefix << ' ' unless trim_prefix.blank?
|
293
|
+
if klass == sub_obj.class # Self-referencing thing pointing to us?
|
294
|
+
# %%% This should be more general than just for self-referencing things.
|
295
|
+
sub_cols = cols.map { |c| c.start_with?(trim_prefix) ? c[trim_prefix.length..-1] : nil }
|
296
|
+
sub_bt, criteria = klass.find_existing(uniques, sub_cols, starred, import_template, keepers, assoc, row, klass, all, trim_prefix)
|
297
|
+
else
|
298
|
+
sub_bt, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, nil, row, klass, all, trim_prefix)
|
299
|
+
end
|
246
300
|
rescue ::DutyFree::NoUniqueColumnError
|
247
301
|
sub_unique = nil
|
248
302
|
end
|
249
|
-
#
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
#
|
255
|
-
sub_bt = assoc.klass.find_by(criteria) if criteria
|
256
|
-
sub_bt || sub_obj.send("#{path_part}=", klass.new(criteria || {}))
|
303
|
+
# binding.pry if sub_obj.is_a?(Employee) && sub_obj.first_name == 'Nancy' &&
|
304
|
+
# sub_bt.is_a?(Employee)
|
305
|
+
# %%% Can criteria really ever be nil anymore?
|
306
|
+
sub_bt ||= klass.new(criteria || {})
|
307
|
+
sub_obj.send("#{path_part}=", sub_bt)
|
308
|
+
sub_bt # unless klass == sub_obj.class # Don't go further if it's self-referencing
|
257
309
|
end
|
258
310
|
end
|
259
311
|
# Look for possible missing polymorphic detail
|
@@ -266,25 +318,16 @@ module DutyFree
|
|
266
318
|
if sub_next.is_a?(ActiveRecord::Associations::CollectionProxy)
|
267
319
|
# Try to find a unique item if one is referenced
|
268
320
|
# %%% There is possibility that when bringing in related classes using a nil
|
269
|
-
# in
|
321
|
+
# in IMPORT_TEMPLATE[:all] that this will break. Need to test deeply nested things.
|
270
322
|
start = (v.pre_prefix.blank? ? 0 : v.pre_prefix.length)
|
271
323
|
trim_prefix = v.titleize[start..-(v.name.length + 2)]
|
272
|
-
|
273
|
-
|
274
|
-
#
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
if hm_obj.send(k).to_s != v.to_s
|
280
|
-
is_good = false
|
281
|
-
break
|
282
|
-
end
|
283
|
-
end
|
284
|
-
is_good
|
285
|
-
end
|
286
|
-
# Try looking it up through ActiveRecord
|
287
|
-
sub_hm = sub_next.find_by(criteria) if sub_hm.nil?
|
324
|
+
trim_prefix << ' ' unless trim_prefix.blank?
|
325
|
+
klass = sub_next.klass
|
326
|
+
# binding.pry if klass.name == 'OrderDetail'
|
327
|
+
|
328
|
+
# assoc.inverse_of is the belongs_to side of the has_many train we came in here on.
|
329
|
+
sub_hm, criteria = klass.find_existing(uniques, cols, starred, import_template, keepers, assoc.inverse_of, row, sub_next, all, trim_prefix)
|
330
|
+
|
288
331
|
# If still not found then create a new related object using this has_many collection
|
289
332
|
sub_next = sub_hm || sub_next.new(criteria)
|
290
333
|
end
|
@@ -305,11 +348,10 @@ module DutyFree
|
|
305
348
|
next if sub_obj.nil?
|
306
349
|
|
307
350
|
sym = "#{v.name}=".to_sym
|
308
|
-
sub_class = sub_obj.class
|
309
351
|
next unless sub_obj.respond_to?(sym)
|
310
352
|
|
311
|
-
col_type = sub_class.columns_hash[v.name.to_s]&.type
|
312
|
-
if col_type.nil? && (virtual_columns =
|
353
|
+
col_type = (sub_class = sub_obj.class).columns_hash[v.name.to_s]&.type
|
354
|
+
if col_type.nil? && (virtual_columns = import_template[:virtual_columns]) &&
|
313
355
|
(virtual_columns = virtual_columns[this_path] || virtual_columns)
|
314
356
|
col_type = virtual_columns[v.name]
|
315
357
|
end
|
@@ -347,13 +389,18 @@ module DutyFree
|
|
347
389
|
end
|
348
390
|
end
|
349
391
|
|
350
|
-
# Give a window of
|
351
|
-
|
392
|
+
# Give a window of opportunity to tweak user objects controlled by Devise
|
393
|
+
obj_class = obj.class
|
394
|
+
is_do_save = if obj_class.respond_to?(:before_devise_save) && obj_class.name == devise_class
|
395
|
+
obj_class.before_devise_save(obj, existing)
|
396
|
+
else
|
397
|
+
true
|
398
|
+
end
|
352
399
|
|
353
400
|
if obj.valid?
|
354
401
|
obj.save if is_do_save
|
355
402
|
# Snag back any changes to the unique columns. (For instance, Devise downcases email addresses.)
|
356
|
-
existing_unique = valid_unique.keys.inject([]) { |s, v| s << obj.send(v) }
|
403
|
+
existing_unique = valid_unique.keys.inject([]) { |s, v| s << obj.send(v).to_s }
|
357
404
|
# Update the duplicate counts and inserted / updated results
|
358
405
|
counts[existing_unique] << row_num
|
359
406
|
(is_insert ? inserts : updates) << { row_num => existing_unique } if is_do_save
|
@@ -365,65 +412,40 @@ module DutyFree
|
|
365
412
|
errors << { row_num => row_errors } unless row_errors.empty?
|
366
413
|
end
|
367
414
|
end
|
368
|
-
duplicates = counts.
|
369
|
-
s
|
415
|
+
duplicates = counts.each_with_object([]) do |v, s|
|
416
|
+
s += v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
|
417
|
+
s
|
370
418
|
end
|
371
|
-
# Check to see if they want to do anything
|
419
|
+
# Check to see if they want to do anything after the import
|
372
420
|
ret = { inserted: inserts, updated: updates, duplicates: duplicates, errors: errors }
|
373
|
-
if @after_import ||= (
|
421
|
+
if @after_import ||= (import_template[:after_import]) # || some generic after_import)
|
374
422
|
ret = ret2 if (ret2 = @after_import.call(ret)).is_a?(Hash)
|
375
423
|
end
|
376
424
|
end
|
377
425
|
ret
|
378
426
|
end
|
379
427
|
|
380
|
-
# Friendly column names that end up in the first row of the CSV
|
381
|
-
# Required columns get prefixed with a *
|
382
|
-
def friendly_columns(import_columns = self::IMPORT_COLUMNS)
|
383
|
-
requireds = (import_columns[:required] || [])
|
384
|
-
template_columns(import_columns).map do |col|
|
385
|
-
is_required = requireds.include?(col)
|
386
|
-
col = col.to_s.titleize
|
387
|
-
# Alias-ify the full column names
|
388
|
-
aliases = (import_columns[:as] || [])
|
389
|
-
aliases.each do |k, v|
|
390
|
-
if col.start_with?(v)
|
391
|
-
col = k + col[v.length..-1]
|
392
|
-
break
|
393
|
-
end
|
394
|
-
end
|
395
|
-
(is_required ? '* ' : '') + col
|
396
|
-
end
|
397
|
-
end
|
398
|
-
|
399
|
-
# The snake-cased column alias names used in the query to export data
|
400
|
-
def template_columns(import_columns = nil)
|
401
|
-
if @template_import_columns != import_columns
|
402
|
-
@template_import_columns = import_columns
|
403
|
-
@template_detail_columns = nil
|
404
|
-
end
|
405
|
-
@template_detail_columns ||= recurse_def(import_columns[:all], import_columns).first.map(&:to_sym)
|
406
|
-
end
|
407
|
-
|
408
428
|
# For use with importing, based on the provided column list calculate all valid combinations
|
409
429
|
# of unique columns. If there is no valid combination, throws an error.
|
410
|
-
|
411
|
-
|
430
|
+
# Returns an object found by this means.
|
431
|
+
def find_existing(uniques, cols, starred, import_template, keepers, train_we_came_in_here_on, row = nil, obj = nil, all = nil, trim_prefix = '')
|
432
|
+
col_name_offset = trim_prefix.length
|
412
433
|
@valid_uniques ||= {} # Fancy memoisation
|
413
434
|
col_list = cols.join('|')
|
414
435
|
unless (vus = @valid_uniques[col_list])
|
415
436
|
# Find all unique combinations that are available based on incoming columns, and
|
416
437
|
# pair them up with column number mappings.
|
417
|
-
template_column_objects =
|
438
|
+
template_column_objects = ::DutyFree::Extensions._recurse_def(self, all || import_template[:all], import_template).first
|
418
439
|
available = if trim_prefix.blank?
|
419
440
|
template_column_objects.select { |col| col.pre_prefix.blank? && col.prefix.blank? }
|
420
441
|
else
|
421
442
|
trim_prefix_snake = trim_prefix.downcase.tr(' ', '_')
|
422
443
|
template_column_objects.select do |col|
|
423
|
-
|
444
|
+
this_prefix = ::DutyFree::Util._prefix_join([col.pre_prefix, col.prefix], '_').tr('.', '_')
|
445
|
+
trim_prefix_snake == "#{this_prefix}_"
|
424
446
|
end
|
425
447
|
end.map { |avail| avail.name.to_s.titleize }
|
426
|
-
vus = defined_uniques(uniques, cols, starred).select do |k, _v|
|
448
|
+
vus = defined_uniques(uniques, cols, nil, starred).select do |k, _v|
|
427
449
|
is_good = true
|
428
450
|
k.each do |k_col|
|
429
451
|
unless k_col.start_with?(trim_prefix) && available.include?(k_col[col_name_offset..-1])
|
@@ -437,26 +459,91 @@ module DutyFree
|
|
437
459
|
end
|
438
460
|
|
439
461
|
# Make sure they have at least one unique combination to take cues from
|
440
|
-
raise ::DutyFree::NoUniqueColumnError, I18n.t('import.no_unique_column_error') if vus.empty?
|
441
|
-
|
442
|
-
# Convert the first entry to a simplified hash, such as:
|
443
|
-
# {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
|
444
|
-
# to {:name => 8, :email => 9}
|
445
|
-
key, val = vus.first
|
446
462
|
ret = {}
|
447
|
-
|
448
|
-
|
463
|
+
unless vus.empty? # raise NoUniqueColumnError.new(I18n.t('import.no_unique_column_error'))
|
464
|
+
# Convert the first entry to a simplified hash, such as:
|
465
|
+
# {[:investigator_institutions_name, :investigator_institutions_email] => [8, 9], ...}
|
466
|
+
# to {:name => 8, :email => 9}
|
467
|
+
key, val = vus.first
|
468
|
+
key.each_with_index do |k, idx|
|
469
|
+
ret[k[col_name_offset..-1].downcase.tr(' ', '_').to_sym] = val[idx] if k.start_with?(trim_prefix)
|
470
|
+
end
|
449
471
|
end
|
450
|
-
|
472
|
+
|
473
|
+
# %%% If uniqueness is based on something else hanging out on a belongs_to then we're pretty hosed.
|
474
|
+
# (Case in point, importing Order with related Order Detail and Product, and then Product needs to
|
475
|
+
# be found or built first before OrderDetail.)
|
476
|
+
# Might have to do a deferred save kind of thing, and also make sure the product stuff came first
|
477
|
+
# before the other stuff
|
478
|
+
|
479
|
+
# Add in any foreign key stuff we can find from other belongs_to associations
|
480
|
+
# %%% This is starting to look like the other BelongsToAssociation code above around line
|
481
|
+
# 697, so it really needs to be turned into something recursive instead of this two-layer
|
482
|
+
# thick thing at best.
|
483
|
+
bts = reflect_on_all_associations.each_with_object([]) do |sn_assoc, s|
|
484
|
+
if sn_assoc.is_a?(ActiveRecord::Reflection::BelongsToReflection) &&
|
485
|
+
(!train_we_came_in_here_on || sn_assoc != train_we_came_in_here_on) &&
|
486
|
+
sn_assoc.klass != self # Omit stuff pointing to us (like self-referencing stuff)
|
487
|
+
# %%% Make sure there's a starred column we know about from this one
|
488
|
+
ret[sn_assoc.foreign_key] = nil if train_we_came_in_here_on == false
|
489
|
+
s << sn_assoc
|
490
|
+
end
|
491
|
+
s
|
492
|
+
end
|
493
|
+
|
494
|
+
# Find by all corresponding columns
|
495
|
+
criteria = {}
|
496
|
+
if train_we_came_in_here_on != false
|
497
|
+
criteria = ret.each_with_object({}) do |v, s|
|
498
|
+
s[v.first.to_sym] = row[v.last]
|
499
|
+
s
|
500
|
+
end
|
501
|
+
end
|
502
|
+
|
503
|
+
bts.each do |sn_bt|
|
504
|
+
# This search prefix becomes something like "Order Details Product "
|
505
|
+
cols.each_with_index do |bt_col, idx|
|
506
|
+
next unless bt_col.start_with?(trim_prefix + "#{sn_bt.name.to_s.underscore.tr('_', ' ').titleize} ")
|
507
|
+
|
508
|
+
fk_id = if row
|
509
|
+
# Max ID so if there are multiple, only the most recent one is picked.
|
510
|
+
# %%% Need to stack these up in case there are multiple (like first_name, last_name on a referenced employee)
|
511
|
+
sn_bt.klass.where(keepers[idx].name => row[idx]).limit(1).pluck('MAX(id)').first
|
512
|
+
else
|
513
|
+
[sn_bt.klass, keepers[idx].name, idx]
|
514
|
+
end
|
515
|
+
criteria[sn_bt.foreign_key] = fk_id
|
516
|
+
end
|
517
|
+
end
|
518
|
+
|
519
|
+
# Short-circuiting this to only get back the valid_uniques?
|
520
|
+
return ret.merge(criteria) if train_we_came_in_here_on == false
|
521
|
+
|
522
|
+
# With this criteria, find any matching has_many row we can so we can update it
|
523
|
+
sub_hm = obj.find do |hm_obj|
|
524
|
+
is_good = true
|
525
|
+
criteria.each do |k, v|
|
526
|
+
if hm_obj.send(k).to_s != v.to_s
|
527
|
+
is_good = false
|
528
|
+
break
|
529
|
+
end
|
530
|
+
end
|
531
|
+
is_good
|
532
|
+
end
|
533
|
+
# Try looking it up through ActiveRecord
|
534
|
+
# %%% Should we perhaps do this first before the more intensive find routine above?
|
535
|
+
sub_hm = obj.find_by(criteria) if sub_hm.nil?
|
536
|
+
[sub_hm, criteria]
|
451
537
|
end
|
452
538
|
|
453
539
|
private
|
454
540
|
|
455
|
-
def defined_uniques(uniques, cols = [], starred = [])
|
541
|
+
def defined_uniques(uniques, cols = [], col_list = nil, starred = [])
|
542
|
+
col_list ||= cols.join('|')
|
456
543
|
@defined_uniques ||= {}
|
457
|
-
unless (
|
544
|
+
unless (defined_uniq = @defined_uniques[col_list])
|
458
545
|
utilised = {} # Track columns that have been referenced thusfar
|
459
|
-
|
546
|
+
defined_uniq = uniques.each_with_object({}) do |unique, s|
|
460
547
|
if unique.is_a?(Array)
|
461
548
|
key = []
|
462
549
|
value = []
|
@@ -479,42 +566,56 @@ module DutyFree
|
|
479
566
|
end
|
480
567
|
end
|
481
568
|
end
|
482
|
-
(starred - utilised.keys).each { |star|
|
483
|
-
@defined_uniques[
|
569
|
+
(starred - utilised.keys).each { |star| defined_uniq[[star]] = [cols.index(star)] }
|
570
|
+
@defined_uniques[col_list] = defined_uniq
|
484
571
|
end
|
485
|
-
|
572
|
+
defined_uniq
|
486
573
|
end
|
574
|
+
end # module ClassMethods
|
487
575
|
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
|
495
|
-
array = array.inject([]) do |s, col|
|
496
|
-
s += if col.is_a?(Hash)
|
497
|
-
col.inject([]) do |s2, v|
|
498
|
-
joins << { v.first.to_sym => (joins_array = []) }
|
499
|
-
s2 += recurse_def((v.last.is_a?(Array) ? v.last : [v.last]), import_columns, assocs, joins_array, prefixes, v.first.to_sym).first
|
500
|
-
end
|
501
|
-
elsif col.nil?
|
502
|
-
if assocs.empty?
|
503
|
-
[]
|
504
|
-
else
|
505
|
-
# Bring in from another class
|
506
|
-
joins << { prefix => (joins_array = []) }
|
507
|
-
# %%% Also bring in uniques and requireds
|
508
|
-
recurse_def(assocs.last.klass::IMPORT_COLUMNS[:all], import_columns, assocs, joins_array, prefixes).first
|
509
|
-
end
|
510
|
-
else
|
511
|
-
[::DutyFree::Column.new(col, pre_prefix, prefix, assocs, self, import_columns[:as])]
|
512
|
-
end
|
513
|
-
s
|
514
|
-
end
|
515
|
-
[array, joins]
|
576
|
+
# The snake-cased column alias names used in the query to export data
|
577
|
+
def self._template_columns(klass, import_template = nil)
|
578
|
+
template_detail_columns = klass.instance_variable_get(:@template_detail_columns)
|
579
|
+
if (template_import_columns = klass.instance_variable_get(:@template_import_columns)) != import_template
|
580
|
+
klass.instance_variable_set(:@template_import_columns, template_import_columns = import_template)
|
581
|
+
klass.instance_variable_set(:@template_detail_columns, (template_detail_columns = nil))
|
516
582
|
end
|
517
|
-
|
583
|
+
unless template_detail_columns
|
584
|
+
puts "* Redoing *"
|
585
|
+
template_detail_columns = _recurse_def(klass, import_template[:all], import_template).first.map(&:to_sym)
|
586
|
+
klass.instance_variable_set(:@template_detail_columns, template_detail_columns)
|
587
|
+
end
|
588
|
+
template_detail_columns
|
589
|
+
end
|
590
|
+
|
591
|
+
# Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
|
592
|
+
# nested hashes to be used with ActiveRecord's .joins() to facilitate export.
|
593
|
+
def self._recurse_def(klass, array, import_template, assocs = [], joins = [], pre_prefix = '', prefix = '')
|
594
|
+
# Confirm we can actually navigate through this association
|
595
|
+
prefix_assoc = (assocs.last&.klass || klass).reflect_on_association(prefix) if prefix.present?
|
596
|
+
assocs = assocs.dup << prefix_assoc unless prefix_assoc.nil?
|
597
|
+
prefixes = ::DutyFree::Util._prefix_join([pre_prefix, prefix])
|
598
|
+
array = array.inject([]) do |s, col|
|
599
|
+
s + if col.is_a?(Hash)
|
600
|
+
col.inject([]) do |s2, v|
|
601
|
+
joins << { v.first.to_sym => (joins_array = []) }
|
602
|
+
s2 += _recurse_def(klass, (v.last.is_a?(Array) ? v.last : [v.last]), import_template, assocs, joins_array, prefixes, v.first.to_sym).first
|
603
|
+
end
|
604
|
+
elsif col.nil?
|
605
|
+
if assocs.empty?
|
606
|
+
[]
|
607
|
+
else
|
608
|
+
# Bring in from another class
|
609
|
+
joins << { prefix => (joins_array = []) }
|
610
|
+
# %%% Also bring in uniques and requireds
|
611
|
+
_recurse_def(klass, assocs.last.klass::IMPORT_TEMPLATE[:all], import_template, assocs, joins_array, prefixes).first
|
612
|
+
end
|
613
|
+
else
|
614
|
+
[::DutyFree::Column.new(col, pre_prefix, prefix, assocs, klass, import_template[:as])]
|
615
|
+
end
|
616
|
+
end
|
617
|
+
[array, joins]
|
618
|
+
end
|
518
619
|
end # module Extensions
|
519
620
|
|
520
621
|
class NoUniqueColumnError < ActiveRecord::RecordNotUnique
|
@@ -167,7 +167,7 @@ module DutyFree
|
|
167
167
|
# belongs_to is more involved since there may be multiple foreign keys which point
|
168
168
|
# from the foreign table to this primary one, so exclude all these links.
|
169
169
|
_find_belongs_tos(assoc.first.last, assoc.last, errored_assocs).map do |f_assoc|
|
170
|
-
[f_assoc.foreign_key.to_s, f_assoc.active_record]
|
170
|
+
[[f_assoc.foreign_key.to_s], f_assoc.active_record]
|
171
171
|
end
|
172
172
|
end
|
173
173
|
# puts "New Poison: #{new_poison_links.inspect}"
|
@@ -247,11 +247,11 @@ module DutyFree
|
|
247
247
|
end
|
248
248
|
end
|
249
249
|
|
250
|
-
# Show a "pretty" version of
|
250
|
+
# Show a "pretty" version of IMPORT_TEMPLATE, to be placed in a model
|
251
251
|
def self._template_pretty_print(template, indent = 0, child_count = 0, is_hash_in_hash = false)
|
252
252
|
unless indent.negative?
|
253
253
|
if indent.zero?
|
254
|
-
print '
|
254
|
+
print 'IMPORT_TEMPLATE = '
|
255
255
|
else
|
256
256
|
puts unless is_hash_in_hash
|
257
257
|
end
|
@@ -305,7 +305,7 @@ module DutyFree
|
|
305
305
|
if indent == 2
|
306
306
|
puts
|
307
307
|
indent = 0
|
308
|
-
puts '}'
|
308
|
+
puts '}.freeze'
|
309
309
|
elsif indent >= 0
|
310
310
|
print "#{' ' unless child_count.zero?}}"
|
311
311
|
end
|
data/lib/duty_free/util.rb
CHANGED
@@ -57,11 +57,11 @@ module DutyFree
|
|
57
57
|
prefixes.reject(&:blank?).join(separator || '.')
|
58
58
|
end
|
59
59
|
|
60
|
-
def self._clean_name(name,
|
60
|
+
def self._clean_name(name, import_template_as)
|
61
61
|
return name if name.is_a?(Symbol)
|
62
62
|
|
63
63
|
# Expand aliases
|
64
|
-
(
|
64
|
+
(import_template_as || []).each do |k, v|
|
65
65
|
if (k[-1] == ' ' && name.start_with?(k)) || name == k
|
66
66
|
name.replace(v + name[k.length..-1])
|
67
67
|
break
|
@@ -1,2 +1,2 @@
|
|
1
1
|
Description:
|
2
|
-
|
2
|
+
Modifies an existing model so that it includes an IMPORT_TEMPLATE.
|
@@ -4,18 +4,10 @@ require 'rails/generators'
|
|
4
4
|
require 'rails/generators/active_record'
|
5
5
|
|
6
6
|
module DutyFree
|
7
|
-
#
|
7
|
+
# Auto-generates an IMPORT_TEMPLATE entry for one or more models
|
8
8
|
class InstallGenerator < ::Rails::Generators::Base
|
9
9
|
include ::Rails::Generators::Migration
|
10
10
|
|
11
|
-
# Class names of MySQL adapters.
|
12
|
-
# - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`.
|
13
|
-
# - `Mysql2Adapter` - Used by `mysql2` gem.
|
14
|
-
MYSQL_ADAPTERS = [
|
15
|
-
'ActiveRecord::ConnectionAdapters::MysqlAdapter',
|
16
|
-
'ActiveRecord::ConnectionAdapters::Mysql2Adapter'
|
17
|
-
].freeze
|
18
|
-
|
19
11
|
source_root File.expand_path('templates', __dir__)
|
20
12
|
class_option(
|
21
13
|
:with_changes,
|
@@ -68,8 +60,14 @@ module DutyFree
|
|
68
60
|
"[#{major}.#{ActiveRecord::VERSION::MINOR}]"
|
69
61
|
end
|
70
62
|
|
63
|
+
# Class names of MySQL adapters.
|
64
|
+
# - `MysqlAdapter` - Used by gems: `mysql`, `activerecord-jdbcmysql-adapter`.
|
65
|
+
# - `Mysql2Adapter` - Used by `mysql2` gem.
|
71
66
|
def mysql?
|
72
|
-
|
67
|
+
[
|
68
|
+
'ActiveRecord::ConnectionAdapters::MysqlAdapter',
|
69
|
+
'ActiveRecord::ConnectionAdapters::Mysql2Adapter'
|
70
|
+
].freeze.include?(ActiveRecord::Base.connection.class.name)
|
73
71
|
end
|
74
72
|
|
75
73
|
# Even modern versions of MySQL still use `latin1` as the default character
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: duty_free
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Lorin Thwaits
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-11-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|