duty_free 1.0.0 → 1.0.1
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/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
|