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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4c9a4a9d552788b5fa4d273e4c000d8d12bbd7f8808eb7904c69bc57c7ccbd61
4
- data.tar.gz: 3e7ccb0be3785814ef2c2bcb488b73d52a956c9ebcc6365ab9a0e3a4160a3924
3
+ metadata.gz: 87d07568c8c7a1692981435e098bd8426d17f30cd733ffff01a7b5a4e4fee4a9
4
+ data.tar.gz: 1d2ef595a271f40e5489cac67e576b4f85338d43d46bff26c6e5a6d706766a00
5
5
  SHA512:
6
- metadata.gz: 10df4b7fbf2d78d2f4c9d24ea318461465e61064697b315e5f84ed92e2d342347a17d0c07e2440858f19453fe0a128524ba70b1adb7c7a564ed84d786ffca7b1
7
- data.tar.gz: 1e377aa74a39e3fe9b3f4accda7ef83535bf3ca2b0fe36de1b3b50289df30a96192d6eca22265e4d6749cf2101b4316cc29211fb450aeb0c2112f84b64f031d8
6
+ metadata.gz: 4bdd6c53f65cb096de1c2da724a735ecee7ceaed464627f69adbb816b3afa20c85a202cce201a7ee5d35e52ae31663c7f8a9c02d47bc11ea7ead86cebfb0972f
7
+ data.tar.gz: 4664e2b6e724f3bba469ef1907b82fe25d90761251ff5cabb204bd75e1a64b522ba3a438438a76e5d58e80952870334abb8194d9d0199b6e2addb2143120b5fa
@@ -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, :import_columns_as
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, import_columns_as)
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.import_columns_as = import_columns_as
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, import_columns_as)],
51
+ [pre_prefix, prefix, ::DutyFree::Util._clean_name(name, import_template_as)],
52
52
  '_'
53
53
  ).tr('.', '_')
54
54
  end
@@ -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, import_columns = nil)
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) && !import_columns
27
- import_columns = is_with_data
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
- import_columns ||= if constants.include?(:IMPORT_COLUMNS)
31
- self::IMPORT_COLUMNS
32
- else
33
- suggest_template(0, false, false)
34
- end
35
- rows = [friendly_columns(import_columns)]
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 = recurse_def(import_columns[:all], import_columns)
39
- relation = joins(template_joins)
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 << template_columns(import_columns).map do |col|
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, import_columns = nil)
67
- import_columns ||= if constants.include?(:IMPORT_COLUMNS)
68
- self::IMPORT_COLUMNS
69
- else
70
- suggest_template(0, false, false)
71
- end
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 = import_columns[: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
- end
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 ||= (import_columns[:before_import]) # || some generic 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 = import_columns[: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
- defined_uniques(uniques, cols, starred)
158
- cols.map! { |col| ::DutyFree::Util._clean_name(col, import_columns[:as]) } # %%%
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 = recurse_def(import_columns[:all], import_columns).first
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 = valid_uniques(uniques, cols, starred, import_columns)
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) { |v, s| s[v[1..-1].map(&:to_s)] = v.first; }
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 << row[v.last].to_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 ||= import_columns[: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
- if sub_obj&.valid?
204
- # %%% Perhaps send them even invalid objects so they can be made valid here?
205
- if around_import_save
206
- around_import_save(sub_obj) do |yes_do_save|
207
- sub_obj.save if yes_do_save && sub_obj&.valid?
208
- end
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
- trim_prefix = v.titleize[0..-(v.name.length + 2)]
279
+ sub_bt = nil
244
280
  begin
245
- sub_unique = assoc.klass.valid_uniques(uniques, cols, starred, import_columns, all, trim_prefix)
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
- # Find by all corresponding columns
250
- criteria = sub_unique&.inject({}) do |s, v|
251
- s[v.first.to_sym] = row[v.last]
252
- s
253
- end
254
- # Try looking up this belongs_to object through ActiveRecord
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 IMPORT_COLUMNS[:all] that this will break. Need to test deeply nested things.
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
- puts sub_next.klass
273
- sub_unique = sub_next.klass.valid_uniques(uniques, cols, starred, import_columns, all, trim_prefix)
274
- # Find by all corresponding columns
275
- criteria = sub_unique.each_with_object({}) { |v, s| s[v.first.to_sym] = row[v.last]; }
276
- sub_hm = sub_next.find do |hm_obj|
277
- is_good = true
278
- criteria.each do |k, v|
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 = import_columns[: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 opportinity to tweak user objects controlled by Devise
351
- is_do_save = before_devise_save(obj, existing) if before_devise_save && obj.class.name == devise_class
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.inject([]) do |s, v|
369
- s + v.last[1..-1].map { |line_num| { line_num => v.first } } if v.last.count > 1
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 before the whole import
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 ||= (import_columns[:after_import]) # || some generic 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
- def valid_uniques(uniques, cols, starred, import_columns, all = nil, trim_prefix = '')
411
- col_name_offset = (trim_prefix.blank? ? 0 : trim_prefix.length + 1)
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 = recurse_def(all || import_columns[:all], import_columns).first
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
- trim_prefix_snake == ::DutyFree::Util._prefix_join([col.pre_prefix, col.prefix], '_').tr('.', '_')
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
- key.each_with_index do |k, idx|
448
- ret[k[col_name_offset..-1].downcase.tr(' ', '_').to_sym] = val[idx]
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
- ret
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 (defined_uniques = @defined_uniques[cols])
544
+ unless (defined_uniq = @defined_uniques[col_list])
458
545
  utilised = {} # Track columns that have been referenced thusfar
459
- defined_uniques = uniques.each_with_object({}) do |unique, s|
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| defined_uniques[[star]] = [cols.index(star)] }
483
- @defined_uniques[cols] = 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
- defined_uniques
572
+ defined_uniq
486
573
  end
574
+ end # module ClassMethods
487
575
 
488
- # Recurse and return two arrays -- one with all columns in sequence, and one a hierarchy of
489
- # nested hashes to be used with ActiveRecord's .joins() to facilitate export.
490
- def recurse_def(array, import_columns, assocs = [], joins = [], pre_prefix = '', prefix = '')
491
- # Confirm we can actually navigate through this association
492
- prefix_assoc = (assocs.last&.klass || self).reflect_on_association(prefix) if prefix.present?
493
- assocs = assocs.dup << prefix_assoc unless prefix_assoc.nil?
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
- end # module ClassMethods
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 IMPORT_COLUMNS, to be placed in a model
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 'IMPORT_COLUMNS = '
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
@@ -57,11 +57,11 @@ module DutyFree
57
57
  prefixes.reject(&:blank?).join(separator || '.')
58
58
  end
59
59
 
60
- def self._clean_name(name, import_columns_as)
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
- (import_columns_as || []).each do |k, v|
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
@@ -5,7 +5,7 @@ module DutyFree
5
5
  module VERSION
6
6
  MAJOR = 1
7
7
  MINOR = 0
8
- TINY = 0
8
+ TINY = 1
9
9
 
10
10
  # PRE is nil unless it's a pre-release (beta, RC, etc.)
11
11
  PRE = nil
@@ -1,2 +1,2 @@
1
1
  Description:
2
- Generates (but does not run) a migration to add a versions table.
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
- # Installs DutyFree in a rails app.
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
- MYSQL_ADAPTERS.include?(ActiveRecord::Base.connection.class.name)
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.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-10-29 00:00:00.000000000 Z
11
+ date: 2020-11-04 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord